首页 > 其他分享 >Nest 实现大文件分片上传

Nest 实现大文件分片上传

时间:2024-01-08 10:37:50浏览次数:32  
标签:files 文件 fs const name Nest 分片 上传

文件上传是常见需求,只要指定 content-type 为 multipart/form-data,内容就会以这种格式被传递到服务端:

Nest 实现大文件分片上传_Node.js

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

Nest 实现大文件分片上传_JavaScript_02

但当文件很大的时候,事情就变得不一样了。

假设传一个 100M 的文件需要 3 分钟,那传一个 1G 的文件就需要 30 分钟。

这样是能完成功能,但是产品的体验会很不好。

所以大文件上传的场景,需要做专门的优化。

把 1G 的大文件分割成 10 个 100M 的小文件,然后这些文件并行上传,不就快了?

然后等 10 个小文件都传完之后,再发一个请求把这 10 个小文件合并成原来的大文件。

这就是大文件分片上传的方案。

Nest 实现大文件分片上传_前端_03

那如何拆分和合并呢?

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

Nest 实现大文件分片上传_Node.js_04

Nest 实现大文件分片上传_上传_05

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

那合并呢?

Nest 实现大文件分片上传_JavaScript_06

fs 的 createWriteStream 方法支持指定 start,也就是从什么位置开始写入。

这样把每个分片按照不同位置写入文件里,不就完成合并了么。

思路理清了,接下来我们实现一下。

创建个 Nest 项目:

npm install -g @nestjs/cli

nest new large-file-sharding-upload

Nest 实现大文件分片上传_Node.js_07

在 AppController 添加一个路由:

Nest 实现大文件分片上传_JavaScript_08

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

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

这里还需要安装用到的 multer 包的类型:

npm install -D @types/multer

然后我们在网页里试一下:

首先在 main.ts 里开启跨域支持:

Nest 实现大文件分片上传_ios_09

然后添加一个 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" 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 和其它字段。

起个静态服务:

npx http-server .

Nest 实现大文件分片上传_前端_10

浏览器访问下:

Nest 实现大文件分片上传_ios_11

选择几个文件:

Nest 实现大文件分片上传_上传_12

这时候,Nest 服务端就接收到了上传的文件和其他字段:

Nest 实现大文件分片上传_JavaScript_13

当然,我们并不是想上传多个文件,而是一个大文件的多个分片。

所以是这样写:

<!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。

这里我们测试用的图片是 80k:

Nest 实现大文件分片上传_前端_14

所以每 20k 一个分片,一共是 4 个分片。

测试下:

Nest 实现大文件分片上传_JavaScript_15

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

Nest 实现大文件分片上传_前端_16

然后我们把它们移动到单独的目录:

@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);
}

用正则匹配出文件名:

Nest 实现大文件分片上传_Node.js_17

在 uploads 下创建 chunks_文件名 的目录,把文件复制过去,然后删掉原始文件。

测试下:

Nest 实现大文件分片上传_前端_18

Nest 实现大文件分片上传_Node.js_19

分片文件移动成功了。

不过直接以 chunks_文件名 做为目录名,太容易冲突了。

我们可以在上传文件的时候给文件名加一个随机的字符串。

Nest 实现大文件分片上传_ios_20

这样就不会冲突了:

Nest 实现大文件分片上传_Node.js_21

接下来,就是在全部分片上传完之后,发送合并分片的请求。

添加一个 merge 的接口:

@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 位置写入到同一个文件里。

浏览器访问下这个接口:

Nest 实现大文件分片上传_JavaScript_22

可以看到,合并成功了:

Nest 实现大文件分片上传_Node.js_23

再测试一个:

Nest 实现大文件分片上传_前端_24

Nest 实现大文件分片上传_前端_25

也没啥问题。

然后我们在合并完成之后把 chunks 目录删掉。

Nest 实现大文件分片上传_前端_26

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

    const files = fs.readdirSync(chunkDir);

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

        if(count === files.length) {
          fs.rm(chunkDir, {
            recursive: true
          }, () =>{});
        }
      })

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

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

Nest 实现大文件分片上传_ios_27

const tasks = [];
chunks.map((chunk, index) => {
    const data = new FormData();

    data.set('name', randomStr + '_' + file.name + '-' + index)
    data.append('files', chunk);
    tasks.push(axios.post('http://localhost:3000/upload', data));
})
await Promise.all(tasks);
axios.get('http://localhost:3000/merge?name=' + randomStr + '_' + file.name);

连起来测试下:

Nest 实现大文件分片上传_前端_28

因为文件比较小,开启 network 的 slow 3g 网速来测。

可以看到,分片上传和最后的合并都没问题。

当然,你还可以加一个进度条,这个用 axios 很容易实现:

Nest 实现大文件分片上传_上传_29

至此,大文件分片上传就完成了。

