《上传那些事儿之Nest与Koa》——文件格式怎么了!

概要

本文主要针对在使用node作为服务端接口时,前端上传上传文件至node作为中转,再次上传至oss/cdn的场景。以及针对在这个过程中,需要对同一个文件进行不同形式之间转换的问题。

Blob、File、Buffer与stream

在解答上述问题之前,我们要先了解一下Blob、File、Buffer与stream这四者分别是什么。以及这四者的关系是什么样的。

Blob

Blob 对象表示一个不可变、原始数据的类文件对象。

这是MDN对Blob的说明。简而言之,所有的“数据”都可以用blob的格式进行存储,而且不一定是 JavaScript 原生格式的数据。包括但不仅限于文本、二进制、文档流等。而通过Blob的实例方法(Blob.prototype.arrayBuffer()Blob.prototype.stream()),我们还可以将blob转换为Buffer和ReadableStream。

File

File接口基于 Blob,继承了 blob 的功能并将其扩展以支持用户系统上的文件。接口提供有关文件的信息,并允许网页中的 JavaScript 访问其内容,且可以用在任意的 Blob 类型的 context 中。

需要注意的一点是,File并没有任何定义方法,而是只从Blob继承了slice方法。

Buffer

Buffer是数据以二进制形式临时存放在内存中的物理映射。在Nodejs中,Buffer类是用于直接处理二进制数据的全局类型。它可以以多种方式构建。

stream

Node.js 中有四种基本的流类型:

开发前的规划

在我们进行文件上传的过程中,经历了两个阶段:

  1. 获取前端上传的文件
  2. 处理文件后,调用内部服务上传至cdn

其实这样看来的话,这是很简单的两个阶段,我们只需要拿到前端的文件后传递给另外一个接口就可以了,可是在这个过程中,有几个我们不得忽视的问题:

  1. 我们的node服务中获取到的前端上传的文件到底是什么格式?
  2. 我们进行上传oss/cdn的接口,需要我们上传的文件格式又是什么样的?
  3. 文件名称如何保持不变/如何进行混淆?
  4. 如何完成文件格式的校验或过滤?

只有在考虑清楚了以上这些内容的处理之后,才应该来考虑我们接口本身的业务逻辑的完善与开发。

开发中的问题

由于一些内部原因,Node端的开发经历了从koa2到express的重构。所以针对两个框架的文件处理,我也都有幸(bushi)全都经历了一次。

node上传格式

由于上传至oss的第三方接口可以在前端调用,也可以在node中进行调用,所以在Postman中可以模仿上传过程,由此可以看到第三方接口真正需要我们传入的其实是一个ReadStream格式的文件。
所以我们的目标也很简单,那就是无论我们获取到什么格式的文件,都转换成为ReadStream格式即可。

image.png

koa2

不同于在koa中使用koa-bodyparser模块来完成post请求的处理;在koa2中,使用koa-body模块不仅可以完成对于post请求的处理,同时也能够处理文件类型的上传。

在这种情况下我们只需要通过ctx.request.files即可访问前端上传给我们的文件实例,同时我们可以看到我们获取到的是一个WriteStream格式的文件。通过size、name、type等属性,即可获取相应的属性,用于进行文件格式的校验与判断

当我直接使用fs.createReadStream方法将它转换为我们所需要的格式时,问题也随之而来:

由于上传后的文件经过了koa的处理,所以我们得到的WriteStream的path发生了一些变化,他变成了内存中的一个地址导致我们转化之后的文件名称也发生了变化,变成了一个内存中的地址串。

很显然,这是我们不想要看到的,因为这对于我们来说是不可控的。为了解决这个问题,我尝试了两种解决方式均有效,大家可以自行选择。

1. 使用koa-body的配置参数,进行地址转存。

app.use(body({
    multipart: true,
    formidable:{
        // 上传存放的路劲
        uploadDir: path.join(__dirname,'./temp'),
        // 保持后缀名\
        keepExtensions: true,
        onError(err){
           console.log(err)
        }
    }
})); 

2. 使用fs将文件转存至本地,上传完成后再进行删除

import * as fs from 'fs';

const file = ctx.request.files.file;
// 通过originalname获取文件原名称
const newName = file.originalname;
fs.writeFileSync(newName, file.path);
const newFile = fs.createReadStream(newName);
// 使用newFile进行文件上传。。。
fs.rmSync(newName);

在处理文件名称的过程中也可以手动的使用uuid来进行名称的混淆。有人可能认为,为什么宁愿那么麻烦的获取原来的名称、再使用uuid重新生成新名称,也不愿意直接使用内存地址作为文件名称呢?

