Nestjs 通过拦截器上传文件和windows文件地址转换处理以及上传文件资源访问
前言
Nestjs的文件上传官方文档给的讲解非常少,在掘金搜对应文章也大多数不全,有的都是好几年前的处理了,这里贴一下我学习到的处理方式。
首先我们需要三个依赖:
multer
,这个是一个express的中间件,它只会处理multipart/form-data
类型的表单数据,主要用于上传文件。@types/multer
这个是multer的一个类型声明依赖。@nestjs/platform-express
,这个依赖提供了上传模块和拦截器用于处理上传,它本身也依赖于multer。
大概流程:
首先我们从 @nestjs/platform-express
引入MulterModule
模块进行注册,为此我们可以创建一个专门处理上传的Nestjs模块upload
,upload负责文件上传相关逻辑,upload里面依赖MulterModule。
MulterModule
在注册的时候传入配置项,比如控制上传的目录,定义上传文件名的处理。
注册完毕后我们使用拦截器处理上传文件,本次只处理本地文件上传处理,如果你是oss之类的第三方上传,原理相同,可以通过自定义拦截器的形式处理它。
上传文件后我们通过对应的参数装饰器获取到上传后返回的参数对象,由于在windows环境中,path的路径是\\
形式,我们需要将其转为统一的/
形式。
安装依赖
pnpm i multer @nestjs/platform-express
pnpm i @types/multer -D
创建upload模块和控制器、服务
nest g mo upload --no-spec
nest g co upload --no-spec
nest g s upload --no-spec
此时文件我们已经创建好了,下面去配置一个环境变量。
配置环境变量
环境变量用于控制上传的文件存储路径。
.env.development
# upload-path
UPLOAD_PATH="uploads"
此时他会以项目根目录为基准,假设我们的项目在:D:/code/nest-project
。
那么上传的目录就是:D:/code/nest-project/uploads
。
我们开发时候这个uploads目录会在根目录,当我们把dist正式部署的时候,这个uploads目录其实还是在项目根目录下,所以路径上没什么问题。
如果没有这个目录插件会自动创建,所以不需要手动补上。
配置上传模块
打开upload.module.ts
import { Module } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { MulterModule } from "@nestjs/platform-express";
import { diskStorage } from "multer";
import { extname } from "path";
import { UploadController } from "./upload.controller";
import { UploadService } from "./upload.service";
@Module({
imports: [
MulterModule.registerAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
storage: diskStorage({
destination: configService.get("UPLOAD_PATH"),
filename: (req, file, callback) => {
const path = `${Date.now()}-${Math.round(Math.random() * 1e10)}${extname(file.originalname)}`;
callback(null, path);
}
})
})
})
],
controllers: [UploadController],
providers: [UploadService]
})
export class UploadModule {}
首先我们通过异步注册的时候,保证可以拿到ConfigService
服务以及对应的环境变量数据。
通过useFactory
工厂函数获取到inject注册的config服务。
我们return一个对象,配置这个对象其实就是multer
的配置对象,可以查看依赖文档:multer(opts)。
我这边也简单提供一个翻译的table表
Key | Description |
---|---|
dest or storage | 文件的存储位置 |
fileFilter | 控制接受哪些文件的函数,也就是过滤掉不需要文件的一个方法,后续可以在Nestjs拦截器里做,这里不设置 |
limits | 上传数据的限制,文件数量,大小之类的 |
preservePath | 保留文件的完整路径,而不仅仅是基本名称 |
事实上multer自己的配置参数中是没有storage
属性的,这个是因为@nestjs/platform-express自己做了封装,这个属性对应了multer的diskStorage(opt)
方法的配置对象。
storage
表示本次磁盘存储配置,它有两个配置项:
destination
,用于确定上传的文件应存储在哪个文件夹中。可以是string,也可以是函数,函数。filename
,用于确定文件夹内的文件应命名的内容。如果不配置会给每个文件随机一个不包含文件扩展名的随机名(从安全的角度)。
具体可以查看文档:DiskStorage
上面的配置中,destination我直接从configServer中取了环境变量;filename则是一个:时间戳+随机数+文件扩展名。
注意:
此时我们的上传模块其实是只会生效于upload
模块,如果后续自己的项目好几个模块需要用到上传,可以考虑将其设置为全局模块,理论上将我们在app.module
中设置全局模块最好,但是在Nestjs中,哪怕我们在app.module
引入的模块A里面,A里面的imports有一个全局模块,这个模块也是会共享到全局的。
简单使用拦截器实现文件上传
我们先来实现一个单文件上传。
import { Controller, Post, UseInterceptors, UploadedFile } from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";
@Controller("upload")
export class UploadController {
/** 上传图片 */
@Post("image")
@UseInterceptors(FileInterceptor('file'))
async uploadImage(@UploadedFile() file: Express.Multer.File) {
return file;
}
}
上传文件,注意必须使用form-data
的形式,上传的字段名:file
,也就是FileInterceptor
传入的参数。
得到如下返回:
{
"fieldname": "file",
"originalname": "006bAExCly1ha33u1pzyej30jg0l0abx.jpg",
"encoding": "7bit",
"mimetype": "image/jpeg",
"destination": "uploads",
"filename": "1704299214142-7746184565.jpg",
"path": "uploads\\1704299214142-7746184565.jpg",
"size": 83019
}
我们可以发现有两个问题:
- 拦截器使用繁琐,这还是没有定制数据要求的情况。
- 返回的文件对象里path的路径不是我们需要的。
不过在此之前我们先看一下Nestjs中文件上传的文档:文件上传
可以看到针对文件上传,会有三种拦截器:
FileInterceptor
,单字段单文件上传;FilesInterceptor
,单字段多文件上传,文件数组;FileFieldsInterceptor
,多字段多文件上传;
对应获取文件参数的装饰器:
UploadedFile
,单字段单文件上传后获取文件对象;UploadedFiles
,单字段多文件或者多字段多文件获取文件对象;
具体使用文档都有,可以自行查看效果更加。
解决拦截器使用繁琐的问题
我们可以自己自定义一个装饰器来封装这个拦截器处理,我们代码如下:
先创建一个types.ts
文件处理类型声明:
import { MulterOptions } from "@nestjs/platform-express/multer/interfaces/multer-options.interface";
export type { MulterOptions };
/** 文件类型 */
export type UploadSingleType = "image" | "file";
export type UploadMultipleType = "images" | "files";
export type UploadType = UploadSingleType | UploadMultipleType;
/** 文件上传配置 */
export type UploadConfig = Record<UploadType, MulterOptions>;
/** 过滤器函数参数1 */
export type Req = Parameters<MulterOptions["fileFilter"]>[0];
/** 过滤器函数参数2 */
export type File = Parameters<MulterOptions["fileFilter"]>[1];
/** 过滤器函数参数3 */
export type Callback = Parameters<MulterOptions["fileFilter"]>[2];
再创建一个upload.ts
文件。
import { MethodNotAllowedException, UseInterceptors, applyDecorators } from "@nestjs/common";
import { FileInterceptor, FilesInterceptor } from "@nestjs/platform-express";
import type {
Callback,
File,
MulterOptions,
Req,
UploadConfig,
UploadMultipleType,
UploadSingleType,
UploadType
} from "./types";
/** 图片过滤 */
function imageFilter(req: Req, file: File, callback: Callback) {
if (file.mimetype.includes("image")) {
callback(null, true);
} else {
callback(new MethodNotAllowedException("请上传图片文件"), false);
}
}
/** file过滤 */
function fileFilter(req: Req, file: File, callback: Callback) {
callback(null, true);
}
const singleType: UploadSingleType[] = ["image", "file"];
const multipleType: UploadMultipleType[] = ["images", "files"];
/** 文件上传配置 */
const uploadConfig: UploadConfig = {
image: {
limits: {
fields: 1,
fileSize: Math.pow(1024, 2) * 2
},
fileFilter: imageFilter
},
images: {
limits: {
fileSize: Math.pow(1024, 2) * 2
},
fileFilter: imageFilter
},
file: {
limits: {
fields: 1,
fileSize: Math.pow(1024, 2) * 5
},
fileFilter: fileFilter
},
files: {
limits: {
fileSize: Math.pow(1024, 2) * 5
},
fileFilter: fileFilter
}
};
/** 文件上传装饰器 */
export function UploadFile(fieldName: string, type: UploadType, options?: MulterOptions) {
const defaultOptions = uploadConfig[type];
if (options) Object.assign(defaultOptions, options);
if (singleType.includes(type as any)) {
return applyDecorators(UseInterceptors(FileInterceptor(fieldName, defaultOptions)));
} else if (multipleType.includes(type as any)) {
return applyDecorators(UseInterceptors(FilesInterceptor(fieldName, 20, defaultOptions)));
}
}
在配置项中fileSize
的单位是字节,已知:1kb = 1024Byte(字节) 1mb = 1024kb
,所以1mb可以写作:Math.pow(1024, 2)
。
fileFilter
用于过滤文件类型,我们可以通过文件的mime
类型来做判断,大家可以自己查询文件类型的mime表自定义处理。
最后UploadFile
装饰器接收三个参数,一个是上传文件的字段名,一个是上传的类型,一个是配置选项。
这里我只封装了,单个字段单文件和文件数组的上传,多字段的相对复杂一些,等以后用到了再更新一下代码吧。
解决Windows下文件对象里path的路径不对的问题
这个是因为windows和其他操作系统使用的文件系统不一样导致的,如果你是一个mac系统,或者是linux系统就不会有这个问题,但是我自己是在windows下的,所以为了方便开发,进行了修复处理,因为在web访问中,资源文件的路径都是/
的形式。
首先我的实现思路就是通过拦截器来实现,在拦截器中我们可以通过map
方法获取到数据对象,然后我们只需要将返回的数据对象里的path进行调整即可。
创建一个interceptor
目录,在该目录下创建filePath.ts
文件。
import type { CallHandler, ExecutionContext, NestInterceptor } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
/** windows下的路径替换 */
function pathReplace(data: Express.Multer.File) {
if (data?.path) data.path = data.path.replace(/\\/g, "/");
return data;
}
@Injectable()
export class FilePath implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
if (Array.isArray(data)) {
data.forEach(pathReplace);
} else {
pathReplace(data);
}
return data;
})
);
}
}
如果是文件数组,那么data就是一个数组,所以判断下是否为数组,然后通过正则替换掉错误路径。
然后我们再去upload.ts
装饰器中使用它。
import { MethodNotAllowedException, UseInterceptors, applyDecorators } from "@nestjs/common";
import { FileInterceptor, FilesInterceptor } from "@nestjs/platform-express";
import { FilePath } from "src/utils/interceptor/filePath";
import type {
Callback,
File,
MulterOptions,
Req,
UploadConfig,
UploadMultipleType,
UploadSingleType,
UploadType
} from "./types";
/** 图片过滤 */
function imageFilter(req: Req, file: File, callback: Callback) {
if (file.mimetype.includes("image")) {
callback(null, true);
} else {
callback(new MethodNotAllowedException("请上传图片文件"), false);
}
}
/** file过滤 */
function fileFilter(req: Req, file: File, callback: Callback) {
callback(null, true);
}
const singleType: UploadSingleType[] = ["image", "file"];
const multipleType: UploadMultipleType[] = ["images", "files"];
/** 文件上传配置 */
const uploadConfig: UploadConfig = {
image: {
limits: {
fields: 1,
fileSize: Math.pow(1024, 2) * 2
},
fileFilter: imageFilter
},
images: {
limits: {
fileSize: Math.pow(1024, 2) * 2
},
fileFilter: imageFilter
},
file: {
limits: {
fields: 1,
fileSize: Math.pow(1024, 2) * 5
},
fileFilter: fileFilter
},
files: {
limits: {
fileSize: Math.pow(1024, 2) * 5
},
fileFilter: fileFilter
}
};
/** 文件上传装饰器 */
export function UploadFile(fieldName: string, type: UploadType, options?: MulterOptions) {
const defaultOptions = uploadConfig[type];
if (options) Object.assign(defaultOptions, options);
if (singleType.includes(type as any)) {
return applyDecorators(UseInterceptors(FileInterceptor(fieldName, defaultOptions), FilePath));
} else if (multipleType.includes(type as any)) {
return applyDecorators(UseInterceptors(FilesInterceptor(fieldName, 20, defaultOptions), FilePath));
}
}
使用
upload.controller.ts 控制器中使用它们。
import { Controller, Post, UploadedFile } from "@nestjs/common";
import { UploadFile } from "src/utils/decorators";
@Controller("upload")
export class UploadController {
/** 上传图片 */
@Post("image")
@UploadFile("file", "image")
async uploadImage(@UploadedFile() file: Express.Multer.File) {
return file;
}
}
返回内容:
{
"fieldname": "file",
"originalname": "006bAExCly1ha33u1pzyej30jg0l0abx.jpg",
"encoding": "7bit",
"mimetype": "image/jpeg",
"destination": "uploads",
"filename": "1704300490305-1291448715.jpg",
"path": "uploads/1704300490305-1291448715.jpg",
"size": 83019
}
如果是单字段多文件:
import { Controller, Post, UploadedFiles } from "@nestjs/common";
import { UploadFile } from "src/utils/decorators";
@Controller("upload")
export class UploadController {
/** 上传图片 */
@Post("image")
@UploadFile("file", "images")
async uploadImage(@UploadedFiles() file: Express.Multer.File[]) {
return file;
}
}
[
{
"fieldname": "file",
"originalname": "006bAExCly1ha33u1pzyej30jg0l0abx.jpg",
"encoding": "7bit",
"mimetype": "image/jpeg",
"destination": "uploads",
"filename": "1704300549625-6121314323.jpg",
"path": "uploads/1704300549625-6121314323.jpg",
"size": 83019
},
{
"fieldname": "file",
"originalname": "9e056509c93d70cf97e3743ebedcd100baa12ba0.jpg",
"encoding": "7bit",
"mimetype": "image/jpeg",
"destination": "uploads",
"filename": "1704300549627-816265949.jpg",
"path": "uploads/1704300549627-816265949.jpg",
"size": 139923
}
]
也是没啥问题的。
配置静态资源访问
文件上传完毕后,自然要进行访问了,我们可以配置一个静态资源访问处理。
main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import type { NestExpressApplication } from "@nestjs/platform-express";
import { ConfigService } from "@nestjs/config";
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
const configService = app.get(ConfigService);
/** 静态资源 */
app.useStaticAssets(configService.get("UPLOAD_PATH"), { prefix: `/${configService.get("UPLOAD_PATH")}` });
await app.listen(3000);
}
bootstrap();
通过useStaticAssets
方法实现一个静态资源访问,其中第一个参数是项目中你需要访问的资源目录,这里我们通过获取环境变量中配置的上传目录。
第二个参数表示的是虚拟路径,如果不配置虚拟路径,用户可以直接访问当前域名+文件名查看资源:
http://localhost:3000/1704299214142-7746184565.jpg
如果我们配置了虚拟路径,比如上面的/uploads
,那么就比如加上对应的虚拟路径才能访问:
http://localhost:3000/uploads/1704299214142-7746184565.jpg
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据