《上传那些事儿之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 中有四种基本的流类型:
Writable
: 可以写入数据的流(例如,fs.createWriteStream()
)。Readable
:可以从中读取数据的流(例如,fs.createReadStream()
)。Duplex
: 两者都是Readable
和的流Writable
(例如,net.Socket
)。Transform
:Duplex
可以在写入和读取数据时修改或转换数据的流(例如,zlib.createDeflate()
)。
开发前的规划
在我们进行文件上传的过程中,经历了两个阶段:
- 获取前端上传的文件
- 处理文件后,调用内部服务上传至cdn
其实这样看来的话,这是很简单的两个阶段,我们只需要拿到前端的文件后传递给另外一个接口就可以了,可是在这个过程中,有几个我们不得忽视的问题:
- 我们的node服务中获取到的前端上传的文件到底是什么格式?
- 我们进行上传oss/cdn的接口,需要我们上传的文件格式又是什么样的?
- 文件名称如何保持不变/如何进行混淆?
- 如何完成文件格式的校验或过滤?
只有在考虑清楚了以上这些内容的处理之后,才应该来考虑我们接口本身的业务逻辑的完善与开发。
开发中的问题
由于一些内部原因,Node端的开发经历了从koa2到express的重构。所以针对两个框架的文件处理,我也都有幸(bushi)全都经历了一次。
node上传格式
由于上传至oss的第三方接口可以在前端调用,也可以在node中进行调用,所以在Postman中可以模仿上传过程,由此可以看到第三方接口真正需要我们传入的其实是一个ReadStream
格式的文件。
所以我们的目标也很简单,那就是无论我们获取到什么格式的文件,都转换成为ReadStream格式即可。
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重新生成新名称,也不愿意直接使用内存地址作为文件名称呢?
很显然,因为这个流程对于我们来说是可控的。
NestJSexpress
由于一些公司内部的历史原因,导致在使用koa2的开发过程中,缺少了一些swagger相关的功能实现。不得不使用NestJS+express来重构整个项目
而在NestJS中的上传,则需要使用NestJS提供的拦截器UseInterceptors
,同时也需要依赖FileInterceptor
和UploadedFile
来对于单文件上传的处理。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
形式的二进制。因此在这个过程中我们就有需要再次处理从Buffer
到ReadStream
的转换。
而在这个过程中,我顺便做了文件名称的混淆,而我采取的方式也是一个较笨的方式,直接上代码:
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服务作为“中转站”添加逻辑后再上传至“终点”。只不过重点还是在于我上面列举过的四个问题上:
- 我们的node服务中获取到的前端上传的文件到底是什么格式?
- 我们进行上传oss/cdn的接口,需要我们上传的文件格式又是什么样的?
- 文件名称如何保持不变/如何进行混淆?
- 如何完成文件格式的校验或过滤?
而解决这四个问题的重点,其实也很简单:
- 弄清楚我们获取到的类型与我们最终需要的类型到底是什么;
- 学习好不同文件类型之间的关系与转换方式;
- 想明白我们最终要上传的文件以一个什么样的名字来进行上传;
- 做好文件类型的白名单控制
- 杜绝
惯性思维
,了解清楚不同框架/技术栈
之间到底有什么不同,再着手逻辑的开发。
参考文献
Blob - Web API 接口参考 | MDN
File - Web API 接口参考 | MDN
Stream | Node.js v15.14.0 Documentation
Buffer | Node.js v15.14.0 Documentaion
NestJS - 拦截器
NestJS - 文件上传
《上传那些事儿之Nest与Koa》——文件格式怎么了!的更多相关文章
- 【转载】文件上传那些事儿,文件ajax无刷上传
导语 正好新人导师让我看看能否把产品目前使用的FileUploader从老的组件库分离出来的,自己也查阅了相关的各种资料,对文件上传的这些事有了更进一步的了解.把这些知识点总结一下,供自己日后回顾,也 ...
- 基于AngularJs的上传控件-angular-file-upload
今天跟大家分享的是一个依赖于angular的上传控件. 前段时间做项目遇到一个需求是上传文件,大概需要实现的样式是这样子的,见下图: 需要同时上传两个文件.并且规定文件格式和文件大小.因为前端框架使用 ...
- [转]html5表单上传控件Files API
表单上传控件:<input type="file" />(IE9及以下不支持下面这些功能,其它浏览器最新版本均已支持.) 1.允许上传文件数量 允许选择多个文件:< ...
- 利用ServletFileUpload组件上传文件
自己的运用: public void UploadNoteFile(HttpServletRequest request,HttpServletResponse response){ String ...
- 在MVC中利用uploadify插件实现上传文件的功能
趁着近段的空闲时间,开发任务不是很重,就一直想把以前在仓促时间里所写的多文件上传功能改一下,在网上找了很多例子,觉得uploadify还可以,就想用它来试试.实现自己想要的功能.根据官网的开发文档,同 ...
- 关于PHP上传文件时配置 php.ini 中的 upload_tmp_dir
在<PHP 5.3 入门经典>9.6.3 的试一试中(P235),给出了一个上传文件的例子,这里的文件格式为jpeg图片(image/jpeg).如果之前未配置 php.ini 中的 up ...
- js 文件异步上传 显示进度条 显示上传速度 预览文件
通常文件异步提交有几个关键 1.支持拖拽放入文件.2.限制文件格式.3.预览图片文件.4.上传进度,速度等,上传途中取消上传.5.数据与文件同时上传 现在开始笔记: 需要一个最基础的元素<inp ...
- SSM实现图片上传管理操作
Spring MVC 实现文件上传 时序图 利用 Spring MVC 实现文件上传功能,离不开对 MultipartResolver 的设置.MultipartResolver 这个类,你可以将其视 ...
- Ant Design Upload 组件上传文件到云服务器 - 七牛云、腾讯云和阿里云的分别实现
在前端项目中经常遇到上传文件的需求,ant design 作为 react 的前端框架,提供的 upload 组件为上传文件提供了很大的方便,官方提供的各种形式的上传基本上可以覆盖大多数的场景,但是对 ...
随机推荐
- Luogu1137 旅行计划 (拓扑排序)
每次入队时DP : \(f[v] = \max \{f[u] + 1\}\) #include <iostream> #include <cstdio> #include &l ...
- Oracle-视图,约束
试图:试图是数据库对象之一视图在sql语句中体现的角色与表一致,但它不是一张真是存在的表,只是对应了一个查询语句的结果集当试图对应的子查询中含有函数或者表达式时,那么必须指定别名试图根据对应的子查询分 ...
- virtio 驱动的数据结构理解
ps:本文基于4.19.204内核 Q:vqueue的结构成员解释: A:结构如下,解析附后: struct virtqueue { struct list_head list;//caq:一个vir ...
- 应用性能监控:SkyWalking
目录 SkyWalking 简介 SkyWalking 搭建 平台后端(Backend) 平台前端(UI) Java Agent(Java 应用监控) Java Agent 下载 Java 演练项目 ...
- 踩坑之旅:配置 ROS 环境
以下内容为本人的著作,如需要转载,请声明原文链接微信公众号「englyf」https://www.cnblogs.com/englyf/p/16660252.html 最近在学习机器人相关的导航算法, ...
- KingbaseES V8R6 账号异常登录锁定案例
数据库版本: test=> select version(); version --------------------------------------------------------- ...
- Fast.Framework ORM 于中秋节后 正式开源
Fast Framework 作者 Mr-zhong 开源项目地址 https://github.com/China-Mr-zhong/Fast.Framework QQ交流群 954866406 欢 ...
- 快速排序C语言版图文详解
算法原理:选一个数位基准,将序列分成两个部分,一边全是比它小序列,另一边全是比它大序列.然后再分别对比他小的序列和比再次进行基准分割.依次分割下去,得到一个有序的队列. 原理图示: 编辑 编辑 ...
- day38-IO流05
JavaIO流05 4.常用的类04 4.4节点流和处理流03 4.4.8打印流-PrintStream和PrintWriter 打印流只有输出流,没有输入流 1.简单介绍及应用 PrintStrea ...
- 最强cron解析器
背景 大家有没有这么一种困境 我现在需要去配置一个定时任务:"每天早上九点执行任务" 若你有一个好的定时任务平台,相信很容易就能配置完成.那若是没有定时任务平台呢?是不是就要自己写 ...