分片文件上传的逻辑比较复杂,为了方便代码阅读这里写一下大致流程
- 从前端接收文件和文件的分片信息,生成文件ID;
- 如果是第一个分片,进行如下步骤:
- 根据文件MD5查询数据库是否已经存在该文件;
- 如果存在则为秒传,此时判断用户的剩余空间是否充足;
- 如果空间充足,则保存文件到数据库,即另存一份信息为当前用户上传的文件。
- 判断用户的空间是否充足,不充足直接返回错误信息;
- 空间充足,将分片文件保存到临时目录,同时记录文件上传的累积大小到Redis,此时上传状态为上传中;
- 当上传到最后一个分片时,将文件信息保存到数据库(文件状态设置为转码中),同时更新Redis和数据库中用户的空间使用情况,此时上传状态为完成;
- 使用异步方式进行文件合并。
接口信息
实体类
查询参数DTO
在包com.swx.easypan.entity.dto
,创建 FileUploadDTO 实体类,将下面代码放入:
FileUploadDTO@Data public class FileUploadDTO {
private String id;
@NotEmpty private String filename;
private String filePid;
@NotEmpty private String fileMd5; @NotNull private Integer chunkIndex;
@NotNull private Integer chunks; }
|
视图实体类
在包com.swx.easypan.entity.vo
,创建 UploadResultVO 实体类,将下面代码放入:
UploadResultVO@JsonIgnoreProperties(ignoreUnknown = true) public class UploadResultVO implements Serializable {
private String id; private String status;
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; } }
|
Redis缓存操作
文件上传过程中需要保存临时变量和用户的
文件上传过程中需要保存临时变量
静态常量,在com.swx.easypan.entity.constants
包下的 Constants 类中定义常量
public class Constants { public static final String REDIS_KEY_USER_FILE_TEMP_SIZE = "easypan:user:file:temp:"; }
|
从Redis缓存中获取和保存文件临时信息,在RedisComponent 编写获取和保存方法。
RedisComponent@Component("redisComponent") public class RedisComponent {
@Resource private RedisUtils<Object> redisUtils;
public void saveFileTempSize(String userId, String fileId, Long fileSize) { Long currentSize = getFileTempSize(userId, fileId); String key = Constants.REDIS_KEY_USER_FILE_TEMP_SIZE + userId + fileId; redisUtils.setex(key, currentSize + fileSize, Constants.REDIS_KEY_EXPIRE_ONE_HOUR); }
public Long getFileTempSize(String userId, String fileId) { Object sizeObj = redisUtils.get(Constants.REDIS_KEY_USER_FILE_TEMP_SIZE + userId + fileId); if (sizeObj == null) { return 0L; } if (sizeObj instanceof Integer) { return ((Integer) sizeObj).longValue(); } else if (sizeObj instanceof Long) { return (Long) sizeObj; } return 0L; } }
|
定义Service
文件信息Service
基础的文件信息操作
找到 FileInfoService,定义保存文件信息方法
FileInfoServicepublic interface FileInfoService extends IService<FileInfo> {
boolean saveFileInfo(String userId, String fileId, FileUploadDTO fileDTO);
boolean saveFileInfoFromFile(String userId, String fileId, FileUploadDTO fileDTO, FileInfo dbFile); }
|
在 FileInfoServiceImpl 中,实现上述方法
FileInfoServiceImpl@Slf4j @Service public class FileInfoServiceImpl extends ServiceImpl<FileInfoMapper, FileInfo> implements FileInfoService {
@Override public boolean saveFileInfo(String userId, String fileId, FileUploadDTO fileDTO) { String month = DateFormatUtils.format(new Date(), DateTimePatternEnum.YYYYMM.getPattern()); String fileSuffix = StringTools.getFileSuffix(fileDTO.getFilename()); FileTypeEnums fileTypeEnums = FileTypeEnums.getBySuffix(fileSuffix); String realFileName = userId + fileId + fileSuffix; FileInfo fileInfo = new FileInfo(); fileInfo.setId(fileId); fileInfo.setUserId(userId); fileInfo.setFileMd5(fileDTO.getFileMd5()); fileInfo.setFilename(autoRename(fileDTO.getFilePid(), userId, fileDTO.getFilename())); fileInfo.setFilePath(month + "/" + realFileName); fileInfo.setFilePid(fileDTO.getFilePid()); fileInfo.setFileCategory(fileTypeEnums.getCategory().getCategory()); fileInfo.setFileType(fileTypeEnums.getType()); fileInfo.setStatus(FileStatusEnums.TRANSFER.getStatus()); fileInfo.setFolderType(FileFolderTypeEnums.FILE.getType()); return save(fileInfo); }
@Override public boolean saveFileInfoFromFile(String userId, String fileId, FileUploadDTO fileDTO, FileInfo dbFile) { dbFile.setId(fileId); dbFile.setFilePid(fileDTO.getFilePid()); dbFile.setUserId(userId); dbFile.setStatus(FileStatusEnums.USING.getStatus()); dbFile.setFileMd5(fileDTO.getFileMd5()); dbFile.setDeleted(FileDelFlagEnums.USING.getFlag()); dbFile.setFilename(autoRename(fileDTO.getFilePid(), userId, fileDTO.getFilename())); return save(dbFile); }
}
|
用户文件Service
文件上传的操作涉及文件服务和用户服务,所以这里新加一层用户文件服务,解决循环依赖的问题。
新建 UserFileService,定义上传文件接口
UserFileService
public interface UserFileService { UploadResultVO uploadFile(String userId, MultipartFile file, FileUploadDTO fileDTO) throws IOException; }
|
新建 UserFileServiceImpl 类实现用户文件接口
UserFileServiceImpl@Service public class UserFileServiceImpl implements UserFileService { private final RedisComponent redisComponent; private final FileInfoService fileInfoService; private final UserInfoService userInfoService; private final AppConfig appConfig; private final FileInfoServiceImpl fileInfoServiceImpl; public UserFileServiceImpl(RedisComponent redisComponent, FileInfoService fileInfoService, UserInfoService userInfoService, AppConfig appConfig, FileInfoServiceImpl fileInfoServiceImpl) { this.redisComponent = redisComponent; this.fileInfoService = fileInfoService; this.userInfoService = userInfoService; this.appConfig = appConfig; this.fileInfoServiceImpl = fileInfoServiceImpl; }
@Override @Transactional(rollbackFor = Exception.class) public UploadResultVO uploadFile(String userId, MultipartFile file, FileUploadDTO fileDTO) throws IOException { UploadResultVO resultVO = new UploadResultVO(); File tempFileFolder = null; try { String fileId = fileDTO.getId(); if (!StringUtils.hasText(fileId)) { fileId = StringTools.getRandomString(Constants.LENGTH_10); } resultVO.setId(fileId);
UserSpaceDTO userSpaceUse = redisComponent.getUserSpaceUse(userId); if (userSpaceUse == null) { userSpaceUse = getUseSpace(userId); } if (fileDTO.getChunkIndex() == 0) { List<FileInfo> dbFileList = fileInfoService.list(new LambdaQueryWrapper<FileInfo>() .eq(FileInfo::getFileMd5, fileDTO.getFileMd5()) .eq(FileInfo::getStatus, FileStatusEnums.USING.getStatus())); if (!dbFileList.isEmpty()) { FileInfo dbFile = dbFileList.get(0); if (dbFile.getFileSize() + userSpaceUse.getUseSpace() > userSpaceUse.getTotalSpace()) { throw new BizException(ResultCode.OUT_OF_SPACE); } boolean save = fileInfoService.saveFileInfoFromFile(userId, fileId, fileDTO, dbFile); if (!save) { throw new BizException("文件保存失败"); } resultVO.setStatus(UploadStatusEnums.UPLOAD_SECONDS.getCode()); updateUserSpace(userId, dbFile.getFileSize(), userSpaceUse); return resultVO; } } Long currentTempSize = redisComponent.getFileTempSize(userId, fileId); if (file.getSize() + currentTempSize + userSpaceUse.getUseSpace() > userSpaceUse.getTotalSpace()) { throw new BizException(ResultCode.OUT_OF_SPACE); } String tempFolderName = appConfig.getProjectFolder() + Constants.FILE_FOLDER_TEMP; String currentUserFolderName = userId + fileId;
tempFileFolder = new File(tempFolderName + currentUserFolderName); if (!tempFileFolder.exists()) { tempFileFolder.mkdirs(); } File newFile = new File(tempFileFolder.getPath() + "/" + fileDTO.getChunkIndex()); file.transferTo(newFile); if (fileDTO.getChunkIndex() < fileDTO.getChunks() - 1) { resultVO.setStatus(UploadStatusEnums.UPLOADING.getCode()); redisComponent.saveFileTempSize(userId, fileId, file.getSize()); return resultVO; } redisComponent.saveFileTempSize(userId, fileId, file.getSize()); fileInfoService.saveFileInfo(userId, fileId, fileDTO); Long totalSize = redisComponent.getFileTempSize(userId, fileId); updateUserSpace(userId, totalSize, userSpaceUse); resultVO.setStatus(UploadStatusEnums.UPLOAD_FINISH.getCode());
String finalFileId = fileId; TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { fileInfoServiceImpl.transferFile(finalFileId, userId); } }); return resultVO; } catch (Exception e) { if (tempFileFolder != null) { FileUtils.deleteDirectory(tempFileFolder); } throw e; } }
private void updateUserSpace(String userId, Long fileSize, UserSpaceDTO userSpaceDTO) { Boolean update = userInfoService.updateUserSpace(userId, fileSize, null); if (!update) { throw new BizException(ResultCode.OUT_OF_SPACE); } userSpaceDTO.setUseSpace(userSpaceDTO.getUseSpace() + fileSize); redisComponent.saveUserSpaceUse(userId, userSpaceDTO); } }
|
定义Controller
在 FileInfoController 下定义上传方法,分片上传。
FileInfoController@RestController @RequestMapping("/file") @ResponseResult @LoginValidator @Validated public class FileInfoController {
private final UserFileService userFileService;
public FileInfoController(UserFileService userFileService) { this.userFileService = userFileService; }
@PostMapping("/uploadFile") public UploadResultVO uploadFile(HttpSession session, MultipartFile file, FileUploadDTO fileDTO) throws IOException { SessionWebUserVO user = (SessionWebUserVO) session.getAttribute(Constants.SESSION_KEY); return userFileService.uploadFile(user.getId(), file, fileDTO); } }
|