首页 > 其他分享 >Nest.js 大文件分片上传

Nest.js 大文件分片上传

时间:2024-02-29 21:33:39浏览次数:27  
标签:files 文件 const name Nest js 分片 data

文件上传是常见需求,只要指定content-typemultipart/form-data,内容就会以如下图这种形式传递到服务端:

服务端再按照multipart/form-data的格式提取数据,就能达到其中的文件。

 

但是当文件很大的时候,事情变得不一样了
假设传一个100M的文件需要三分钟,那么传 1G 的文件就需要 30 分钟
所以大文加上传的场景,需要专门的优化
把1G的大文件分割成 10个 100M 的小文件,然后把这些小文件并行上传,就会变快了
然后等这10个小文件都上传完毕,再发一个请求把这些小文件合并成原来的大文件
这就是大文件分片上传

那如何拆分和合并呢?

浏览器里Blob对象有slice方法,可以截取某个范围的数据,而File就是一种Blob

可以在input里选择了file之后,通过slice对file分片

 
那合并呢?

nodejs中fs对象的createWriteStream方法支持指定start,也就是从什么位置开始写入
这样把每个分片按照不同位置写入文件,完成合并

 
 
先来创建个Nest项目:

nest new large-file-sharding-upload

在AppController添加一个路由:

这是一个Post接口,会读取请求体里的files文件字段传入该方法

这里需要安装一下 multer 包的类型:npm install -D @types/multer

我们在main.js中开启一下跨域访问:

然后在任意的目录中添加一个index.html,后续通过vs code的live server插件启动这个页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script>
</head>
<body>
    <input id="fileInput" type="file" multiple/>
    <script>
        const fileInput = document.querySelector('#fileInput');

        fileInput.onchange =  async function () {
            const data = new FormData();
            data.set('name','光');
            data.set('age', 20);

            [...fileInput.files].forEach(item => {
                data.append('files', item)
            })

            const res = await axios.post('http://localhost:3000/upload', data);
            console.log(res);
        }
    </script>
</body>
</html>

input指定multiple,可以选择多个文件,但是我们这里没有做多文件的演示

选择文件之后,通过post请求upload接口,携带FormData,FormData里保存着files和其他字段

这时候,Nest服务端就接受到了上传的内容和其他字段:

当然,我们并不想上传如此多的文件,后边会将他们合并成一个大文件,所以要修改一下index.html的内容:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script>
</head>
<body>
    <input id="fileInput" type="file"/>
    <script>
        const fileInput = document.querySelector('#fileInput');

        const chunkSize = 20 * 1024;

        fileInput.onchange =  async function () {

            const file = fileInput.files[0];

            console.log(file);

            const chunks = [];
            let startPos = 0;
            while(startPos < file.size) {
                chunks.push(file.slice(startPos, startPos + chunkSize));
                startPos += chunkSize;
            }

            chunks.map((chunk, index) => {
                const data = new FormData();
                data.set('name', file.name + '-' + index)
                data.append('files', chunk);
                axios.post('http://localhost:3000/upload', data);
            })
        
        }

    </script>
</body>
</html>

对拿到的文件进行分片,然后单独上传分片,分片名字为文件名+index

这里我们测试的图片是80多k,所以每20k一个分片,一共是四个分片

服务端接收到了这四个分片:

我们把这四个分片移动到单独的目录,方便后续操作:

@Post('upload')
@UseInterceptors(FilesInterceptor('files', 20, {
  dest: 'uploads'
}))
uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body: { name: string }) {
  console.log('body', body);
  console.log('files', files);

  const fileName = body.name.match(/(.+)\-\d+$/)[1];
  const chunkDir = 'uploads/chunks_'+ fileName;

  if(!fs.existsSync(chunkDir)){
    fs.mkdirSync(chunkDir);
  }
  fs.cpSync(files[0].path, chunkDir + '/' + body.name);
  fs.rmSync(files[0].path);
}

然后用正则匹配出文件名:

在uploads文件夹下创建chunk_文件名的目录,把文件复制过去,然后删除同路径同名文件

 
分片文件移动成功了
不过直接以 chunk_文件名 做目录名,太容易产生冲突了,所以可以在上传时在文件名中间加个随机字符串“

这样产生冲突的概率就很小了:

接下来就是在全部分片上传完成之后的合并请求:

