nestjs上传文件到磁盘与minio
前言
开发过程中,肯定会用到上传文件功能,这里目前就讨论两种情况,分别是直接使用文件系统保存到磁盘(常见)
、文件存储服务库(minio)
还有其他存储服务的都可以类推,其中 minio 服务和 mysql
一样,都是需要下载配置的,算是用的很多的一个服务了(并且还支持远端购买空间,一般都是用自己服务器吧😅)
minio 地址
上传到磁盘
这里的磁盘默认就是在本地,如果文件服务器设置到另一个大储存空间那端,那么另一端也可以像这样配置,只不过就是需要其他服务器间接调用,甚至可以直接调用都可以
项目配置
使用前,我们先安装文件库
yarn add multer @nestjs/platform-express yarn add @types/multer --dev //获取ts支持 //这是单纯为为了文件不重名且方便管理引用的事件库,比较小,足够日常使用,当然也可以使用moment yarn add dayjs
创建 file 模块,用于我们编写功能,我们仍然是使用 res 创建几个常用文件
nest g res file
ps
: File 库会跟系统的一些库重名,引用时额外注意位置,有必要的话改个名字(例如: store、diskfile、filestore都行)
如下所示已创建完毕了
我们进入 .file.module
中添加内容, @Module
中 import
我们的数据库 File
,同时如下所示,注册 MulterModule
模块,注册后,每次上传文件都会走到下面的 filename 后买你的回调
这是我们下载用的操作,基本都在这里了,设置目录、文件名字
等等
import dayjs = require('dayjs'); //上传配置 imports: [ TypeOrmModule.forFeature([File]), //后续再说其作用 MulterModule.register({ storage: diskStorage({ // 配置文件上传后的文件夹路径,设置到我们实际项目并列的 public 文件夹中 destination: `./public/uploads/${dayjs().format('YYYY-MM-DD')}`, filename: (req, file, cb) => { //文件上传之后的回调,文件为空不走 //取出结尾的类型为扩展名,如果没有扩展名,多拼一个点也没事,实际使用也没影响 let ext = file.originalname.split('.').at(-1) // 在此处自定义保存后的文件名称,仍然使用原后缀名,没有就不用 let filename = `${new Date().getTime()}.${ext ? ext : ''}` return cb(null, filename); }, }), }), ],
controller 中编写我们的接口,当然实际逻辑应当到我们的 service 中编写,controller 永远做分发功能,也方便我们理清逻辑
另外,前面编写完毕后,到这里获取 file 时,实际上已经保存到磁盘了,当然如果文件为空,是不会走上面的保存回调的,我们这里获取到的 file 也会是空
上传单个文件 @Post('file') @UseInterceptors(FileInterceptor('file')) uploadFiles( @UploadedFile() file: Express.Multer.File, ) { console.log(files); } //上传多个文件 @Post('file') @UseInterceptors(FilesInterceptor('file')) uploadFiles( @UploadedFiles() files: Array, ) { console.log(files); } //上传带其他参数的文件 @Post('file') @UseInterceptors(AnyFilesInterceptor(file)) uploadFile( @Body() objDto: ObjDto, @UploadedFiles() files: Array, ) { console.log(files); console.log(objDto) }
实际使用的最多的是单个文件上传,其他的都可以被代替(大不了调用多个接口是吧)
访问配置
到这里还没结束,file 中保存的 path 等,默认应当拼接我们的路由前缀就可以访问才对,这里实际上不能直接访问,还需要我们配置,也就是授权的目录才可以直接访问
在我们的 main 中添加目录,这里是设置 public 可以随意访问,从下面可以看出与我们的项目并列,注意部署多个项目时别被其他项目的窜到一个文件夹了
import * as express from 'express'; app.use('/public', express.static(`${__dirname}/../public`));
文件与数据库问答
疑问
:
有了文件为什么还要创建数据库表格存放到里面
回答
:
文件存放后,我们要保存它的路径,后续才能直接访问到他,否则,他就成了无主之物一般,我们无法知道他的位置,也无法访问了,另外我们的数据库除了保存文件的基础信息,我们还会被关联到相应的人,一个人可以发布多张图,一张图可以被多个人点赞、收藏、设置使用等等
数据库存放文件
不多介绍了,保存内容,方便使用,顺便引出文档
async upload( mFile: Express.Multer.File, user: User, ) { if (!mFile) { return ResponseData.fail('请选择文件'); } console.log(mFile) let file = new File() file.originalname = mFile.originalname; file.mimetype = mFile.mimetype; file.size = mFile.size; file.path = mFile.path; file.user = user; await this.fileRepository.save(file) return ResponseData.ok(file) }
文档
上面只介绍了怎么用,但是却没有配置文档,下面配置一下参数文档(返回的前面有介绍,就不介绍了)
@ApiOperation({ summary: '上传文件到磁盘' }) @ApiConsumes('multipart/form-data') //设置类型form-data ApiBody({ //设置file类型 schema: { type: 'object', properties: { file: { type: 'string', format: 'binary', }, }, }, }) @Post('upload') @UseInterceptors(FileInterceptor('file')) upload( @UploadedFile() file: Express.Multer.File, @ReqUser() user: User, //用户信息,可以用来与用户绑定,也可以不绑定 ) { //存放文件到数据库,这时已经存放完毕了,我们将一些信息放到数据库,为了好方便建立关系 return this.fileService.upload(file, user); //如果想与 user 建立联系,可以继续往后写 }
上面的两个文档参数设置很不方便,如果有多个上传就比较臃肿了,我们可以封装一下,新建一个装饰器,创建为 file.decorator.ts
,在里面设置一下
import { ApiBody, ApiConsumes } from "@nestjs/swagger" //我们包装一下,至少用着方便了 export const ApiFileConsumes = () => ApiConsumes('multipart/form-data') export const ApiFileBody = () => ApiBody({ schema: { type: 'object', properties: { file: { type: 'string', format: 'binary', }, }, }, })
简化后的装饰器配置
@ApiOperation({ summary: '上传文件到磁盘' }) @ApiFileConsumes() @ApiFileBody() @Public() @APIResponse() @Post('upload') @UseInterceptors(FileInterceptor('file')) upload( @UploadedFile() file: Express.Multer.File, @ReqUser() user: User, ) { //存放文件到数据库,这时已经存放完毕了,我们将一些信息放到数据库,为了好方便建立关系 return this.fileService.upload(file, user); //如果想与 user 建立联系,可以继续往后写 }
上传到minio储存库
minio 是一个高性能的文件存储服务仓库,且支持存放超大文件对象,最高支持 5 TB(未来甚至可能更多),且支持购买远端(一般不购买),是很多大企业文件服务器的选择
minio-文档地址、minio-js文档
环境配置
这里以 mac 为例, 其他的端的文档也有,差不太多,根据自己需要配置即可
//安装minio brew install minio/stable/minio
创建目录,并运行
//创建~/minio文件夹 mkdir ~/minio //启动minio服务,并设置保存目录为 ~/minio minio server ~/minio --console-address :9090
这样就启动成功了,我们可以看到用户名密码,也可以看到地址了,我们直接进入即可
后面我们保存的基本上就在这个 minio 文件夹了,后面是我创建的 bucket 仓库
我们复制上面的地址,然后使用给定的密码进入即可
http://127.0.0.1:9090
然后设置 buckets
、 accessKey
配置, object browser
查看管理上传文件(自己简单摸索下,这三个看了可以直接用)
地址和端口号就不多说了吧 127.0.0.1:9000
, :
前后就是,两个 key 记录下来,后面有用(地址如果是另一个服务器部署的,使用另一个服务器的地址,没有端口号就不填写即可)
项目配置
导入minio
yarn add minio
下面我们唯一需要做的就是看 minio-js文档,这时候可以直接调用里面的api了
我们直接创建 minio 客户端,然后设置我们的信息
import { Client } from 'minio'; //里面的参数,我们可以和我们的其他信息放到一起方便修改 this.client = new Client({ endPoint: '127.0.0.1', //ip,我们的 minio 所在服务地址 port: 9000, //端口号,又就传没有就不传 useSSL: false, accessKey: '123123123' //我们设置的 accesskey复制一下即可, secretKey: '1231231', });
这里面我们主要会用到下面几个方法,一个是直接上传 buff 内容,第二个
//直接上传接收到的 buff putFile(filename: string, buffer: Buffer) { //如果想两个端都存在文件,可以使用 fPutObject 逻辑更简单 return this.client.putObject( envConfig.minioBucketName, filename, buffer ) //{ etag: '4889457ca823d079a800e4a5f427b353', versionId: null } } //如果本地磁盘保存了(结合上面的磁盘),备份到minio,可以直接利用file来上海窜 fPutFile(filename: string, path: string) { return this.client.fPutObject( envConfig.minioBucketName, filename, path ) } //获取url签名,默认7天,可以设置时间,数据库获取图片url时,可以通过这个获取 //避免别人知道url就可以肆意访问我们的仓库数据(另外没授权的用户也不能随意访问) presignedUrl(filename: string) { return this.client.presignedUrl( 'GET', envConfig.minioBucketName, filename, // 7 * 24 * 60 ^ 60 //时长 s 默认7天 ) }
如果需要用到大文件下载,可以直接去文档搜索 getObject
之类的走下载流程,另外如果出现了数据库和bucket数量不一致,也可以通过遍历方式进行对比,删除多余的信息即可
实现上传
上传分为两种,一个是只使用minio作为文件系统,另一个是使用 disk 作为文件系统,minio作为备份系统
仅仅上传minio
如果单纯使用 minio,不使用磁盘,请将前面的 disk 相关移除(测试的话,将 module 中相关代码删除),否则我们获取到的 Express.Multer.File
将会不存在 buff
信息,只会有文件路径相关信息
async uploadMinio( mFile: Express.Multer.File ) { //文件没上传 if (!mFile) { return ResponseData.fail('请选择文件'); } let url = null //生成一个唯一的 filename let ext = originalname.split('.').at(-1); // 在此处自定义保存后的文件名称 let filename = `${new Date().getTime()}.${ext ? ext : ''}`; // let filename = getFilename(mFile.originalname); //这是我们封装的方便使用 try { //直接 put 到 minio 中即可 await this.minioService.putFile(filename, mFile.buffer); //我们还要顺道给用户返回一个url,不然他上传后无法访问,还得掉接口 url = await this.minioService.presignedUrl(filename) } catch(err) { console.log(err) return '失败了'; } //将文件信息保存到数据库中,方便进行关联 let file = new File() file.filename = filename; file.mimetype = mFile.mimetype; file.size = mFile.size; await this.fileRepository.save(file) return '成功了'; }
同时上传disk和minio
disk 上传的逻辑和前面的一样,minio 的逻辑发生了一些变化,由于上传disk,Express.Multer.File
获取到的就是文件信息了,buff信息就没了,但是有文件path路径,我们可以通过文件 path 上传到 minio,也就是 fPutFile 方法,如下所示i
async uploadMinioEx( mFile: Express.Multer.File ) { if (!mFile) { return ResponseData.fail('请选择文件'); } try { //直接上传的 minio await this.minioService.fPutFile(mFile.filename, mFile.path); } catch(err) { return ResponseData.fail() } //成功后写入到数据库,我们甚至可以在数据库中加入是否备份的参数,备份作为可选操作,定期备份 let file = new File() file.filename = mFile.filename; file.mimetype = mFile.mimetype; file.size = mFile.size; file.path = mFile.path; await this.fileRepository.save(file) return ResponseData.ok({ ...file, url: envConfig.filePre + file.path }); }
最后
相信也了解过分布式,我们的多个服务可能分不到多个服务器上,因此会涉及到不同的ip端口号等,这也是需要额外注意的
ps
: 如果是小项目,只有一个用户小头像,那么不配置文件都是可以的,让用户直接 post 上传 base64 图片即可,我们直接保存到 mysql 也不是不行
这篇就讲这么多了,希望大家有所收获