文章目录

  • 前言
  • 整体过程
  • 流程图
  • 数据库设计
  • 一、前端
  • 计算分片
  • 文件上传
  • 合并文件
  • 二、后端
  • 1.检查文件接口
  • 2.上传文件接口
  • 3.合并文件接口
  • 三、测试
  • 断点续传
  • 秒传
  • 总结



前言

文件断点续传和秒传是基于分片上传实现,本次基于SpringBoot和Vue实现。

整体过程

1、前端将文件分片并将分片信息提交给后端保存
2、后端将前端每次上传的文件,放入到缓存目录
3、等待前端将全部的文件内容都上传完毕后,发送一个合并请求
4、后端合并文件

流程图

springboot 断点nacos 配置_上传

数据库设计

springboot 断点nacos 配置_vue.js_02


springboot 断点nacos 配置_List_03

一、前端

安装vue-simple-uploader、spark-md5依赖
vue-simple-uploader用来快速实现文件上传页面
spark-md5用来计算分片信息

计算分片

springboot 断点nacos 配置_vue.js_04

文件上传

springboot 断点nacos 配置_合并文件_05

合并文件

springboot 断点nacos 配置_上传_06

二、后端

1.检查文件接口

前端分片完成之后请求后端根据md5检查当前文件是否上传过,如果有上传过,判断是否已经上传完成,若上传完成则可以实现秒传,若没有上传过则继续上次上传的位置开始上传即实现分片上传。

public FileChunkCheckResponse checkFileChunk(FileChunkRequest fileChunkRequest) {
        SysFileList sysFileList = sysFileListService.getByMd5(fileChunkRequest.getIdentifier());
        // 根据md5值查询文件是否已经上传过、则可以秒传
        if(Objects.nonNull(sysFileList)){
            return FileChunkCheckResponse.builder().skipUpload(Boolean.TRUE).build();
        }
        // 先检查文件是否已上传
        List<SysChunk> chunkList = this.getChunkList(fileChunkRequest.getIdentifier());
        List<Integer> chunkNumberList = chunkList.stream().mapToInt(SysChunk::getChunkNumber).boxed().collect(Collectors.toList());
        return FileChunkCheckResponse.builder().skipUpload(false).uploaded(chunkNumberList).build();
    }

2.上传文件接口

能走到这个接口说明文件还没上传完,判断是否存在临时文件夹不存在则创建,然后保存当前分片信息到文件分片表,最后校验是否所有分片都上传完成,返回标识给前端。

public FileChunkCheckResponse upload(FileChunkRequest fileChunkRequest) throws IOException {
        String path = chunkPath + File.separator + fileChunkRequest.getIdentifier();
        boolean exist = FileUtil.exist(path);
        if (!exist) {
            // 创建文件夹
            FileUtil.mkdir(path);
        }
        // 保存文件
        FileUtil.writeBytes(fileChunkRequest.getFile().getBytes(), path + File.separator + fileChunkRequest.getChunkNumber());
        // 保存分片信息
        SysChunk sysChunk = new SysChunk();
        sysChunk.setChunkNumber(fileChunkRequest.getChunkNumber());
        sysChunk.setChunkSize(fileChunkRequest.getChunkSize());
        sysChunk.setCurrentChunkSize(fileChunkRequest.getCurrentChunkSize());
        sysChunk.setFilename(fileChunkRequest.getFilename());
        sysChunk.setIdentifier(fileChunkRequest.getIdentifier());
        sysChunk.setRelativePath(path);
        sysChunk.setTotalChunks(fileChunkRequest.getTotalChunks());
        sysChunk.setTotalSize(fileChunkRequest.getTotalSize());
        sysChunkService.save(sysChunk);
        // 查询所有分块
        List<SysChunk> chunkList = sysChunkService.getChunkList(fileChunkRequest.getIdentifier());
        List<Integer> chunkNumberList = chunkList.stream().mapToInt(SysChunk::getChunkNumber).boxed().collect(Collectors.toList());
        FileChunkCheckResponse response = FileChunkCheckResponse.builder().uploaded(chunkNumberList).build();
        // 是否上传完成
        if (chunkNumberList.size() == fileChunkRequest.getTotalChunks()) {
            response.setNeedMerge(Boolean.TRUE);
        }
        // 是否需要添加当前分块
        return response;
    }

3.合并文件接口

当前端接收到后端返回的合并标识后,再请求文件合并接口。
后端使用RandomAccessFile来合并所有分片文件、并删除临时文件夹的文件,并将文件信息存入文件信息表,当再次上传相同文件时就可以通过查询表并读取相应文件实现文件秒传。

public boolean merge(FileChunkRequest fileChunkRequest) {
        Integer totalChunks = fileChunkRequest.getTotalChunks();
        String chunkFile = chunkPath + File.separator + fileChunkRequest.getIdentifier();
        if (checkChunk(totalChunks, chunkFile)) {
            // 读取所有分片文件
            String file = uploadPath + File.separator + fileChunkRequest.getFilename();
            File chunkFileFolder = new File(chunkFile);
            // 要合并生成的文件
            File mergeFile = new File(file);
            File[] chunks = chunkFileFolder.listFiles();
            // 切片排序
            assert chunks != null;
            List<File> fileList = Arrays.asList(chunks);
            fileList.sort(Comparator.comparingInt((File o) -> Integer.parseInt(o.getName())));
            try {
                RandomAccessFile randomAccessFileWriter = new RandomAccessFile(mergeFile, "rw");
                byte[] bytes = new byte[1024];
                for (File chunk : chunks) {
                    RandomAccessFile randomAccessFileReader = new RandomAccessFile(chunk, "r");
                    int len;
                    while ((len = randomAccessFileReader.read(bytes)) != -1) {
                        randomAccessFileWriter.write(bytes, 0, len);
                    }
                    randomAccessFileReader.close();
                }
                randomAccessFileWriter.close();
            } catch (Exception ignored) {
                return Boolean.FALSE;
            }
            // 删除分片文件夹
            FileUtil.del(chunkFile);
            // 清空分片信息
            sysChunkService.removeByFileIdentifier(fileChunkRequest.getIdentifier());
            // 计算文件总大小

            // 保存文件信息
            SysFileList sysFileList = new SysFileList();
            sysFileList.setFilename(fileChunkRequest.getFilename());
            sysFileList.setIdentifier(fileChunkRequest.getIdentifier());
            sysFileList.setLocation(file);
            sysFileList.setTotalSize(fileChunkRequest.getTotalSize());
            sysFileListService.save(sysFileList);
        }
        return Boolean.TRUE;
    }

三、测试

断点续传

在文件上传到一半的时候取消上传,并且再次上传可以看到是从中间部分开始重新上传的

springboot 断点nacos 配置_上传_07

秒传

可以看到已经上传的文件可以直接上传成功

springboot 断点nacos 配置_List_08


总结

代码已经上传到gitee需要自取(可以的话麻烦点个star,感谢):https://gitee.com/xyzissj/study/tree/master/fileUpload

以上仅是简单实现文件断点续传、秒传,实际业务系统中文件上传时还需要校验授权信息等,还可以改进为多线程提高上传效率。