@Get('merge')
merge(@Query('name') name: string) {
    const chunkDir = 'uploads/chunks_'+ name;

    const files = fs.readdirSync(chunkDir);

    let startPos = 0;
    files.map(file => {
      const filePath = chunkDir + '/' + file;
      const stream = fs.createReadStream(filePath);
      stream.pipe(fs.createWriteStream('uploads/' + name, {
        start: startPos
      }))

      startPos += fs.statSync(filePath).size;
    })
}

接受文件名,然后查找对应的chunks目录,把下面的文件读取出来,按照不同的start位置写入到同一个文件中

 

然后再合并之后把chunks目录删掉:

 
然后在前端代码中,当分片全部上传完成后,调用merge接口:

标签:files,文件,const,name,Nest,js,分片,data
From: https://www.cnblogs.com/sunyan97/p/18045255

相关文章

  • 记账本jsp
     <%@pagecontentType="text/html;charset=UTF-8"language="java"%><!DOCTYPEhtml> <html><head><metacharset="UTF-8"> <title>家庭记账本主页</title> <!--采用绝对路径导入css文件--> <linkrel=&......
  • jsp自定义标签
    一、自定义标签的作用自定义标签主要用于移除Jsp页面中的java代码。二、自定义标签开发和使用2.1、自定义标签开发步骤1、编写一个实现Tag接口的Java类(标签处理器类)1packageme.gacl.web.tag;23importjava.io.IOException;45importjavax.servl......
  • Livewire 和 Alpine.js比较
    Livewire和Alpine.js是两个在构建交互式Web应用程序时常用的工具,它们在一些方面有相似之处,但也有一些关键区别。Livewire基于Laravel:Livewire是Laravel的一部分,与Laravel框架紧密集成,使得开发者可以使用Laravel的语法和功能来构建动态页面。服务器端渲染:Livew......
  • jq ajax传递json对象到服务端及contentType的用法
    目录0、一般情况下,通过键值对的方式将参数传递到服务端0.1客户端代码:0.2服务端代码:0.3在浏览器的网络中查看此次请求:1、ajax传递复杂json对象到服务端1.1方法一:通过formdata传值,服务端通过key获取值;1.2方法二:通过formdata方式传值,服务端读取Request.InputStrea......
  • require.js
    require.js的加载使用require.js的第一步,是先去官方网站下载最新版本。下载后,假定把它放在js子目录下面,就可以加载了。<scriptsrc="js/require.js"></script>有人可能会想到,加载这个文件,也可能造成网页失去响应。解决办法有两个,一个是把它放在网页底部加载,另一个是写成下......
  • 数组对象删除不满足某些条件的对象 js
    recursiveFunction(items,childrenNodeName,ids){console.log('items',ids);//获取数组长度if(items)items=[];letlen=items?.length//循环遍历数组for(leti=0;i<len;i++){//如果有子节点,递归遍历......
  • uniapp 小程序 request.js 文件书写
    uniapp小程序request.js文件书写:https://blog.csdn.net/weixin_46479579/article/details/124431422?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_utm_term~default-16-124431422-blog-132159578.235^v43^pc_blog_bottom_relevance_base9&spm=1001......
  • 微信小程序的配置文件app.json的所有内容(十七)
    微信小程序的配置文件app.json的所有内容  {  "pages":["page1","page2","page3"], //页面路径列表,第一个页面路径为小程序初始页面  "window":{ //小程序窗口设置   //如果不进行额外的配置,小程序页面将使用默认的导航样式。"navigationStyle":"defau......
  • spring-boot整合jsp + mybatis ems小案例分析
    1.项目开发流程 需求分析:分析用户主要需求提取出项目核心功能根据核心功能构建页面原型 库表设计(概要设计):1.分析整个系统有哪些表2.分析出表之间关联关系3.确定字段 详细设计(流程图,伪代码):用来验证库表准确性 功能实现(编码):环境搭建具体功能实现 功能测试......
  • createRange表示文档中的一个范围——用于js判断文字添加省略号情况
    document.createRange()是JavaScript中的一个方法,用于创建一个Range对象,表示文档中的一个范围。Range对象通常用于选择文档中的一部分内容,然后对其进行操作。它可以:设置选中文本范围:可以使用document.createRange()方法创建一个Range对象,并使用setStart()和setEnd(......