【网盘系统】文件分片上传

1. 什么是分片上传?

分片上传是将大文件分割成多个小片段,分别上传到服务器,最后在服务器端将这些片段合并成完整文件的上传方式。这种方式可以实现断点续传、并行上传等功能,提高大文件上传的可靠性和效率。

2. 实现原理

分片上传的基本流程:

  1. 客户端将文件分割成固定大小的片段

  2. 每个片段都有一个序号(chunkIndex)

  3. 服务器接收到片段后存储到临时目录

  4. 所有片段上传完成后进行合并

3. 代码实现分析

3.1 Controller层实现

java 复制代码
@RequestMapping("/uploadFile")
public ResponseVO uploadFile(HttpSession session, String fileId, MultipartFile file, 
                           @NotEmpty String fileName, @NotEmpty String filePid, 
                           @NotEmpty String fileMd5, @NotNull Integer chunkIndex, 
                           @NotNull Integer chunks) {
    SessionWebUserDto webUserDto = getUserInfoFromSession(session);
    UploadResultDto resultDto = fileInfoService.uploadFile(webUserDto, fileId, file, 
                                                         fileName, filePid, fileMd5, 
                                                         chunkIndex, chunks);
    return getSuccessResponseVO(resultDto);
}

主要参数说明:

  • fileId: 文件唯一标识

  • fileMd5: 文件的MD5值,用于秒传判断

  • filePid: 父文件夹ID

  • fileName: 文件名称

  • chunkIndex:文件分片的索引

  • chunks:文件分片的总数

3.2 Service层实现

java 复制代码
    @Override
    @Transactional(rollbackFor = Exception.class)
    public UploadResultDto uploadFile(SessionWebUserDto webUserDto, String fileId, MultipartFile file, String fileName,
                                      String filePid, String fileMd5, Integer chunkIndex, Integer chunks) {

        UploadResultDto resultDto = new UploadResultDto();
        // 用户的临时文件暂存路径
        File uerTempDirPath = null;
        boolean uploadSuccess = true;
        try {
            if(StrUtil.isEmpty(fileId)){
                fileId = RandomUtil.randomString(Constants.LENGTH_10);
            }
            resultDto.setFileId(fileId);
            Date curDate = new Date();
            // 获取用户已使用的空间
            UserSpaceDto userSpaceDto = redisComponent.getUserSpaceUse(webUserDto.getUserId());
            if(chunkIndex == 0) {
                // 第一次上传(第一个分片),去数据库中查找有没有这个文件,如果有,则可秒传
                // ...妙传逻辑在此处省略,可查看我的另一篇博客
                    return resultDto;
                }
            }
            // 没找到,进行分片上传
            // 判断网盘空间
            Long currentTempSize = redisComponent.getFileTempSize(webUserDto.getUserId(), fileId);
            if (file.getSize() + currentTempSize + userSpaceDto.getUseSpace() > userSpaceDto.getTotalSpace()) {
                // 网盘空间不足
                throw new BusinessException(ResponseCodeEnum.CODE_904);
            }
            // 临时文件暂存目录
            String tempDirPath = appConfig.getProjectFolder() + Constants.FILE_FOLDER_TEMP;
            String userFileFolderName = webUserDto.getUserId() + fileId;
            // 用户的临时文件暂存路径
            uerTempDirPath = new File(tempDirPath + userFileFolderName);
            if(!uerTempDirPath.exists()){
                // 如果不存在,则创建
                uerTempDirPath.mkdirs();
            }
            // 分片文件
            File chunkFile = new File(uerTempDirPath.getPath() + "/" +chunkIndex);
            file.transferTo(chunkFile);
                // 保存临时文件大小
                redisComponent.saveFileTempSize(webUserDto.getUserId(), fileId, file.getSize());
            if(chunkIndex < chunks - 1){
                // 不是最后一个分片
                resultDto.setStatus(UploadStatusEnums.UPLOADING.getCode());
                return resultDto;
            }
            // 最后一个分片上传完成,记录数据库,异步合并分片
            String month = DateUtil.format(curDate, "yyyyMM");
            String fileSuffix = StringTools.getFileSuffix(fileName);
            // 真实文件名
            String realFileName = userFileFolderName + fileSuffix;
            FileTypeEnums fileTypeEnums = FileTypeEnums.getFileTypeBySuffix(fileSuffix);
            // 自动重命名
            fileName = autoRename(fileName, filePid, webUserDto.getUserId());
            // 将文件写入数据库
            FileInfo fileInfo = new FileInfo();
            fileInfo.setFileId(fileId);
            fileInfo.setUserId(webUserDto.getUserId());
            fileInfo.setFileMd5(fileMd5);
            fileInfo.setFileName(fileName);
            fileInfo.setFilePath(month + "/" + realFileName);
            fileInfo.setFilePid(filePid);
            fileInfo.setCreateTime(curDate);
            fileInfo.setLastUpdateTime(curDate);
            fileInfo.setFileCategory(fileTypeEnums.getCategory().getCode());
            fileInfo.setFileType(fileTypeEnums.getType());
            fileInfo.setStatus(FileStatusEnums.TRANSFER.getStatus());
            fileInfo.setFolderType(FileFolderTypeEnums.FILE.getType());
            fileInfo.setDelFlag(FileDelFlagEnums.USING.getFlag());
            this.baseMapper.insert(fileInfo);
            // 更新用户使用空间
            Long useSpace = redisComponent.getFileTempSize(webUserDto.getUserId(), fileId);
            updateUserSpace(webUserDto, useSpace);
            resultDto.setStatus(UploadStatusEnums.UPLOAD_FINISH.getCode());
            // 异步合并分片
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    fileInfoService.transferFile(fileInfo.getFileId(), webUserDto);
                }
            });
            return resultDto;
        }catch (BusinessException e) {
            log.error("上传文件失败", e);
            throw e;
        } catch (IOException e) {
            log.error("上传文件失败", e);
            uploadSuccess = false;
        }finally {
            if(uerTempDirPath != null && !uploadSuccess){
                FileUtil.del(uerTempDirPath);
            }
        }
        return resultDto;
    }

