首页 > 其他分享 >实现多个大文件拖拽上传+大文件分片上传+断点续传+文件预览

实现多个大文件拖拽上传+大文件分片上传+断点续传+文件预览

时间:2023-11-17 16:22:06浏览次数:37  
标签:文件 const 断点续传 filename 分片 nameHash 上传

技术关键词

前端:@vue/cli-service+element-ui+axios
后端:node.js+koa

思路分析

拖拽上传

拖拽上传是利用 HTML5 新特性实现拖拽上传,详细用法可阅读 MDN-drag
利用 dragover 事件(当某物被拖动的对象在另一对象容器范围内拖动时触发此事件)和 drop 事件(在一个拖动过程中,释放鼠标键时触发此事件)来处理文件。

文件分片上传

文件分片的大体思路就是前端将大文件拆成一小片的文件,发送给后端,后端进行保存分片的文件,然后当完成所有分片文件的上传之后,前端通过调用后端的合并接口来通知后端将保存的分片文件进行读取写入新的文件里。

在实现文件分片上传之前,前端需要先思考如下问题:
如何获取用户选择的文件
获取到的大文件如何进行分片以及要分多大
一个大文件拆成的小分片如何区别先后顺序,让后端可知先读哪个文件
如何区分多个不同大文件
用户上传多个大文件,前端通过什么来保存每个大文件所对应的小文件切片
如何将每个小分片的数据发送给后端
如何通知后端所有切片都上传完成

在实现文件分片上传之前,后端需要先思考如下问题:
保存分片的接口,需要前端给予哪一些字段,来区分不同文件的保存
合并文件接口,通过什么方式读文件生成新文件

当这些问题的思路清晰的时候,实现起来就不难了。
首先获取文件可通过 change 事件来获取文件,实现多文件上传可通过给 input 标签设置属性 multiple。当获取到的大文件之后,要对大文件进行切片,可通过 slice 方法实现,切多大可根据用户传的 props 进行选择。因为用户可能会上传多个文件,所以要将每个大文件所对应的小文件关联起来,这样子在数量文件数据方面不会乱掉,这里用对象进行处理保存,代码如下:

//接受文件函数
handleChange(e){
    this.filesAry=Array.from(e.target.files);
    this.data=this.createChunk(this.filesAry);
},
//将单个文件切割
handleChunk(file){
    let current=0;
    let fileList=[];
    while(current<=file.size){
        fileList.push({
            file:file.slice(current,this.SIZE+current)
        });
        current+=this.SIZE;
    }
    return fileList;
},
//将大文件切割
createChunk(files=[]){
    let filesObj=files.reduce((pre,cur,index,ary)=>{
        pre[`${cur.name}_${index}`]=this.handleChunk(cur);
        return pre;
    },{});
    return filesObj;
},

最后 this.data 就是用户上传所有文件的容器,key 是有文件名+索引组成的,key 所对应的值是一个数组,存放每个大文件所对应的小文件切片。

到这里基本上完成了前端的 1-2 的 coding 问题了。

接下来,this.data 数据中的每个小分片就是要送给后端的数据,即要发送请求。demo 中,通过 axios 进行请求,并且通过 FormData 表单的形式发送数据。那么 axios 的请求封装代码如下:

createRequest({method='post',url='',data={}}){
    return axios({
        method:method,
        url:url,
        data:data,
    })
},

所以在请求之前,要先将 this.data 大文件所对应的每个小文件组装成 formData 的数据格式,传送的数据有:file文件自身,index小文件自身的索引值,hash大文件名字_大文件所对应的索引,nameHash文件的 MD5,filename==文件名称。这些在数据上觉的都送给后端,这样子到后面后端处理文件时,要什么数据都可直接用了。

那么在组装成 formData 数据时需要用到文件的 MD5(这是唯一值),所以用到 spark-md5 这个库,详细用法可查看官网。生成 MD5 的代码如下:

createMd5(fileChunkList=[]){
    let currentChunk=0,md5;
    let reader=new FileReader();
    let spark = new SparkMD5.ArrayBuffer();
    function readFile(){
        if(fileChunkList[currentChunk].file){
            reader.readAsArrayBuffer(fileChunkList[currentChunk].file)
        }
    }
    readFile();
    return new Promise(resolve=>{
        reader.onload=e=>{
            currentChunk++;
            spark.append(e.target.result);
            if(currentChunk<fileChunkList.length){
                readFile();
            }else{
                md5=spark.end();
                resolve(md5);
            }
        };
    })
},

那么,就可以将每个小分片生成 formData 数据了,然后将每个 formDta 对象都一一调用请求函数,就可完成切片的请求封装了。代码如下:

