springboot 大文件分片上传、断点续传和秒传

目录

前置说明

获取文件分片

项目流程简述

关键代码解读

表设计SQL

接口测试

测试项目获取地址


前置说明

目前没弄前端,搁置后续再说。前端若打算使用element-ui的el-upload改造分片上传组件的,推荐这篇文章

获取文件分片

后端自测使用的分片可以通过ChunkFile来获取。

public class ChunkFile {
    private static final String PATH = "D:/file/test/";
    private static final String FILE_NAME = "稻香-周杰伦";
    private static final String FILE_EXTENSION = ".mp4";
    private static final Integer CHUNK_SIZE = 10485760; // 10MB

    public static void main(String[] args) throws Exception {
        File file = new File(PATH, FILE_NAME + FILE_EXTENSION);
        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
        long size = FileUtil.size(file);
        // 分片数
        int chunkNum = (int) Math.ceil((double) size / CHUNK_SIZE);
        byte[] bytes;
        // 循环进行文件读取并写入数据至分片文件
        for (int i = 0; i < chunkNum; i++) {
            // 文件当前偏移量
            long pointer = randomAccessFile.getFilePointer();
            int len = i == chunkNum - 1 ? (int) (size - pointer) : CHUNK_SIZE;
            bytes = new byte[len];
            randomAccessFile.read(bytes, 0, len);
            FileUtil.writeBytes(bytes, new File(PATH, i + FILE_EXTENSION));
        }
    }
}

测试文件大小14.5MB,CHUNK_SIZE分片大小设置为10MB。通过RandomAccessFile进行数据读取,通过hutool的FileUtil工具类进行数据写入,得到下面的0.mp4和1.mp4两个分片文件。

项目流程简述

该测试项目实现的功能:文件普通上传、文件分片上传、断点续传、秒传。文件普通上传不谈,这里展开分片逻辑进行讨论。

第一步:前端在进行大文件上传之前,先通过MD5算法(MD5不是加密算法)获取大文件的MD5值( 每个文件的MD5都不一样),将MD5值作为参数调用后端秒传检测接口——目的就是在文件上传前判断文件是否已经上传。此步好处->

        好处一:如果文件已经上传过,则不用再上传,文件地址复用即可;

        好处二:如果大文件是分片上传的,并且因为某些原因,只上传了部分分片,则接口返回已上传的分片信息,前端可以根据信息判断哪些分片还没有上传,只上传还没有上传的分片即可(断点续传)。

第二步:根据第一步的接口响应信息,判断是否需要进行文件上传。若要上传,调用文件上传接口进行文件或分片文件的上传。(每个分片文件也都需要其MD5值进行校验,分片文件合并在后端自动进行)

关键代码解读

有文件上传记录sys_file和分片记录sys_chunk_record两张表,在下面的文字中分表叫做A表和B表。

一、秒传检测代码

public UploadResultVo fastUploadCheck(Boolean isChunk, String md5) {
    UploadResultVo vo = new UploadResultVo();

    // 文件表查找
    QueryWrapper<SysFile> wrapper = new QueryWrapper<SysFile>()
            .eq("file_md5", md5);
    List<SysFile> sysFileList = sysFileMapper.selectList(wrapper);
    if (sysFileList.size() > 0) {
        String kid = sysFileList.get(0).getKid();
        vo.setUploaded(true).setFileKid(kid);
    }
    if (!vo.getUploaded() && isChunk) {
        // 分片记录表查询
        QueryWrapper<SysChunkRecord> chunkWrapper = new QueryWrapper<SysChunkRecord>()
                .eq("file_md5", md5);
        List<SysChunkRecord> sysChunkRecordList = sysChunkRecordMapper.selectList(chunkWrapper);
        List<Integer> chunkNum = new ArrayList<>();
        if (CollectionUtil.isNotEmpty(sysChunkRecordList)) {
             chunkNum = sysChunkRecordList.stream().map(SysChunkRecord::getChunk).collect(Collectors.toList());
        }
        vo.setChunkNum(chunkNum);
    }
    return vo;
}

isChunk和md5都是秒传检测接口请求传递的参数,含义分别是——是否分片和文件md5值。

isChunk为false:只查询A表,selectList()方法如果有数据返回,则说明之前有上传过相同的文件。设置返回参数——uploaded为true,fileKid为文件记录主键。(这里其实使用mp的selectOne()方法即可,不用使用selectList(),因为表已经做了md5字段的唯一约束)

isChunk为true:查询A表的逻辑不变,另还查询了B表。此处的代码对应上面描述的好处二——告诉前端已经上传成功了哪些分片。

二、分片文件上传代码

