nestjs-上传文件到磁盘与minio

前言

开发过程中,肯定会用到上传文件功能,这里目前就讨论两种情况,分别是直接使用文件系统保存到磁盘(常见)文件存储服务库(minio)

还有其他存储服务的都可以类推,其中 minio 服务和 mysql 一样,都是需要下载配置的,算是用的很多的一个服务了(并且还支持远端购买空间,一般都是用自己服务器吧😅)

minio 地址

上传到磁盘

这里的磁盘默认就是在本地,如果文件服务器设置到另一个大储存空间那端,那么另一端也可以像这样配置,只不过就是需要其他服务器间接调用,甚至可以直接调用都可以

项目配置

使用前,我们先安装文件库

js 复制代码
yarn add multer
yarn add @types/multer --dev //获取ts支持

//这是单纯为为了文件不重名且方便管理引用的事件库,比较小,足够日常使用,当然也可以使用moment
yarn add dayjs

创建 file 模块,用于我们编写功能,我们仍然是使用 res 创建几个常用文件

js 复制代码
nest g res file

ps: File 库会跟系统的一些库重名,引用时额外注意位置,有必要的话改个名字(例如: store、diskfile、filestore都行)

如下所示已创建完毕了

我们进入 .file.module 中添加内容, @Moduleimport 我们的数据库 File,同时如下所示,注册 MulterModule 模块,注册后,每次上传文件都会走到下面的 filename 后买你的回调

这是我们下载用的操作,基本都在这里了,设置目录、文件名字等等

js 复制代码
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 也会是空

js 复制代码
上传单个文件
@Post('file')
@UseInterceptors(FileInterceptor('file'))
uploadFiles(
    @UploadedFile() file: Express.Multer.File,
) {
    console.log(files);
}

//上传多个文件
@Post('file')
@UseInterceptors(FilesInterceptor('file'))
uploadFiles(
    @UploadedFiles() files: Array<Express.Multer.File>,
) {
    console.log(files);
}

//上传带其他参数的文件
@Post('file')
@UseInterceptors(AnyFilesInterceptor(file))
uploadFile(
    @Body() objDto: ObjDto,
    @UploadedFiles() files: Array<Express.Multer.File>,
) {
    console.log(files);
    console.log(objDto)
}

实际使用的最多的是单个文件上传,其他的都可以被代替(大不了调用多个接口是吧)

访问配置

到这里还没结束,file 中保存的 path 等,默认应当拼接我们的路由前缀就可以访问才对,这里实际上不能直接访问,还需要我们配置,也就是授权的目录才可以直接访问

在我们的 main 中添加目录,这里是设置 public 可以随意访问,从下面可以看出与我们的项目并列,注意部署多个项目时别被其他项目的窜到一个文件夹了

js 复制代码
import * as express from 'express';

app.use('/public', express.static(`${__dirname}/../public`));

文件与数据库问答

疑问: 有了文件为什么还要创建数据库表格存放到里面

回答: 文件存放后,我们要保存它的路径,后续才能直接访问到他,否则,他就成了无主之物一般,我们无法知道他的位置,也无法访问了,另外我们的数据库除了保存文件的基础信息,我们还会被关联到相应的人,一个人可以发布多张图,一张图可以被多个人点赞、收藏、设置使用等等

数据库存放文件

不多介绍了,保存内容,方便使用,顺便引出文档

js 复制代码
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)
  }

文档

上面只介绍了怎么用,但是却没有配置文档,下面配置一下参数文档(返回的前面有介绍,就不介绍了)

js 复制代码
@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,在里面设置一下

js 复制代码
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',
            },
        },
    },
})

简化后的装饰器配置

js 复制代码
@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 为例, 其他的端的文档也有,差不太多,根据自己需要配置即可

js 复制代码
//安装minio
brew install minio/stable/minio

创建目录,并运行

bash 复制代码
//创建~/minio文件夹
mkdir ~/minio
//启动minio服务,并设置保存目录为 ~/minio
minio server ~/minio --console-address :9090

这样就启动成功了,我们可以看到用户名密码,也可以看到地址了,我们直接进入即可

后面我们保存的基本上就在这个 minio 文件夹了,后面是我创建的 bucket 仓库

我们复制上面的地址,然后使用给定的密码进入即可

js 复制代码
http://127.0.0.1:9090

然后设置 bucketsaccessKey 配置, object browser 查看管理上传文件(自己简单摸索下,这三个看了可以直接用)

地址和端口号就不多说了吧 127.0.0.1:9000:前后就是,两个 key 记录下来,后面有用(地址如果是另一个服务器部署的,使用另一个服务器的地址,没有端口号就不填写即可)

项目配置

导入minio

js 复制代码
yarn add minio

下面我们唯一需要做的就是看 minio-js文档,这时候可以直接调用里面的api了

我们直接创建 minio 客户端,然后设置我们的信息

js 复制代码
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 内容,第二个

js 复制代码
//直接上传接收到的 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 信息,只会有文件路径相关信息

js 复制代码
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

js 复制代码
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 也不是不行

这篇就讲这么多了,希望大家有所收获

相关推荐
前端小王hs10 天前
Nest通用工具函数执行顺序
javascript·后端·nestjs
明远湖之鱼12 天前
从入门到入门学习NestJS
前端·后端·nestjs
吃葡萄不吐番茄皮14 天前
从零开始学 NestJS(一):为什么要学习 Nest
前端·nestjs
东方小月14 天前
Vue3+NestJS实现权限管理系统(六):接口按钮权限控制
前端·后端·nestjs
白雾茫茫丶17 天前
Nest.js 实战 (十四):如何获取客户端真实 IP
nginx·nestjs
Spirited_Away1 个月前
Nest世界中的AOP
前端·node.js·nestjs
Eric_见嘉1 个月前
NestJS 🧑‍🍳 厨子必修课(六):Prisma 集成(下)
前端·后端·nestjs
kongxx2 个月前
NestJS中使用Guard实现路由保护
nestjs
白雾茫茫丶2 个月前
Nest.js 实战 (十二):优雅地使用事件发布/订阅模块 Event Emitter
nestjs·nest.js·发布订阅·event emitter
lph65823 个月前
比起上传资源更应该懂得如何资源回收
node.js·nestjs