Vue Spring Boot大文件上传

2023-11-16

前言

  • 在项目中,上传大文件往往会遇上很多问题,比如:
    • 1、超时和请求体大小限制;一般我们使用Nginx,它对每个HTTP连接时间和请求内容大小限制的,这些配置值不建议设置很大。
    • 2、网络中断;比如突然断网了,那么整个大文件要重新上传。
    • 3、用户体验差;直接HTTP上传大文件耗时长,用户只能等待,没有友好的上传进度提示。
  • 如果我们将一个大文件拆分成多个文件上传,就能解决上面的问题了。这就是本文主要讲的分片

整体思路

前端

  • 1、将文件转为ArrayBuffer对象。
  • 2、利用SparkMD5根据文件内容生成 hash 值。

SparkMD5 GitHub 地址 https://github.com/satazor/js-spark-md5
安装命令:npm install --save spark-md5

  • 3、设置每个分片大小,生成切片。
  • 4、发送上传切片的请求。

后端

  • 1、接收前端传输的数据,设定好保存的位置。
  • 2、把分片内容追加到文件上。

代码实现

前端

	// 调用这个方法
    async chunkUploadFile(){

      
      try{
      	// 这是File对象
        const fileObj = this.uploadFile
        const buffer = await this.fileToBuffer(fileObj)
        console.log("getFileBuffer ",buffer,fileObj)
       const {hash,chunkList,chunkSize,chunkListLength} = this.createChunk(fileObj,buffer)
       this.sendRequest(chunkList,hash,chunkSize,chunkListLength)
      }catch(e){
        console.log(e)
      }
    },
 // 将 File 对象转为 ArrayBuffer 
    fileToBuffer(file) {
      return new Promise((resolve, reject) => {
        const fr = new FileReader()
        fr.onload = e => {
          resolve(e.target.result)
        }
        fr.readAsArrayBuffer(file)
        fr.onerror = () => {
          reject(new Error('转换文件格式发生错误'))
        }
      })
    },
    

    createChunk(fileObj,buffer){
      // 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量
        const chunkSize = 2097152,
          chunkList = [], // 保存所有切片的数组
          chunkListLength = Math.ceil(fileObj.size / chunkSize), // 计算总共多个切片
          suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1] // 文件后缀名
          
          
        // 根据文件内容生成 hash 值
        const spark = new SparkMD5.ArrayBuffer()
        spark.append(buffer)
        const hash = spark.end()

        // 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)
        let curChunk = 0 // 切片时的初始位置
        for (let i = 0; i < chunkListLength; i++) {
          const item = {
            chunk: fileObj.slice(curChunk, curChunk + chunkSize),
            fileName: `${hash}_${i}.${suffix}`, // 文件名规则按照 hash_1.jpg 命名
            chunkNumber: i,
          }
          curChunk += chunkSize
          chunkList.push(item)
        }

        console.log("chunkList",chunkList)
   
        return {chunkList:chunkList,hash:hash,chunkSize:chunkSize,chunkListLength:chunkListLength}
    },

	// 发送请求是同步的,这样更简单
    sendRequest(chunkList,hash,chunkSize,chunkListLength) {
      let _vm = this 
      const requestList = [] // 请求集合
      chunkList.forEach(item => {
        const fn = () => {
          const formData = new FormData()
          // 分片文件
          formData.append('file', item.chunk)
          // 文件hash值
          formData.append("hash",hash)
          // 把文件放到哪个文件夹下
          formData.append("folder","live")
          // 每个分片大小
          formData.append("chunkSize",chunkSize)
          // 第几个分片
          formData.append("chunkNumber",item.chunkNumber)
          // 分片总数
          formData.append("chunkListLength",chunkListLength)
          // 分片名称
          formData.append('fileName', item.fileName)
          return _vm.$axios({
            url: '/api/common/chunkUploadFile',
            method: 'post',
            headers: { 'Content-Type': 'multipart/form-data' },
            data: formData
          }).then(res => {
            if (res.data.success) { // 成功

              // 进度

              const percentCount = item.chunkNumber / chunkListLength
              console.log("percentCount ",percentCount);
             
           
            }
          })
        }
        requestList.push(fn)
      })
      
      let i = 0 // 记录发送的请求个数
      const send = async () => {
        if (i >= requestList.length) {
          // 发送完毕
          this.uploadComplete()
          return
        } 
        await requestList[i]()
        i++
        send()
      }
      send() // 发送请求
    },
    uploadComplete(){
      console.log("发送视频完毕");
     
    },