很显然,因为这个流程对于我们来说是可控的

NestJS➕express

由于一些公司内部的历史原因,导致在使用koa2的开发过程中,缺少了一些swagger相关的功能实现。不得不使用NestJS+express来重构整个项目😭😭😭

而在NestJS中的上传,则需要使用NestJS提供的拦截器UseInterceptors,同时也需要依赖FileInterceptorUploadedFile来对于单文件上传的处理。FileInterceptor是拦截器负责处理请求接口后的文件 再使用UploadedFile进行文件接收。

import {  UploadedFile, UseInterceptors, Body, Post, Query } from '@nestjs/common';
import {  FileInterceptor } from '@nestjs/platform-express';

@Post('/upload')
// "file" 表示 上传文件的键名
@UseInterceptors(FileInterceptor('file'))
public async uploadFileUsingPOST(
  @Query() query: any,
  @Body() body: any,
  @UploadedFile() file,
) {
  // body为form/data中的其他非文件参数
  // query为请求中的Query参数
  console.log(file, body, query);
  return "上传成功";
}

由于思维惯性的影响,对于文件的处理产生了先入为主的思想,下意识的认为接口中获取到的前端上传文件格式仍然为WriteStream,结果在处理过程中发现文件格式变成了Buffer形式的二进制。因此在这个过程中我们就有需要再次处理从BufferReadStream的转换。

而在这个过程中,我顺便做了文件名称的混淆,而我采取的方式也是一个较笨的方式,直接上代码:

import { v4 } from 'uuid';
import * as fs from 'fs';

// 使用uuid作为文件名称,并且保留文件后缀
const newName: string = `${v4()}.${file.originalname.split('.')[1]}`;
// 将文件写入本地
fs.writeFileSync(newName, file.buffer);
// 使用本地文件生成ReadStream
const newFile = fs.createReadStream(newName);
// 生成请求使用的FormData
const formData = new FormData();
formData.append('files[]', newFile);

/**
  POST formData,完成文件上传
*/
fs.rmSync(newName); // 上传完成后,移除本地文件

文件格式校验

在解决了文件上传逻辑以及格式转换的问题后,我们再回过头来看一下是不是所有文件类型都允许上传至我们的oss或cdn上呢?这过程中会不会混入一些我们“不喜欢”的文件。

这里简单以NestJS的逻辑为例,简单列举一下代码。

import {  UploadedFile, UseInterceptors, Body, Post, Query } from '@nestjs/common';
import {  FileInterceptor } from '@nestjs/platform-express';

@Post('/upload')
@UseInterceptors(FileInterceptor('file'))
public async uploadFileUsingPOST(
  @Query() query: any,
  @Body() body: any,
  @UploadedFile() file,
) {
  // 定义我们允许上传的文件类型白名单      
  const filterType: string[] = ['image', 'video'];
  const { mimetype } = file;
  // 判断当前上传至接口的文件类型是否在白名单中,如果在则允许上传,不在则返回错误信息
  if (filterType.findIndex((f: string) => mimetype.includes(f)) < 0) {
    return {
      result: -1,
      errMessage: "文件格式错误,仅支持上传图片、动图或视频",
      success: false
    };
  }
  return {
    result: 1,
    message: "上传成功",
    success: true
  };
}

总结

其实单纯就逻辑来讲,这是一件很简单的事情。无非就是我们获取文件流后用node服务作为“中转站”添加逻辑后再上传至“终点”。只不过重点还是在于我上面列举过的四个问题上:

  1. 我们的node服务中获取到的前端上传的文件到底是什么格式?
  2. 我们进行上传oss/cdn的接口,需要我们上传的文件格式又是什么样的?
  3. 文件名称如何保持不变/如何进行混淆?
  4. 如何完成文件格式的校验或过滤?

而解决这四个问题的重点,其实也很简单:

  1. 弄清楚我们获取到的类型与我们最终需要的类型到底是什么;
  2. 学习好不同文件类型之间的关系转换方式
  3. 想明白我们最终要上传的文件以一个什么样的名字来进行上传;
  4. 做好文件类型的白名单控制
  5. 杜绝惯性思维,了解清楚不同框架/技术栈之间到底有什么不同,再着手逻辑的开发。

参考文献

Blob - Web API 接口参考 | MDN

File - Web API 接口参考 | MDN

Stream | Node.js v15.14.0 Documentation

Buffer | Node.js v15.14.0 Documentaion

NestJS - 拦截器

NestJS - 文件上传

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容