第二十章 Nest 大文件分片上传

news2024/11/13 8:04:09


在前端的文件上传功能中,只要请求头里定义 content-type 为 multipart/form-data,内容就会以下面形式传递到服务端,接着服务器再按照multipart/form-data的格式去提取数据 获取文件数据
1719751955317.png
但是当文件体积很大时 就会出现一个问题 文件越大 请求的时间会越长,会导致产品的体验很不好。
所以大文件上传时 我们要对齐进行优化 ,例如把1G的大文件 分割成10个100M的小文件,接着并行上传这些文件,服务端接收到10个文件之后再合并这10个小文件 成为一个大文件 这就是大文件分片上传的方案。

1、前端分片大文件方法

在游览器中 Blob 有着一个slice方法 可以截取特定范围的数据 File就是Blob中的一种,我们在选择文件之后可以通过slice 对 File 分片

地址:https://developer.mozilla.org/zh-CN/docs/Web/API/Blob/slice
1719752678591.png

2、后端合并分片文件的方法

在前端分片发送文件到后端全接收之后,在后端我们可以使用fs 的 createWriteStream 方法把每个分片按照不同位置写入文件

地址:https://nodejs.cn/api-v12/fs.html#fscreatewritestreampath-options
1719753140827.png

接下来我们创建项目来测试一下:

nest new large-file-sharding-upload

1719753353544.png
安装 multer 的 ts 类型的包

npm install -D @types/multer

在app.controller.ts中增加一个路由:

import { Body, Controller, Get, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { FilesInterceptor } from '@nestjs/platform-express';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

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

接着启动服务:

pnpm run start:dev

可以看到根目录下,生成了uploads 目录:
1719753839814.png
然后我们在main.ts里面开启跨域支持:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors();
  await app.listen(3000);
}
bootstrap();

在根目录下新建index.html:(其中input 指定 multiple,可以选择多个文件)

<!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>
    <!-- 引入axios库,用于发送HTTP请求 -->
    <script src="https://unpkg.com/axios@0.24.0/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;
            }

            /* 对每个分块创建表单数据并发送POST请求 */
            chunks.map((chunk, index) => {
                const data = new FormData();
                /* 为每个分块设置唯一的名称 */
                data.set('name', file.name + '-' + index);
                /* 添加分块到表单数据 */
                data.append('files', chunk);
                /* 使用axios发送POST请求上传分块 */
                axios.post('http://localhost:3000/upload', data);
            })
        }
    </script>
</body>

</html>

启动前端index.html

npx http-server

1719754178991.png
游览器访问:http://127.0.0.1:8080/
1719754275979.png
接着我们上传个文件 这里我上传的文件大小为20多kb 我的切片是20kb分片一个 所以一共会分片成2给
1719754989392.png
1719755058410.png
可以看到服务端接收了2给分片
1719755090845.png
接下来 我们在服务端实现接收到服务端的数据之后 实现将同一个文件的分片 放置到单独目录中

import { Body, Controller, Get, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { FilesInterceptor } from '@nestjs/platform-express';
import * as fs from 'fs';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) { }

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

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

再次上传文件 可以看到创建了目录 文件夹下有2个文件
1719755431335.png
我们在上传的时候 给文件名增加一个随机的字符串 防止重复

<!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>
    <!-- 引入axios库,用于发送HTTP请求 -->
    <script src="https://unpkg.com/axios@0.24.0/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;
            }

            const randomStr = Math.random().toString().slice(2, 8);

            /* 对每个分块创建表单数据并发送POST请求 */
            chunks.map((chunk, index) => {
                const data = new FormData();
                /* 为每个分块设置唯一的名称 */
                data.set('name', randomStr + '_' + file.name + '-' + index);
                /* 添加分块到表单数据 */
                data.append('files', chunk);
                /* 使用axios发送POST请求上传分块 */
                axios.post('http://localhost:3000/upload', data);
            })
        }
    </script>
</body>

</html>

最后就是合并分片请求:




  /**
   * 合并上传的文件片段。
   * 
   * 该方法用于处理文件分片上传后的合并操作。通过读取指定目录下的所有文件片段,
   * 将它们按顺序拼接成完整的文件,并删除已经合并的文件片段。
   * 
   * @param name 文件名,用于定位和识别待合并的文件片段。
   */
  @Get('merge')
  merge(@Query('name') name: string) {
    // 构建存储文件片段的目录路径
    const chunkDir = 'uploads/chunks_' + name;

    // 读取文件片段目录中的所有文件
    const files = fs.readdirSync(chunkDir);

    // 初始化用于跟踪已合并文件数量的变量
    let startPos = 0;
    let count = 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.rmSync(chunkDir, { recursive: true });
        }
      })

      // 更新下一个文件片段的写入起始位置
      startPos += fs.statSync(filePath).size;
    })
  }

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