后端代码


@ApiModel("分片文件上传对象")
@Data
public class ChunkUploadFileDto {

    @ApiModelProperty(value = "分片文件")
    @NotNull(message = "请上传分片文件")
    @JSONField(serialize = false)
    private MultipartFile file;

    @NotBlank(message = "文件夹不能为空")
    @ApiModelProperty("把分片数据上传到哪个文件夹")
    private String folder;
    @NotBlank(message = "文件hash为空")
    @ApiModelProperty("文件hash")
    private String hash;

    @NotBlank(message = "文件名称不能为空")
    @ApiModelProperty("文件名称")
    private String fileName;

    @NotNull(message = "每个分片大小不能为空")
    @ApiModelProperty("每个分片大小")
    private Integer chunkSize;
    @NotNull(message = "当前分片编号不能为空")
    @ApiModelProperty("当前分片编号")
    private Integer chunkNumber;

    @NotNull(message = "总分片次数不能为空")
    @ApiModelProperty("总分片次数")
    private Integer chunkListLength;
}


@RestController
@RequestMapping("/common")
@Api(tags = "通用处理请求")
public class CommonController {
   @PostMapping("/chunkUploadFile")
    @ApiOperation("分片上传文件")
    public R<Void> chunkUploadFile(@Validated ChunkUploadFileDto chunkUploadFileDto) {
        log.info("分片上传文件 [ {} ]", chunkUploadFileDto.getFileName());
        // FileStorageService  是文件存储策略 ,目前有本地和OSS
        FileStorageService fileStorageService = applicationContext.getBean(sysConfigService.selectConfigByKey(SysConfig.RESOURCE_FILE_CONFIGURE), FileStorageService.class);
        return fileStorageService.chunkUploadFile(chunkUploadFileDto);
    }

}

/**
 * @author Zhou Zhongqing
 * @ClassName FileUploadService
 * @description: 文件存储的service
 * @date 2022-08-15 15:40
 */

public interface FileStorageService {

    public final String UNKNOWN = "unknown";

 

    /***
     * 分片上传文件
     * @param chunkUploadFileDto
     * @return
     */
    R<Void> chunkUploadFile(ChunkUploadFileDto chunkUploadFileDto);


}


// 本地上传策略

/**
 * @author Zhou Zhongqing
 * @ClassName LocalFileStorageServiceImpl
 * @description: 本地存储策略
 * @date 2022-08-15 16:13
 */
@Service(value = "localFileStorageServiceImpl")
public class LocalFileStorageServiceImpl implements FileStorageService {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Resource
    private ISysConfigService sysConfigService;

  

    @Override
    public R<Void> chunkUploadFile(ChunkUploadFileDto chunkUploadFileDto) {
        String extName = StrUtil.DOT + FileUtil.extName(chunkUploadFileDto.getFileName());
        String fileName = (WebSecurityUtils.isLogin() ? WebSecurityUtils.getLoginUser().getId() : UNKNOWN) + Constants.SEPARATOR + chunkUploadFileDto.getFolder() + Constants.SEPARATOR + chunkUploadFileDto.getHash() + extName;
        File dest = new File(FileUtil.getTmpDirPath() + fileName);
        FileUtil.mkdir(dest.getParentFile().getPath());
        appendFileByMappedByteBuffer(dest.getPath(), chunkUploadFileDto);
        if (chunkUploadFileDto.getChunkNumber().equals(chunkUploadFileDto.getChunkListLength() - Constants.ONE)) {
		//TODO 表示上传完了
        }
        return R.ok();
    }