//将每个切片组装成formdata对象
createFormDataRequest(files=[],prop='',nameHash='',fileName='',fileChunk=[]){
    let target=files.map((file,index)=>{
        let formdata= new FormData();
        formdata.append('file',file.file);
        formdata.append('index',index);
        formdata.append('hash',prop);
        formdata.append('nameHash',nameHash);
        formdata.append('filename',fileName);
        return {formdata,index};
    }).map(({formdata,index})=>{
    return this.createRequest({
            method:'post',
            url:'http://localhost:3001/api/handleUpload',
            data:formdata,
        })
    })
    return target;
},

最后在点击处理上传按钮处理函数里面调用生成 MD5,然后在传给 createFormDataRequest 函数。因为是多个大文件上传,所以要循环遍历 this.data,将每个大文件所对应的小文件数组传给 createMd5 函数做处理。代码如下:

//点击上传函数
async handleUpload(e){
    this.targetRequest={};
    for(let prop in this.data){
        if(this.data.hasOwnProperty(prop)){
            let fileName=splitFilename(prop);
            let nameHash=await this.createMd5(this.data[prop]);
            this.targetRequest[`${fileName}_${nameHash}`]=this.createFormDataRequest(this.data[prop],prop,nameHash,fileName);
        }
    }
    //发送请求,并且请求完成之后合并
    Object.keys(this.targetRequest).forEach(async key=>{
        let {filename,nameHash} =splitFileHash(key);
        await Promise.all(this.targetRequest[key]).then(async res=>{
            this.createRequest({
                method:'post',
                url:'http://localhost:3001/api/handleMerge',
                data:{
                filename,
                nameHash,
                SIZE:this.SIZE
                },
            }).then(res=>{
                this.$message.success({
                    message:`${filename}上传成功~`
                })
            })
        })
    })
},

获取到的 targetRequest 是一个数组,里面承载了 Promise(就是每个小分片)。所有通过并发处理发送请求,在完成请求之后,调用合并函数通知后端应该进行文件的合并了。
基本上大文件分片上传前端方面完成了,那么后端就是写处理分片和合并分片的接口。
后端在处理分片时,创建一个文件夹存放每个小分片,文件夹的名称就是用 nameHash 命名,每个小分片进行重命名,改写成文件名_文件的索引。

const Router = require("koa-router");
const apiRouter = new Router();
const path = require("path");
const fs = require("fs");
const targetPath = path.resolve(__dirname, "../target/");

const splitExt = (filename = "") => {
  let name = filename.slice(0, filename.lastIndexOf("."));
  let ext = filename.slice(filename.lastIndexOf(".") + 1, filename.length);
  return { name, ext };
};

//合并文件
/**
 * hash:大文件名+大文件索引
 * nameHash:大文件的MD5
 * filename:文件名称
 * index:小文件分片的索引
 */
apiRouter.post("/api/handleUpload", async (ctx) => {
  const { hash, nameHash, filename, index } = ctx.request.body;
  const chunkPath = path.resolve(targetPath, `${nameHash}`);
  if (!fs.existsSync(chunkPath)) {
    await fs.mkdirSync(chunkPath);
  }
  const { name, ext } = splitExt(filename);
  console.log(ctx.request.files.file.path, "-", index);
  await fs.renameSync(
    ctx.request.files.file.path,
    `${chunkPath}/${filename}_${index}`
  );
  return (ctx.response.status = 200);
});

module.exports = apiRouter;

在处理合并的时候,关键是要有目标文件的文件名和扩展名,然后通过 createWriteStream 和 createReadStream 流的形式将内容写入到目标文件。部分代码如下:

const Router = require("koa-router");
const apiRouter = new Router();
const path = require("path");
const fs = require("fs");
const targetPath = path.resolve(__dirname, "../target/");
const splitExt = (filename = "") => {
  let name = filename.slice(0, filename.lastIndexOf("."));
  let ext = filename.slice(filename.lastIndexOf(".") + 1, filename.length);
  return { name, ext };
};
//文件合并
/**
 * 参数:filename :大文件mingc
 * nameHash:文件的MD5
 * SIZE:切割的大小
 */
apiRouter.post("/api/handleMerge", async (ctx) => {
  const { filename, nameHash, SIZE } = ctx.request.body;
  const targetFilePath = path.resolve(targetPath, `${filename}`);
  const pipStream = (path, writeStream) => {
    return new Promise((resolve) => {
      const readStream = fs.createReadStream(path);
      readStream.on("end", function (err) {
        if (err) throw err;
        // fs.unlinkSync(path);
        resolve();
      });
      readStream.pipe(writeStream, { end: false });
    });
  };
  fs.readdir(path.resolve(targetPath, nameHash), async (err, files) => {
    if (err) return console.log("err:", err);
    files.sort((a, b) => a.split("_")[1] - b.split("_")[1]);
    files = files.map((file) => path.resolve(targetPath, nameHash, file));
    Promise.all(
      files.map(async (file, index) => {
        return pipStream(
          file,
          fs.createWriteStream(targetFilePath, {
            start: index * SIZE,
            end: (index + 1) * SIZE,
          })
        );
      })
    );
  });
  ctx.response.status = 200;
});

