前言
开发过程中,肯定会用到上传文件功能,这里目前就讨论两种情况,分别是直接使用文件系统保存到磁盘(常见)
、文件存储服务库(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 也不是不行
这篇就讲这么多了,希望大家有所收获