首页 > 编程语言 >Node分片上传和OSS上传

Node分片上传和OSS上传

时间:2024-03-13 23:14:55浏览次数:27  
标签:Node files const 文件 OSS 分片 上传

大文件分片

切片就是为了解决大文件上传时间过长,优化体验。将大文件拆分成多个小文件,依次上传,上传完毕后合并成源文件。
浏览器的 Blob 提供了 slice 方法,可以截取某个范围的数据,而文件上传的 File 就是一种 Blob

image

前端可以通过 Blob.slice 进行文件拆分,然后就是后端文件合并。

image

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

编写常规上传接口

安装 multer 类型

pnpm i @types/multer -D

编写 controller 接收文件

  @Post('upload')
  @UseInterceptors(FilesInterceptor('files', 20, { dest: 'uploads' }))
  uploadFile(@UploadedFiles() files: Express.Multer.File[]) {
    return files;
  }

安装静态资源访问包

pnpm i @nestjs/serve-static

设置可访问的静态资源

// app.module.ts
@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, '..', 'client'),
    }),
	// ...
  ],
  controllers: [AppController],
  providers: [AppService],
})

编写请求联调

<body>
  <input id="fileControll" type="file" multiple />
</body>
<script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script>
<script>
  const fileInput = document.querySelector('#fileControll');

  fileInput.addEventListener('change', async (event) => {
    const files = event.target.files;
    const data = new FormData();
    // files 是一个伪数组,需转成数组

    const f = Array.from(files).forEach((file) => data.append('files', file));

    const res = await axios.post('/person/upload', data);
  });
</script>

至此,一个最常规的文件上传前后端联调已经完成了。

分片上传

此时是需要做分片的,即前端文件拆分上传,后端文件合并,常规的文件上传是不适用的,需要对其进行改写。

前端文件分片上传

const fileInput = document.querySelector('#fileControll');

const chunkSize = 1000 * 1024; // 1024 就是 1k。*1000 就是每 1000k 拆分

fileInput.addEventListener('change', async (event) => {
  const file = event.target.files[0];  // 暂时先上传一个文件进行测试
  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('/person/upload', data);
  });
});

image

前端分片时,每 1000k 拆分成一份,最终可以看到文件在存储到后端时,拆分成了七份。

后端分片处理

创建分片目录

所有的分片都存储在 uploads 文件夹下,合并时是无法区分哪个分片是属于谁的,此时可以将每次上传的文件分文件夹存储,一个文件一个文件夹。

  @Post('upload')
  @UseInterceptors(FilesInterceptor('files', 20, { dest: 'uploads' }))
  uploadFile(
    @UploadedFiles() files: Express.Multer.File[],
    @Body() body: { name: string },
  ) {
    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); // 将传到 uploads 文件夹下的内容 copy 到 分片文件夹下
    fs.rmSync(files[0].path); // 删除 uploads 文件夹下的文件
    return files;
  }

此时再进行上传

image

分片目录名冲突

以文件名作为分片目录,造成的结果就是会出现重复目录,即两个相同文件名的分片跑到一个目录下,前端在传入文件名时,可以加上随机数(uuid)等,这样可以避免该问题,当然,后端加也一样。

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

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

image

后端分片合并

文件分片上传完毕后,可以再发请求让这些文件进行合并,比如传入文件名,让后端找这个文件对应的分片目录进行合并

  @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.rm(chunkDir, { recursive: true }, () => {});
          }
        });

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

    return 'merge file success';
  }

image

需要注意的是,传入的文件名必须是上传时的文件名,而不是文件的原本名

OSS 上传(阿里云)

本地存储的文件目录结构

image

OSS 存储的目录结构,是由桶来存储文件的

image

购买 阿里云 OSS 云存储

image

创建 Bucket(桶)

image

上传一个文件,查看存储再 OSS 中的详细信息

image

此时在公网环境下就可以访问该图片

image

通常,生产环境下我们不会直接用 OSS 的 URL 访问,而是会开启 CDN,用网站域名访问,最终回源到 OSS 服务

Node 集成 OSS

阿里云提供了 OSS 的开发文档:Nodejs OSS对象存储

image

OSS 简单使用

先按照文档进行简单使用

mkdir oss-test
cd oss-test
npm init -y
npm i ali-oss # 安装sdk开发包

在 index.js 中,将官方示例粘贴过来研究

const OSS = require("ali-oss");
const path = require("path");

const client = new OSS({
  // yourregion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
  region: "yourregion",
  // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
  accessKeyId: process.env.OSS_ACCESS_KEY_ID,
  accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
  // 填写Bucket名称。
  bucket: "examplebucket",
});

async function put() {
  try {
    // 填写OSS文件完整路径和本地文件的完整路径。OSS文件完整路径中不能包含Bucket名称。
    // 如果本地文件的完整路径中未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
    const result = await client.put(
      "login.png",
      path.normalize("./api-login.2fcc9f35.jpg")
    );
    console.log(result);
  } catch (e) {
    console.log(e);
  }
}

put();

文档代码中给出的解释,依次去寻找参数来源。

  • region,bucket 所在区域