module.exports = apiRouter;

断点续传

断点续传的概念是当用户点击暂停按钮时,将正在发送请求的小切片的请求中止了,点击恢复上传之后,再在原来已经上传过的小切片的基础上再进行上传。

大致的问题就是:
前端如何获取每个小切片的请求中止函数,并且何时处理这些中止函数的时间点
再点击恢复上传时,如何获取已经传过的小切片
获取到的小切片之后,如何在原来的切片数组中进行过滤已经上传过的切片

demo 中是通过后端将已经上传的文件切片名称进行返回,所以先提供一个接口,返回已经上传过的切片,接收大文件的 MD5 和大文件名称。代码如下:

const Router = require("koa-router");
const apiRouter = new Router();
const path = require("path");
const fs = require("fs");
const targetPath = path.resolve(__dirname, "../target/");

const splitExt = (filename = "") => {
  let name = filename.slice(0, filename.lastIndexOf("."));
  let ext = filename.slice(filename.lastIndexOf(".") + 1, filename.length);
  return { name, ext };
};

//重新上传文件
/**
 * nameHash:大文件的MD5
 * filename:文件名称
 */
apiRouter.post("/api/handleAgain", async (ctx) => {
  const { nameHash, filename } = ctx.request.body;
  console.log(nameHash, filename);
  const chunkPath = path.resolve(targetPath, `${nameHash}`);
  if (fs.existsSync(chunkPath)) {
    let filesChunk = await fs.readdirSync(chunkPath);
    return (ctx.body = {
      fileChunk: filesChunk,
      filename: filename,
      flag: true,
    }); //找到了
  } else {
    return (ctx.body = { fileChunk: [], filename: filename, flag: false }); //找不到
  }
});

module.exports = apiRouter;

接口不仅返给前端已经上传的文件切片,还给一个 flag 标识符(代码是否找的到切片的文件夹),而且将文件名称一起给前端,因为是多个文件上传,这样前端在待会恢复上传函数处理数据会更简单一些。
demo 是通过 axios 的 CancelToken 进行生成每个小切片的中止函数,并且是在请求拦截器进行获取。然后在点击暂停按钮进行中止函数的发布,再清空。当然要记住已经完成请求的中止函数要过滤掉。在 createRequest 函数新增代码:

axios.interceptors.request.use(
  (config) => {
    let CancelToken = axios.CancelToken;
    //设置取消函数
    config.cancelToken = new CancelToken((c) => {
      this.cancelAry.push({ fn: c, url: config.url });
    });
    return config;
  },
  (err) => {
    return Promise.reject(err);
  }
);
axios.interceptors.response.use(
  (response) => {
    let { config } = response;
    this.cancelAry = this.cancelAry.filter(
      (cancel) => cancel.url !== config.url
    );
    return response;
  },
  (err) => {
    return Promise.reject(err);
  }
);

在点击取消处理函数中发布中止函数:

//点击暂停函数
handleCancel(e){
    this.cancelAry.forEach(fn=>fn());
    this.cancelAry=[];
},

接下来就是完成点击恢复上传的逻辑,调用/api/handleAgain 将已经上传的数据获取,然后过滤数据,最后再调用上传分片的和合并分片的接口。
为了不让代码冗余,将过滤数据的逻辑新增到 createFormDataRequest 函数中,这样过滤完成就可生成 formData 数据。新增的代码如下:

files.filter(
  (file, index) => fileChunk.includes(`${fileName}_${index}`) != true
);

后端的 flag 标识符是为了做是否是第一次上传。因为如果 flag 为 true,即后端找到了大目标文件,那么说明就要过滤数据,并且给用户秒传成功的信号。如何第一次开始上传的大文件,后端没有数据,那么就是第一次上传,就不需要过滤数据。并且在点击上传文件的时候就要做这层的判断了,因为目标文件要在原有基础上继续上传。
所以在点击上传函数要新增调用恢复点击上传处理函数,新增代码如下:

let { result, fileChunk } = await this.handleAgain(
  this.data[prop],
  prop,
  nameHash,
  fileName
);
if (!result) {
  return;
}
this.targetRequest[`${fileName}_${nameHash}`] = this.createFormDataRequest(
  this.data[prop],
  prop,
  nameHash,
  fileName,
  fileChunk
);

那么点击恢复上传的部分代码如下:

//点击重新上传函数
async handleAgain(nameHash,filename,requestChunk=[]){
  let result,fileChunk=[];
  await this.restoreFile(nameHash,filename).then(res=>{
    if(res.status==200){
      if(res.data.flag==false){
        this.$message('秒传成功~~');
        result=true;
      }else{
        fileChunk=re.data.fileChunk;
      }
    }
  })
  return {result,fileChunk};
},