    private boolean appendFileByMappedByteBuffer(String resultFileName, ChunkUploadFileDto param) {
        // 分片上传
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(resultFileName, "rw");
             FileChannel fileChannel = randomAccessFile.getChannel()) {
            // 分片大小必须和前端匹配,否则上传会导致文件损坏
            long chunkSize = param.getChunkSize().longValue();
            // 写入文件
            long offset = chunkSize * param.getChunkNumber();
            byte[] fileBytes = param.getFile().getBytes();
            MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileBytes.length);
            mappedByteBuffer.put(fileBytes);
            // 释放
            unmap(mappedByteBuffer);
        } catch (IOException e) {
//            log.error("文件上传失败:" + e);
            return false;
        }
        return true;
    }

    /**
     * 释放 MappedByteBuffer
     * 在 MappedByteBuffer 释放后再对它进行读操作的话就会引发 jvm crash,在并发情况下很容易发生
     * 正在释放时另一个线程正开始读取,于是 crash 就发生了。所以为了系统稳定性释放前一般需要检
     * 查是否还有线程在读或写
     * 来源:https://my.oschina.net/feichexia/blog/212318
     *
     * @param mappedByteBuffer mappedByteBuffer
     */
    public static void unmap(final MappedByteBuffer mappedByteBuffer) {
        try {
            if (mappedByteBuffer == null) {
                return;
            }
            mappedByteBuffer.force();
            AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
                try {
                    Method getCleanerMethod = mappedByteBuffer.getClass()
                            .getMethod("cleaner");
                    getCleanerMethod.setAccessible(true);
                    Cleaner cleaner =
                            (Cleaner) getCleanerMethod
                                    .invoke(mappedByteBuffer, new Object[0]);
                    cleaner.clean();
                } catch (Exception e) {
                    e.printStackTrace();
//                    log.error("MappedByteBuffer 释放失败:" + e);
                }
//                System.out.println("clean MappedByteBuffer completed");
                return null;
            });
        } catch (Exception e) {
//            log.error("unmap error:" + e);
        }
    }


}


...省略OSS...


执行效果

在这里插入图片描述

在这里插入图片描述

总结

  • 以上代码只使用了1个接口,通过追加数据的方式边上传就边合并分片了。另外的一种方式定义2个接口,一个是上传分片,一个是专门合并分片接口,上传完成后,调用合并分片的接口。
  • 本文使用同步的方式上传分片,效率并不高,只是代码实现更简单。

其他问题

网络中断,分片上传失败怎么办?

方案1:可以在每个分片上传成功后,做1个标记,保存到CookielocalStorage之类的地方,也就是实现断点续传
方案2:把chunkList设置为当前页面对象,每上传成功1个分片就删除1个,重传的逻辑已经是遍历chunkList

如何实现秒传

服务器端对比文件hash值,如果有则直接返回成功。

服务器端多实例的情况

方案1:需要有1个中心服务,单独抽1个服务处理处理上传文件功能,还可以使用第三方服务比如OSS等等。
方案2:如果使用了Nginx直接代理了后端服务,Nginx配置ip的负载均衡策略。
方案3:上传分片使用WebSocket

如何删除无用的分片?

  • 如果上传到一半,用户直接关闭了页面或浏览器,那么已经上传的分片就无用,如果一直存在就浪费空间。

例如在Linux服务器中,/tmp目录是会定期清理的。所以我的想法是先传到类似/tmp机制的目录,这样就省了我们自己清理无用文件的工序,如果这个文件是有效的再移动到其他目录。

参考

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Vue Spring Boot大文件上传 的相关文章