public UploadResultVo chunkUpload(MultipartFileParam param) {
    UploadResultVo vo = new UploadResultVo();

    // 因为表设置了唯一约束,在插入数据前先判断数据是否已经存在
    SysFile sf = queryByMd5(param.getMd5());
    if (ObjectUtil.isNotEmpty(sf)) {
        return vo.setUploaded(true).setFileKid(sf.getKid());
    }
    SysChunkRecord scr = queryByChunkMd5(param.getChunkMd5());
    if (ObjectUtil.isNotEmpty(scr)) {
        return vo.setUploaded(false);
    }

    File file = buildUploadFile(param);
    MultipartFile multipartFile = param.getFile();
    try {
        // 分片文件md5校验
        checkMd5(multipartFile.getInputStream(), param.getChunkMd5());
        // 分片数据写入文件
        RandomAccessFile accessFile = new RandomAccessFile(file, "rw");
        if (accessFile.length() == 0) {
            accessFile.setLength(param.getTotalSize());
        }
        FileChannel channel = accessFile.getChannel();
        int position = (param.getChunk() - 1) * fileConfig.getChunkSize();
        MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, position, multipartFile.getSize());
        map.put(multipartFile.getBytes());
        cleanBuffer(map);
        channel.close();
        accessFile.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 分片记录入库
    saveSysChunkRecord(file, param);

    // 检测是否为最后一块分片
    QueryWrapper<SysChunkRecord> wrapper1 = new QueryWrapper<SysChunkRecord>()
            .eq("file_md5", param.getMd5());
    Integer count = sysChunkRecordMapper.selectCount(wrapper1);
    if (count.equals(param.getTotalChunk())) {
        try {
            // 文件md5检验
            checkMd5(new FileInputStream(file), param.getMd5());
            // 文件上传记录入库
            String kid = saveSysFile(file, param);
            return vo.setUploaded(true).setFileKid(kid);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            // 清除分片记录
            cleanChunkData(param.getMd5());
        }
    }
    return vo.setUploaded(false);
}

MultipartFileParam是对文件上传参数的封装,有是否为分片上传、文件、文件名、md5值、分片数等参数。

先使用md5值查询A表和B表,检查是否已经上传过;然后对当前分片的chunkMd5进行校验,校验通过则通过RandomAccessFile进行数据的写入;写入完成后当前分片记录入库;最后判断当前分片是否是最后一块分片,如果是,则校验完整文件的md5,并完成文件记录的入库和分片记录的删除。

注:buildUploadFile()方法——创建文件上传目录和上传文件。

表设计SQL

CREATE TABLE `sys_file` (
  `kid` varchar(36) NOT NULL COMMENT 'kid',
  `file_name` varchar(255) NOT NULL COMMENT '文件名称',
  `extension` varchar(255) DEFAULT NULL COMMENT '文件扩展名',
  `file_size` bigint DEFAULT NULL COMMENT '文件大小',
  `file_path` varchar(255) NOT NULL COMMENT '文件存放路径',
  `file_md5` varchar(255) NOT NULL COMMENT '文件md5',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`kid`),
  UNIQUE KEY `idx_file_md5` (`file_md5`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='附件表';

CREATE TABLE `sys_chunk_record` (
  `kid` varchar(36) NOT NULL COMMENT 'kid',
  `chunk_file_name` varchar(255) NOT NULL COMMENT '文件名称',
  `chunk_file_path` varchar(255) NOT NULL COMMENT '文件存放路径',
  `file_md5` varchar(255) NOT NULL COMMENT '文件md5',
  `chunk_file_md5` varchar(255) NOT NULL COMMENT '文件md5',
  `chunk` int NOT NULL COMMENT '第几块分片',
  `total_chunk` int NOT NULL COMMENT '总分片数量',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`kid`),
  UNIQUE KEY `idx_chunk_file_md5` (`chunk_file_md5`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='附件表';

接口测试

测试工具:Apifox,测试时间:2022/10/21

先上传测试文件的第二块分片,sys_chunk_record表中保存了该分片上传记录;

 

此时调用秒传校验接口,可看见返回的分片数据;

然后上传测试文件的第一块分片,sys_chunk_record表中保存了该分片上传记录。

因为该分片为最后一块分片,sys_file表中会保存文件上传记录并清除sys_chunk_record表中的分片记录。

 

上传目录使用年月日格式,文件名是传递的参数fileName。

:测试工具自测时,md5值的获取方式——打开cmd,使用certutil -hashfile命令。如:

测试项目获取地址

——代码结构

https://gitee.com/dlqx/springboot-code-book中的bigfile-up-down文件夹。

猜你喜欢

转载自blog.csdn.net/qq_38058241/article/details/127430749