image

  • accessKeyIdaccessKeySecret,访问凭证/私钥

image

  • bucket 就是自己创建的Bucket名称

参数都了解并完善之后执行代码,此时再去查看 OSS 的文件列表,就可以发现多了个文件。

image

使用 RAM 子用户 AccessKey

在点击进入 AccessKey 管理时,每次都会弹出以下内容,提示 AccessKey 不安全,让使用子用户 AccessKey

image

那就创建子用户 AccessKey

image

创建完成之后,将代码中已有的凭证替换成新的

image

此时执行代码,会出现无权限的报错提示,需要开通权限

image

开通权限

image

此时再次运行代码就可以了。

image

RAM 子用户的好处就是,就算 accessKey 泄露,由于有权限分配,可以直接解除该主体的 accessKey 访问权限

授权给第三方上传

授权第三方上传出现的原因是:

  • 前端经过服务器,服务器再转存到 OSS,消耗服务器资源
  • 前端直接传给 OSS,增加 accessKey 暴露风险

基于以上两点,给出的两全其美的解决方法就是授权给第三方上传,此处可查看 文档

Node 版获取临时签名完整代码,部分代码也可查看 文档

const express = require("express");
const moment = require("moment");
const { Buffer } = require("buffer");
const OSS = require("ali-oss");

const app = express();
const path = require("path");

const config = {
  accessKeyId: "accessKeyId",
  accessKeySecret: "accessKeySecret",
  bucket: "bucket",
  callbackUrl: "url",  // 
  dir: "prefix/", // OSS文件的前缀
};

app.get("/", async (req, res) => {
  const client = new OSS(config);

  const date = new Date();
  date.setDate(date.getDate() + 1);
  const policy = {
    expiration: date.toISOString(), // 请求有效期
    conditions: [
      ["content-length-range", 0, 1048576000], // 设置上传文件的大小限制
      // { bucket: client.options.bucket } // 限制可上传的bucket
    ],
  };

  //  跨域才设置
  res.set({
    "Access-Control-Allow-Origin": req.headers.origin || "*",
    "Access-Control-Allow-Methods": "PUT,POST,GET",
  });

  //签名
  const formData = await client.calculatePostSignature(policy);
  //bucket域名
  const host = `http://${config.bucket}.${
    (await client.getBucketLocation()).location
  }.aliyuncs.com`.toString();
  //回调
  const callback = {
    callbackUrl: config.callbackUrl,
    callbackBody:
      "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}",
    callbackBodyType: "application/x-www-form-urlencoded",
  };

  //返回参数
  const params = {
    expire: moment().add(1, "days").unix().toString(),
    policy: formData.policy,
    signature: formData.Signature,
    accessid: formData.OSSAccessKeyId,
    host,
    callback: Buffer.from(JSON.stringify(callback)).toString("base64"),
    dir: config.dir,
  };

  res.json(params);
});

//接收回掉
app.post("/result", (req, res) => {
  //公钥地址
  const pubKeyAddr = Buffer.from(
    req.headers["x-oss-pub-key-url"],
    "base64"
  ).toString("ascii");
  //判断
  if (
    !pubKeyAddr.startsWith("https://gosspublic.alicdn.com/") &&
    !pubKeyAddr.startsWith("https://gosspublic.alicdn.com/")
  ) {
    System.out.println("pub key addr must be oss addrss");
    res.json({ Status: "verify not ok" });
  }
  res.json({ Status: "Ok" });
});

app.listen(9000, () => {
  console.log("http://localhost:9000");
  console.log("App of postObject started.");
});

运行得到的结果大概如图所示

image

经过以上步骤,上传 OSS 的地址 host,用的临时 signaturepolicy 都有了,此时就能让前端直接使用临时签名上传。

前端使用临时签名

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

    // 调用服务端的提供临时凭证接口
    async function getOSSInfo() {
      return {
        expire: "1710427719",
        policy: "eyJleHBpcmF0aW9uIjoiMjAyNC0wMy0xNFQxNDo0ODozOC42MDRaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF1dfQ==",
        signature: "ncnYb+6AsWVquMzYJuDVJPOG3Y8=",
        accessid: "LTAI5tMSeQWSbHF4Ky9QmDV4",
        host: "http://jsonq.oss-cn-beijing.aliyuncs.com",
        callback: "eyJjYWxsYmFja1VybCI6InVybCIsImNhbGxiYWNrQm9keSI6ImZpbGVuYW1lPSR7b2JqZWN0fSZzaXplPSR7c2l6ZX0mbWltZVR5cGU9JHttaW1lVHlwZX0maGVpZ2h0PSR7aW1hZ2VJbmZvLmhlaWdodH0md2lkdGg9JHtpbWFnZUluZm8ud2lkdGh9IiwiY2FsbGJhY2tCb2R5VHlwZSI6ImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCJ9",
        dir: "prefix/",
      };
    }

    fileInput.addEventListener("change", async (event) => {
      const file = event.target.files[0];

      const ossInfo = await getOSSInfo();
      const formdata = new FormData();
      formdata.append("key", file.name);
      formdata.append("OSSAccessKeyId", ossInfo.accessid);
      formdata.append("policy", ossInfo.policy);
      formdata.append("signature", ossInfo.signature);
      formdata.append("success_action_status", "200"); //让服务端返回200,不然,默认会返回204
      formdata.append("file", file);

      const res = await axios.post(ossInfo.host, formdata);
      if (res.status === 200) {
        const img = document.createElement("img");
        img.src = ossInfo.host + "/" + file.name;
        document.body.append(img);

        alert("上传成功");
      }
    });
  </script>