<!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>
    <!-- 引入axios库,用于发送HTTP请求 -->
    <script src="https://unpkg.com/axios@0.24.0/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;
            }

            const randomStr = Math.random().toString().slice(2, 8);

            let tasks = []

            /* 对每个分块创建表单数据并发送POST请求 */
            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)
        }
    </script>
</body>

</html>

1719757051775.png
可以看到文件分片上传之后 会执行 merge 接口 合并生成了下面文件
1719757116857.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1931115.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

cpp 强制转换

一、static_cast static_cast 是 C 中的一个类型转换操作符&#xff0c;用于在类的层次结构中进行安全的向上转换&#xff08;从派生类到基类&#xff09;或进行不需要运行时类型检查的转换。它主要用于基本数据类型之间的转换、对象指针或引用的向上转换&#xff08;即从派生…

Mysql缓存调优的基本知识(附Demo)

目录 前言1. 配置2. 缓存3. 策略 前言 基本的知识推荐阅读&#xff1a; java框架 零基础从入门到精通的学习路线 附开源项目面经等&#xff08;超全&#xff09;Mysql优化高级篇&#xff08;全&#xff09;Mysql底层原理详细剖析常见面试题&#xff08;全&#xff09; MySQL…

【网络安全的神秘世界】HackBar绕过许可证

&#x1f31d;博客主页&#xff1a;泥菩萨 &#x1f496;专栏&#xff1a;Linux探索之旅 | 网络安全的神秘世界 | 专接本 | 每天学会一个渗透测试工具 &#x1f344;问题描述 用Firefox浏览器安装hackbar插件后&#xff0c;按F12键&#xff0c;发现如下提示&#xff1a;无法使…

防火墙的带宽管理

一、实验拓扑 目录 一、实验拓扑 二、实验要求 三、实验步骤 3.1将防火墙组网改成双机热备的组网形式&#xff0c;做负载分担模式&#xff0c;游客区和DMZ区走FW3&#xff0c;生产区和办公区的流量走FW1 3.1.1心跳线设置 3.2启用双机热备&#xff0c;并配置VRRP组 3.2.1…

MySQL常用命令的实战应用

MySQL常用命令全攻略&#xff1a;从入门到精通的实用指南 在数字化时代的浪潮中&#xff0c;MySQL作为关系型数据库管理系统的佼佼者&#xff0c;以其稳定性和高效性赢得了全球用户的青睐。无论是新手还是资深开发者&#xff0c;掌握MySQL的常用命令都是提升数据库管理能力的基…

内行人才知道的白酒术语

&#x1f61c;宝子们&#xff0c;今天来给大家分享一些只有内行人懂的白酒术语&#xff0c;让你在酒桌上也能显得很专业&#xff01;&#x1f4aa; ⬆️基酒术语解释&#xff1a;所谓基酒就是最基础的酒&#xff0c;也叫原浆酒&#xff0c;是指成酒后不经过勾调的酒液。基酒度…

数据库内核研发学习之路(三)创建postgres内置函数

本章之前已经讲明白了我们的postgres如何进行编译安装&#xff0c;这是很重要的一步&#xff0c;接下来就是学会对postgres进行小的改动&#xff0c;然后保证依然能够顺利编译安装运行&#xff01; 本章续讲内容如何创建一个内置函数。 1、内置函数和用户自定义函数的区别 熟…

国产精品ORM框架-SqlSugar详解 进阶功能 集成整合 脚手架应用 附源码 云草桑 专题二