随机推荐

  • PyTorch 08 —预训练模型(迁移学习)

    一 什么是预训练网络 预训练网络是一个保存好的之前已在大型数据集 大规模图像分类任务 上训练好的卷积神经网络 如果这个原始数据集足够大且足够通用 那么预训练网络学到的特征的空间层次结构可以作为有效的提取视觉世界特征的模型 即使新问题和新任务
  • go 语言中通过go get下载包比较慢,解决方法

    1 先下载gopm go get v u github com gpmgo gopm 2 gopm 用法介绍 查看当前工程依赖 gopm list 显示依赖详细信息 gopm list v 列出文件依赖 gopm list t file 拉
  • 执行 conda env create -f *.yml 命令时出现 ResolvePackageNotFound:

    解决办法 将报错的代码注释掉 在后面添加pip 用pip安装
  • 【深度学习】基于Tensorflow的YOLOV4,已跑通程序,效果不错

    完整的程序放在这里了 已经跑通no bug 完整程序 实现效果 自己在colab上训练模型后得到的权重预测 试了几个场景 B站视频链接 识别校门口一号路 识别海盗狗 截图 部分代码如下 voc annotation py import os
  • Dynamics 365 Online-Relevance Search

    区别于Quick Find 以及Full Text Quick Find Dynamics 365 Online有了一个特有的Search功能 Relevance Search 至于为什么是Online特有 是因为这个功能依赖于Azure
  • python中保存mysql字符串不成功问题!

    在python中使用pymysql保存数据到数据库中 代码如下 nowTime int time time insertSql INSERT INTO table name SET ori id s so html s baidu html
  • [LeetCode] Palindrome Number & Valid Palindrome - 回文系列问题

    题目概述 Determine whether an integer is a palindrome Do this without extra space 题目分析 判断数字是否是回文 例如121 656 3443 方法有很多 正着看和到着
  • PostgreSQL 12系统表(10)pg_locks

    PostgreSQL 12系统表 10 pg locks 视图pg locks提供了数据库服务器上活动进程中保持的锁的信息 名称 类型 引用 描述 locktype text 可锁对象的类型 relation extend page tup
  • Vue核心插件 —— Vuex

    Vuex之集成 在项目目录新建store文件夹 推荐项目结构 安装vuex插件 npm i vuex S 在store js文件 中编写入口文件代码 推荐使用 export default gt return new Vuex Store
  • 详解vue中使用echarts地图实现上钻下钻的可视化 三级下钻 省>市>县

    简述功能概要 最近有需求做一个数据可视化的功能 会具体显示全国各地区的买家分布情况 鼠标放置在地图上会显示当前城市的分布人数 点击当前省份会下钻到城市地图 会显示当前省市下各个城市的买家数和分布情况 如果遇到没有下一级再次点击会进行返回到国
  • C++STL模板库——vector容器(上)

    本期介绍基础的vector知识 内容全部在主程序之中 大家自行阅读 include
  • 微信小程序 camera 系统相机 组件

    完整微信小程序 Java后端 技术贴目录清单页面 必看 系统相机 扫码二维码功能 需升级微信客户端至6 7 3 需要用户授权 scope camera 2 10 0起 initdone 事件返回 maxZoom 最大变焦范围 相关接口 Ca
  • react多重判断条件渲染相应组件

    需求来了 多种判断条件下 判断后渲染对应的组件 如果说if else堆叠 那代码会又乱又没有可读性 并且还要渲染对应的组件 最好的思路就是用switch case语句 但是又不想在render里写 那就要借助react的state 是的 r
  • JS深拷贝实现的三种方法

    对象的深拷贝 会另外创建一个一模一样的对象 新对象和原对象不共享内存 修改新对象不会影响原对象 1 递归 function deepClone obj 定义一个变量 并判断是数组还是对象 var objClone Array isArray
  • 260道2023最新网络安全工程师面试题(附答案)

    2023年过去了一大半 先来灵魂三连问 年初定的目标完成多少了 薪资涨了吗 女朋友找到了吗 好了 不扎大家的心了 接下来进入正文 由于我之前写了不少网络安全技术相关的文章和回答 不少读者朋友知道我是从事网络安全相关的工作 于是经常有人私信问
  • jeesite上传返回路径

    lt form fileupload id upload3 returnPath true filePathInputId author fileNameInputId upload3Name uploadType image readon
  • nar神经网络_基于神经网络的预测模型

    基本思想 根据前几次的数据模拟下一次的数据 需要数据具有 周期性 且周期可知 matlab代码 x 54167 55196 56300 57482 58796 60266 61465 62828 64653 65994 67207 6620
  • mllib 协同过滤_使用spark mllib协同过滤进行图书推荐(Java版)

    0 协同过滤算法简介 协同过滤 Collaborative Filtering 简单来说是利用某兴趣相投 拥有共同经验之群体的喜好来推荐用户感兴趣的信息 根据关注内容的不同 协同过滤算法分为三类 以用户为基础 User based 的协同过
  • 7.3 行高:line-height属性[3]

    7 3 4 浏览器的差别与错误 浏览器在显示的时候往往会有自己的表现形式 例如在Opera内 行高将按照CSS定义的将行距除以2增加到内容区域的上下两边 而IE和Firefox则不是完全平分 如图7 29所示 图7 29 不同浏览器对行高的
  • Vue Spring Boot大文件上传

    目录 前言 整体思路 前端 后端 代码实现 前端 后端代码 执行效果 总结 其他问题 网络中断 分片上传失败怎么办 如何实现秒传 服务器端多实例的情况 如何删除无用的分片 参考 前言 在项目中 上传大文件往往会遇上很多问题 比如 1 超时和