3.3 临时文件大小管理

使用Redis管理临时文件大小:

java 复制代码
@Component
public class RedisComponent {
    
    /**
     * 获取临时文件大小
     */
    public Long getFileTempSize(String userId, String fileId) {
        Long currentSize = getFileSize(Constants.REDIS_KEY_USER_FILE_TEMP_SIZE + 
                                     userId + fileId);
        return currentSize;
    }
    
    /**
     * 保存临时文件大小
     */
    public void saveFileTempSize(String userId, String fileId, Long fileSize) {
        Long currentSize = getFileTempSize(userId, fileId);
        stringRedisTemplate.opsForValue().set(
            Constants.REDIS_KEY_USER_FILE_TEMP_SIZE + userId + fileId,
            Convert.toStr(currentSize + fileSize),
            Constants.REDIS_KEY_EXPIRES_ONE_HOUR,
            TimeUnit.SECONDS
        );
    }
}

4. 核心技术要点

4.1 分片管理

  • 使用chunkIndex标识分片序号

  • 使用临时目录管理分片文件

4.2 空间管理

  • 实时统计临时文件占用空间

  • 使用Redis缓存临时文件大小

  • 定期清理过期的临时文件

4.3 文件存储

  • 按用户ID和文件ID创建临时目录

  • 分片文件使用序号命名

  • 支持并发上传多个文件

相关推荐
骷大人17 分钟前
mysql开启配置binlog
数据库·mysql
Yolo_nn18 分钟前
MySQL_第14章_存储过程与函数
数据库·mysql·存储过程·函数
GDDGHS_27 分钟前
Flink自定义数据源
大数据·数据库·flink
Milk夜雨1 小时前
数据库进阶教程:结合编程实现动态数据操作
数据库·python·adb
weisian1511 小时前
Redis篇-5--原理篇4--Lua脚本
数据库·redis·lua
是十一月末1 小时前
Linux的基本功能和命令
linux·服务器·开发语言·数据库
MavenTalk1 小时前
Spring Cloud Alibaba:一站式微服务解决方案
java·数据库·spring boot·spring cloud·微服务·netflix
荼蘼_1 小时前
宝塔内设置redis后,项目以及RedisDesktopManager客户端连接不上!
数据库·redis·缓存
打码人的日常分享2 小时前
【网络安全资料文档】网络安全空间态势感知系统建设方案,网络安全数据采集建设方案(word原件)
数据库·安全·web安全·需求分析·规格说明书
发光者2 小时前
Maven、mybatis框架
java·数据库·maven·mybatis