</html>

此时上传是有跨域限制的,有条件的情况下可以希望在项目的本地做proxy代理,此处直接让 OSS 允许跨域请求

image

点击上传即可

image

标签:Node,files,const,文件,OSS,分片,上传
From: https://www.cnblogs.com/jsonq/p/18067031

相关文章

  • [GPT] nodejs 什么情况下可以使用 import 来引入 export 的模块
    在Node.js中,原生并不支持ES6的import语句来引入模块。不过从Node.jsv12开始,通过实验性功能(--experimental-modules)可以使用.mjs扩展名的文件来启用对ES6模块的支持,并使用import语句。新版本Nodejs已移除了--experimental-modules但是,在生产环境中,为了确保兼......
  • Node.js毕业设计安全输血医用网站(Express)
    本系统(程序+源码)带文档lw万字以上  文末可获取本课题的源码和程序系统程序文件列表系统的选题背景和意义选题背景:在现代医疗体系中,输血是救治患者的重要手段之一。然而,随着血液资源的日益紧张和血液安全问题的不断凸显,如何确保安全、高效地进行输血成为了一个亟待解决的......
  • Node.js毕业设计安全教育平台(Express)
    本系统(程序+源码)带文档lw万字以上  文末可获取本课题的源码和程序系统程序文件列表系统的选题背景和意义选题背景:随着信息技术的飞速发展,教育领域也在逐渐实现现代化、信息化。安全教育作为培养人们安全意识和技能的重要途径,其传统教育方式已经不能满足现代社会的需求。......
  • 解决表格文件上传无法删除临时文件的问题Failed to perform cleanup of multipart ite
    java.io.UncheckedIOException:CannotdeleteC:\Users\hasee\AppData\Local\Temp\tomcat如图所示,刚开始以为是apifox没删除的问题,换了之后依旧这样 尝试方案1-失败 方法二-失败 方法三-成功 原文链接报错:StandardServletMultipartResolver:Failedtoperform......
  • 文件上传漏洞
    漏洞描述文件上传漏洞是指由于程序员未对上传的文件进行严格的验证和过滤,而导致用户可以越过其本身权限向服务器上传可执行的动态脚本文件。如常见的头像上传,图片上传,oa办公文件上传,媒体上传,允许用户上传文件,如果过滤不严格,恶意用户利用文件上传漏洞,上传有害的可以执行脚本文件......
  • 文件上传[SUCTF 2019]CheckIn
    文件上传[SUCTF2019]CheckIn打开提交js图片马后台检测文件类型在木马出添加GIF89a绕过显示上传成功的地址uploads/f65a0ca982c669865231909b0ec85a0c上传.user.ini解马关于.user.ini和.htaccess后者有局限性,只能用于apache前者只要能运行php都可用auto_prepend_file......
  • Node+Vue毕设高校教师项目申报管理平台(程序+mysql+Express)
    本系统(程序+源码)带文档lw万字以上 文末可获取本课题的源码和程序系统程序文件列表系统的选题背景和意义选题背景:在高等教育领域,教师参与科研项目是推动学科发展和创新的重要途径。随着科研竞争的加剧,高校教师需要积极申报各类科研项目以获取资金支持。然而,项目申报过程......
  • Node+Vue毕设高校实践活动管理平台(程序+mysql+Express)
    本系统(程序+源码)带文档lw万字以上 文末可获取本课题的源码和程序系统程序文件列表系统的选题背景和意义选题背景:在全面提升学生实践能力和创新精神的当代教育背景下,高校实践活动成为了教学体系中不可或缺的一部分。这些活动包括社会实践、科研实践、志愿服务、技能培训......
  • Node+Vue毕设购物网站的设计与渗透测试(程序+mysql+Express)
    本系统(程序+源码)带文档lw万字以上 文末可获取本课题的源码和程序系统程序文件列表系统的选题背景和意义选题背景:在电子商务迅猛发展的今天,购物网站已成为人们日常生活的一部分。一个具备良好用户体验、安全可靠的购物网站能够吸引并留住大量用户,对于提升品牌影响力和实......
  • Node+Vue毕设风投项目管理(程序+mysql+Express)
    本系统(程序+源码)带文档lw万字以上 文末可获取本课题的源码和程序系统程序文件列表系统的选题背景和意义选题背景:在当今快速发展的科技与经济时代,风险投资(VentureCapital,简称VC)扮演着至关重要的角色。它为初创企业和创新项目提供了必要的资金支持,帮助它们在市场中站稳脚......