案例代码上传里 github:github.com/QuarkGluonP…

总结

当文件比较大的时候,文件上传会很慢,这时候一般我们会通过分片的方式来优化。

原理就是浏览器里通过 slice 来把文件分成多个分片,并发上传。

服务端把这些分片文件保存在一个目录下。

当所有分片传输完成时,发送一个合并请求,服务端通过 fs.createWriteStream 指定 start 位置,来把这些分片文件写入到同一个文件里,完成合并。

这样,我们就实现了大文件分片上传。

标签:files,文件,fs,const,name,Nest,分片,上传
From: https://blog.51cto.com/u_15506823/9139590

相关文章

  • RuoYi-Cloud-Plus使用minio进行文件上传图片后无法预览解决_修改minio配置minio桶权限
     在文件管理的位置,发现刚刚上传的图片文件,会显示 预览图片失败 后来经过多方查看,发现是minio的配置的问题可以从这里:可以看到首先登录RuoYi-Cloud-Plus系统然后,打开文件管理页面可以看到,当上传了图片文件以后显示文件展示中,文件预览失败,那么这个时......
  • • 如何在flask中处理文件上传
    问题如何在flask中处理文件上传步骤123在Flask中处理文件上传的步骤如下:1.**创建HTML表单**:首先,你需要在前端创建一个HTML表单来接受用户上传的文件。表单的`enctype`属性应设置为`multipart/form-data`,以便浏览器以二进制形式发送文件¹²。```html<!doctypehtml><......
  • nested exception is java.lang.IllegalArgumentException异常问题解决
    项目启动报错如下:nestedexceptionisjava.lang.IllegalArgumentException:Couldnotresolveplaceholder'xxx'invalue"${xxx}"问题解决比较简单,只说我所遇到的情况,原因就是字母拼写问题仔细看还是能看到大写的K和小写的k有一些细微的区别,将nacos中的k和代码中修改一致后启......
  • 浏览器接口 fetch 发起的请求如何跟踪上传进度?
    在使用浏览器接口`fetch`发起请求时,你可以利用`ReadableStream`对象的`onprogress`事件来跟踪上传进度。`fetch`函数返回的是一个`Promise`对象,可以通过调用`response.body.getReader()`获取到`ReadableStream`,然后通过监听`onprogress`事件来获取上传进度信息。以下是一个示例代码......
  • 如何将视频上传到可用于HTML5 <video> 标签的 'src' 属性的平台?
    要将视频上传到可用于HTML5<video>标签的'src'属性的平台,您需要遵循以下步骤:1.选择一个支持视频上传的平台,例如YouTube、Vimeo或自建服务器。2.注册并登录到所选平台。3.创建一个新的视频项目或上传您的视频文件。4.获取视频的嵌入代码或URL。这通常可以在平台的......
  • Python武器库开发-武器库篇之上传本地仓库到Git(三十八)
    武器库篇之上传本地仓库到Git(三十八)当我们在Git中创建远程仓库和进行了SSHkey免密登陆之后,我们点击Yourrespositories可以查看我们所创建的远程仓库,如图所示:如果我们需要将本地的仓库上传到Git,首先我们需要建立一个本地的仓库,我们创建一个和远程仓库同名的本地仓库,然后进入这......
  • uniapp:全局消息是推送,实现app在线更新,WebSocket,apk上传
    全局消息是推送,实现app在线更新,WebSocket1.在main.js中定义全局的WebSocket2.java后端建立和发送WebSocket3.通知所有用户更新背景:开发人员开发后app后打包成.apk文件,上传后通知厂区在线用户更新app。那么没在线的怎么办?因为我们在上一篇博客中写了,在app打开的时候回去校验是否......
  • HTML5 文件上传的2种方式
    以前上传文件需要提交Form表单。HTML5方式上传文件,可以通过使用FormData类模拟Form表单提交,从而实现无刷新上传文件。 假设有一个文件选择框<inputtype="file"name="pic"id="pic"accept="image/gif"/>有下面2种方式上传文件:1、XMLHttpRequest(有进度事件)varfiles=document.......
  • html5实现文件批量上传组件
    一、概述在html5中,相对于之前添加了不少新的元素和属性,在javascript中也添加了一些新的API,这些给我们的开发带来了很多便利。但由于各浏览器的发展步骤不一致,也导致了不同浏览器对html5支持的差异性。 二、实现原理1.在该html5实现的文件批量上传组件中,我们主要是利用html5中的一......
  • 使用HTML5实现多文件上传
    入门Fileinput之所以叫fileinput是因为它是一个标准的input元素,且其type属性被设置为"file"。很多年以来,fileinput一次只能选择并上传单个文件。在新的HTML5规范中为input元素添加了多选模式。当然,不同浏览器对于规范的实现一直以来都不尽相同。比如,Firefox3.6和WebKit以及Firef......