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 也不是不行

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

相关推荐
XiaoYu200217 小时前
第2章 Nest.js入门
前端·ai编程·nestjs
实习生小黄1 天前
NestJS 调试方案
后端·nestjs
当时只道寻常5 天前
NestJS 如何配置环境变量
nestjs
濮水大叔16 天前
VonaJS是如何做到文件级别精确HMR(热更新)的?
typescript·node.js·nestjs
ovensi16 天前
告别笨重的 ELK,拥抱轻量级 PLG:NestJS 日志监控实战指南
nestjs
ovensi17 天前
Docker+NestJS+ELK:从零搭建全链路日志监控系统
后端·nestjs
Gogo81619 天前
nestjs 的项目启动
nestjs
没头发的卓卓1 个月前
新手入门:nest基本使用规则(适合零基础小白)
nestjs
孟祥_成都1 个月前
深入 Nestjs 底层概念(1):依赖注入和面向切面编程 AOP
前端·node.js·nestjs