国产精品ORM框架-SqlSugar详解 SqlSugar初识 专题一-CSDN博客 sqlsugar 官网-CSDN博客 4、进阶功能 5、集成整合 6、脚手架应用 4、进阶功能 4.1、生命周期 Queryable 什么时候操作库 Queryable是一个引用类型 Queryable拷贝机制 {ISugarQueryable<Student> quer…

[Vulnhub] devt-improved slog_users+vim权限提升+nano权限提升+passwd权限提升+Lxc逃逸权限提升

信息收集 IP AddressOpening Ports192.168.101.149TCP:22,113,139,445,8080 $ nmap -p- 192.168.101.149 --min-rate 1000 -sC -sV PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | …

Python基础语法篇(上)

Python基础语法&#xff08;上&#xff09; 一、基知二、基本数据类型&#xff08;一&#xff09;标准数据类型&#xff08;二&#xff09;数据类型转换 三、字符串基本操作&#xff08;一&#xff09;字符串的索引和切片&#xff08;二&#xff09;字符串的拼接 三、运算符四、…

51单片机嵌入式开发:11、 STC89C52RC 实现一个滑动的led点阵程序

STC89C52RC 实现一个滑动的led点阵程序 1 概述2 LED点阵介绍2.1 LED概述2.2 LED点阵注意事项 3 LED点阵原理3.1 Led点阵内部电路3.2 原理图电路 4 软件实现点阵图案的滑动4.1 软件工程代码4.2 Protues仿真 5 扩展74HC595&#xff08;后续专题开展&#xff09;6 总结 第十一节 1…

MySQL篇:主从复制

概述 话不多说&#xff0c;直接上概念&#xff1a; DDL&#xff08;Data Definition Language&#xff09;语句&#xff1a; 数据定义语言&#xff0c;主要是进行定义/改变表的结构、数据类型、表之间的链接等操作。常用的语句关键字有 CREATE、DROP、ALTER 等。 DML&#xff…

Redis实战—附近商铺、用户签到、UV统计

本博客为个人学习笔记&#xff0c;学习网站与详细见&#xff1a;黑马程序员Redis入门到实战 P88 - P95 目录 附近商铺 数据导入 功能实现 用户签到 签到功能 连续签到统计 UV统计 附近商铺 利用Redis中的GEO数据结构实现附近商铺功能&#xff0c;常见命令如下图所示。…

逻辑漏洞-支付漏洞

【实验目的】 通过本次实验&#xff0c;掌握最基础的支付漏洞 【实验环境】 win7操作机&#xff1a;10.0.0.2 centos7靶机&#xff1a;10.0.0.3 【实验步骤】 1. 启动实验环境 点击“启动场景”按钮&#xff0c;成功启动后&#xff0c;点击操作机按钮进入操作界面。 打开浏…

景区客流统计系统提升服务精准度

在当今旅游业蓬勃发展的时代&#xff0c;景区面临着越来越多的挑战和机遇。如何在保障游客良好体验的同时&#xff0c;实现景区的高效管理和可持续发展&#xff0c;成为了摆在景区管理者面前的重要课题。景区客流统计系统的出现&#xff0c;为解决这一问题提供了有力的支持&…

如何通过成熟的外发平台,实现文档安全外发管理?

文档安全外发管理是企业信息安全管理的重要组成部分&#xff0c;它涉及到企业向外发送的文件&#xff0c;需要进行严格的控制和管理&#xff0c;防止敏感或机密信息的泄露。以下是一些关键考虑因素&#xff1a; 文件外发的挑战&#xff1a;企业在文件外发时面临的主要挑战包括…

Python数据分析-植物生长数据分析(机器学习模型和神经网络模型)

一、研究背景 植物生长受多种环境因素的影响&#xff0c;包括土壤类型、日照时间、浇水频率、肥料类型、温度和湿度等。这些因素不仅影响植物的生长速度和健康状况&#xff0c;还对植物在不同生长阶段的表现有显著影响。随着气候变化和环境污染问题的加剧&#xff0c;研究如何…

【NLP实战】基于TextCNN的新闻文本分类

TextCNN文本分类在pytorch中的实现 基于TextCNN和transformers.BertTokenizer的新闻文本分类实现&#xff0c;包括训练、预测、数据加载和准确率评估。 目录 项目代码TextCNN网络结构相关模型仓库准备工作项目调参预测与评估 1.项目代码 https://github.com/NeoTse0622/Te…

数电基础 - 硬件描述语言

目录 一. 简介 二. Verilog简介和基本程序结构 三. 应用场景 四. Verilog的学习方法 五.调式方法 一. 简介 硬件描述语言&#xff08;Hardware Description Language&#xff0c;HDL&#xff09;是用于描述数字电路和系统的形式化语言。 常见的硬件描述语言包括 VHDL&…

如何落地实际场景,解决跨境传输共性需求?免费白皮书可下载

在全球化的背景下&#xff0c;海外市场对于数据驱动的产品和服务的需求不断增加&#xff0c;各行业数据跨境传输也日趋频繁&#xff0c;在这种前景下&#xff0c;越来越多的企业寻求更深度的跨国业务及合作&#xff0c;因此&#xff0c;企业数据跨境流动也成为了势不可挡的趋势…