好了,基本上完成了断点续传的功能了。

文件预览

demo 目前支持图片预览功能,后续有时间再继续完善,主要是运用 FileReader 来生成 reader 对象,并且通过 readAsDataURL 来获取 url。

标签:文件,const,断点续传,filename,分片,nameHash,上传
From: https://www.cnblogs.com/wp-leonard/p/17839043.html

相关文章

  • JAVA解析Excel文件 + 多线程 + 事务回滚
    1.项目背景:客户插入Excel文件,Ececel文件中包含大量的数据行和数据列,单线程按行读取,耗时大约半小时,体验感不好。思路:先将excel文件按行读取,存入List,然后按照100均分,n=list.szie()/100+1;n就是要开启的线程总数。(实际使用的时候,数据库连接池的数量有限制,n的大小要结合数据库连......
  • 使用Linux命令sort及uniq对文件或屏幕输出进行分组统计
    sortdemo.txt|uniq-c|sort-rn|head-3在日常Linux操作常常需要对一些文件或屏幕数次中重复的字段进行分组统计。实现的方法非常简单,核心命令为:sort|uniq--c|sort-rn。sort:对指定列进行排序,使该列相同的字段排练到一起uniq-c:uniq命令用于检查及删除文本文件......
  • 单文件WebUploader做大文件的分块和断点续传
    前言:WebUploader是由BaiduWebFE(FEX)团队开发的一个简单的以HTML5为主,FLASH为辅的现代文件上传组件。在现代的浏览器里面能充分发挥HTML5的优势,同时又不摒弃主流IE浏览器,沿用原来的FLASH运行时,兼容IE6+,iOS6+,android4+。两套运行时,同样的调用方式,可供用户任意选用。 上面......
  • vue上传视频插件
    视频作为一种信息表达方式,越来越受到人们的关注和喜爱。近年来,随着移动互联网的普及,手机、平板电脑等设备可以随时随地观看视频。在开发网站或移动应用时,上传和展示视频成为一项不可或缺的功能。Vue作为一种现代化JavaScript框架,提供了丰富的开发工具和插件,其中视频上传插件是Vue开......
  • vue实现视频上传功能
    本文实例为大家分享了vue实现视频上传功能的具体代码,供大家参考,具体内容如下环境:vue+TS上传视频+上传到阿里云主要处理前端在vue下上传视频使用的是阿里云的视频点播服务1、需要后台去申请一个开发API,请求阿里云的接口访问控制2、有了开发视频的token,供给前端3、前端去请求阿......
  • 安装 IIS 访问临时文件夹 C:\WINDOWS\TEMP\3C 读取/写入权限 错误: 0x80070005
    在windows中使用命令行方式安装IIS(Web服务器)WindowsServer2022安装IIS报错访问临时文件夹C:\WINDOWS\TEMP\3C读取/写入权限错误:0x80070005,可以使用命令行方式来安装和配置Web服务(IIS)。以下是使用DeploymentImageServicingandManagement(DISM)工具的步骤:1.打......
  • 大文件上传的处理方法——切片上传
    本篇介绍了切片上传的基本实现方式,以及实现切片上传后的一些附加功能,切片上传原理较为简单,代码注释比较清晰就不多赘述了,后面的附加功能介绍了实现原理,并贴出了在原本代码上的改进方式。有什么错误希望大佬可以指出,感激不尽。切片后上传切片上传的原理较为简单,即获取文件后切片,切片......
  • 前端大文件上传如何做到刷新续传?
    前言这两天在学习阿里云oss上传。踩了不少坑,终于实现了大文件分片、断点续传的功能。这篇文章主要分享学习笔记,希望能给大家一些帮助。先看效果 技术栈1.前端:react+Ts+axios上传文件2.Node部分:定义接口、阿里云oss3.socket.io:实时同步上传进度特别说明axios中onUploadPr......
  • java如何做大体积的文件上传和下载
    在Java中,实现大体积文件的上传和下载涉及到处理文件的分片、并发上传、断点续传等问题。本文将详细介绍如何通过Java实现大体积文件的上传和下载。1.文件上传文件上传是将本地文件上传到服务器的过程。对于大体积文件的上传,我们可以将文件分成多个小片段进行并发上传。1.1文件分......
  • 前端如何实现大文件上传
    在开发过程中,经常会遇到一些较大文件上传,如果只使用一次请求去上传文件,一旦这次请求中出现什么问题,那么无论这次上传了多少文件,都会失去效果,用户则需要重新上传所有资源。所以就想到一种方式,将一个大文件分成多个小文件,这样通过多个请求实现大文件上传。接下来我们就来看看具体是怎......