侧边栏壁纸
博主头像
恪晨博主等级

前端程序员

  • 累计撰写 143 篇文章
  • 累计创建 41 个标签
  • 累计收到 17 条评论

目 录CONTENT

文章目录

Nestjs+Minio实现文件上传、删除、下载、获取文件列表

恪晨
2023-03-20 / 0 评论 / 0 点赞 / 130 阅读 / 2,194 字 / 正在检测是否收录...

前言

  在之前的文章中我们通过群晖Docker搭建了私有的MinIO文件服务,在参考中已经放置了链接,这里我们利用Nestjs调用MinIO的API实现在MinIO文件的上传、下载、删除功能。

新建Minio模块

  假设我们已经新建好了Nestjs项目,只需要新建一个Minio模块即可,使用命令nest g resource modules/minioClient --no-spec快速新建一个模块,并且会将该模块自动导入到App.module中,可以选择不生成curd接口,执行后生成如下三个文件。
image-1679466637871

设置基础配置信息

  在src/config文件夹下新建一个config.ts的文件,主要内容如下,其中MINIO_ENDPOINT为MinIO的访问地址,MINIO_PORT为MinIO的API访问端口,MINIO_ACCESSKEY为创建MinIO服务时设置的登陆名,MINIO_SECRETKEY为创建MinIO服务时设置的密码,MINIO_BUCKET为创建的桶,注意该文件不可公开

export const MINIO_CONFIG = {
  MINIO_ENDPOINT: '192.168.2.11',
  MINIO_PORT: 9900,
  MINIO_ACCESSKEY: 'root',
  MINIO_SECRETKEY: '123456',
  MINIO_BUCKET: 'testBucket',
};

安装所需依赖

npm i -S nestjs-minio-client
npm i -D @types/minio

引入所需依赖

在minio-client.module中引入MinioModule,配置链接MinIO的相关服务

import { MINIO_CONFIG } from '@/config/config';
import { Module } from '@nestjs/common';
import { MinioModule } from 'nestjs-minio-client';
import { MinioClientService } from './minio-client.service';
import { MinioClientController } from './minio-client.controller';

@Module({
  imports: [
    MinioModule.register({
      endPoint: MINIO_CONFIG.MINIO_ENDPOINT,
      port: MINIO_CONFIG.MINIO_PORT,
      useSSL: true,
      accessKey: MINIO_CONFIG.MINIO_ACCESSKEY,
      secretKey: MINIO_CONFIG.MINIO_SECRETKEY,
    }),
  ],
  controllers: [MinioClientController],
  providers: [MinioClientService],
})
export class MinioClientModule {}

上传接口

minio-client.controller中新建一个uploadMinio方法,使用@nestjs/swagger中相关的API进行接口描述,因为是上传文件,所以这里需要设置@ApiConsumes('multipart/form-data')以及@UseInterceptors(FileInterceptor('file')),后面会给出完整的代码,这里我们看到使用了minioService调用了upload方法,将文件作为参数进行了传旨

  @Post('uploadFile')
  @UseInterceptors(FileInterceptor('file'))
  @ApiOperation({ summary: '文件上传,返回 url 地址' })
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    schema: {
      type: 'object',
      properties: {
        file: {
          description: '文件',
          type: 'string',
          format: 'binary',
        },
      },
    },
  })
  async uploadMinio(@UploadedFile() file: Express.Multer.File) {
    return await this.minioService.upload(file);
  }

minio-client.service中新建upload方法,主要内容如下,this.baseBucket为我们之前定义好的桶名称,通过MinioService获取好minio提供的client,并且我们使用了crypto对文件名进行了md5加密,这样就不会因为重复上传文件导致文件名变化,在上传的时候用promise进行了封装,主要是为了后续如果有其他的内部处理可以在Promise里面进行。

  private readonly baseBucket = MINIO_CONFIG.MINIO_BUCKET;

  constructor(private readonly minio: MinioService) {}

  get client() {
    return this.minio.client;
  }
async upload(
    file: Express.Multer.File,
    baseBucket: string = this.baseBucket,
  ) {
    // 转换文件名中的中文 
    file.originalname = Buffer.from(file.originalname, 'latin1').toString(
      'utf8',
    );
    const temp_fileName = file.originalname;
    // 加密文件名
    const hashedFileName = crypto
      .createHash('md5')
      .update(temp_fileName)
      .digest('hex');
    const ext = file.originalname.substring(
      file.originalname.lastIndexOf('.'),
      file.originalname.length,
    );
    const filename = hashedFileName + ext;

    const fileName = `${filename}`;
    const fileBuffer = file.buffer;

    return new Promise<any>((resolve) => {
      // 调用minio的保存方法
      this.client.putObject(baseBucket, fileName, fileBuffer, async (err) => {
        if (err) {
          throw new HttpException('Error upload file', HttpStatus.BAD_REQUEST);
        }
        // 上传成功回传文件信息
        resolve('上传成功');
      });
    });
  }

image-1679466663280

删除接口

minio-client.controller中新建一个删除方法,内容如下:

@ApiOperation({ summary: '删除文件' })
@Delete('deleteFile/:fileName')
async deleteFile(@Param('fileName') fileName: string) {
    return await this.minioService.deleteFile(fileName);
}

minio-client.service中新建一个删除,内容如下,删除方法需要用到文件名和backet名称,其中文件名是需要后缀名的完整文件名,在删除之前需要判断文件是否存在,不存在则提示不存在,存在着删除,这里也可以用Promise进行封装,根据实际业务场景进行处理。

async deleteFile(objetName: string, baseBucket: string = this.baseBucket) {
    const tmp: any = await this.listAllFilesByBucket();
    const names = tmp?.map((i) => name);
    if (!names.includes(objetName)) {
      throw new HttpException(
        '删除失败,文件不存在',
        HttpStatus.SERVICE_UNAVAILABLE,
      );
    }
    return this.client.removeObject(baseBucket, objetName, async (err) => {
      if (err) {
        throw new HttpException('删除失败,请重试', HttpStatus.BAD_REQUEST);
      }
    });

image-1679466670534

获取文件列表接口

minio-client.controller中新建fileList方法,内容如下,调用了service中的listAllFilesByBucket方法

  @ApiOperation({ summary: '文件列表' })
  @Get('fileList')
  async fileList() {
    return await this.minioClientService.listAllFilesByBucket();
  }

minio-client.service中新增listAllFilesByBucket方法,内容如下,这里我们用到了readData方法,因为minio返回的是文件流,需要进行转换一下才能拿到实际的返回数据,所以需要进行处理一下,具体代码后面会有。

async listAllFilesByBucket() {
    const tmpByBucket = await this.client.listObjectsV2(
    this.baseBucket,
    '',
    true);
    return this.readData(tmpByBucket);
}

image-1679466678699

下载接口

minio-client.controller中新建download方法,内容如下,minio返回的依旧是文件流,需要进行特殊处理将流写到返回信息中,并且需要设置对应的header头信息,正常情况下下载时也要去判断文件是否存在,减少不必要的调用。

  @ApiOperation({ summary: '通过文件流下载指定文件' })
  @Get('download/:fileName')
  async download(@Param('fileName') fileName: string, @Res() res: Response) {
    const readerStream = await this.minioClientService.download(fileName);
    readerStream.on('data', (chunk) => {
      res.write(chunk, 'binary');
    });
    res.set({
      'Content-Type': 'application/octet-stream',
      'Content-Disposition': 'attachment; filename=' + fileName,
    });
    readerStream.on('end', () => {
      res.end();
    });
    readerStream.on('error', () => {
      throw new HttpException(
        '下载失败,请重试',
        HttpStatus.SERVICE_UNAVAILABLE,
      );
    });
  }

minio-client.service中新建download方法,内容如下,只需要调用一下minio的getObject方法即可

async download(fileName) {
    return await this.client.getObject(this.baseBucket, fileName);
}

image-1679466691789

总结

  通过以上的内容,就可以实现使用nestjs操作MinIO上的文件了,当然更多的功能可在此基础上进行扩展。

完整代码

  • minio-client.module.ts
import { MINIO_CONFIG } from '@/config/config';
import { Module } from '@nestjs/common';
import { MinioModule } from 'nestjs-minio-client';
import { MinioClientService } from './minio-client.service';
import { MinioClientController } from './minio-client.controller';

@Module({
  imports: [
    MinioModule.register({
      endPoint: MINIO_CONFIG.MINIO_ENDPOINT,
      port: MINIO_CONFIG.MINIO_PORT,
      useSSL: true,
      accessKey: MINIO_CONFIG.MINIO_ACCESSKEY,
      secretKey: MINIO_CONFIG.MINIO_SECRETKEY,
    }),
  ],
  controllers: [MinioClientController],
  providers: [MinioClientService],
})
export class MinioClientModule {}

  • minio-client.controller.ts
import {
  Controller,
  Delete,
  Get,
  HttpException,
  HttpStatus,
  Param,
  Post,
  Res,
  UploadedFile,
  UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Response } from 'express';
import { MinioClientService } from './minio-client.service';

@ApiTags('minio-client')
@Controller('minio-client')
export class MinioClientController {
  constructor(private readonly minioClientService: MinioClientService) {}

  @Post('uploadFile')
  @UseInterceptors(FileInterceptor('file'))
  @ApiOperation({ summary: '文件上传,返回 url 地址' })
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    schema: {
      type: 'object',
      properties: {
        file: {
          description: '文件',
          type: 'string',
          format: 'binary',
        },
      },
    },
  })
  async uploadMinio(@UploadedFile() file: Express.Multer.File) {
    return await this.minioClientService.upload(file);
  }

  @ApiOperation({ summary: '删除文件' })
  @Delete('deleteFile/:fileName')
  async deleteFile(@Param('fileName') fileName: string) {
    return await this.minioClientService.deleteFile(fileName);
  }

  @ApiOperation({ summary: '文件列表' })
  @Get('fileList')
  async fileList() {
    return await this.minioClientService.listAllFilesByBucket();
  }

  @ApiOperation({ summary: '通过文件流下载指定文件' })
  @Get('download/:fileName')
  async download(@Param('fileName') fileName: string, @Res() res: Response) {
    const readerStream = await this.minioClientService.download(fileName);
    readerStream.on('data', (chunk) => {
      res.write(chunk, 'binary');
    });
    res.set({
      'Content-Type': 'application/octet-stream',
      'Content-Disposition': 'attachment; filename=' + fileName,
    });
    readerStream.on('end', () => {
      res.end();
    });
    readerStream.on('error', () => {
      throw new HttpException(
        '下载失败,请重试',
        HttpStatus.SERVICE_UNAVAILABLE,
      );
    });
  }
}
  • minio-client.service.ts
import { MINIO_CONFIG } from '@/config/config';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import * as crypto from 'crypto';
import { MinioService } from 'nestjs-minio-client';

@Injectable()
export class MinioClientService {
  private readonly baseBucket = MINIO_CONFIG.MINIO_BUCKET;

  constructor(private readonly minio: MinioService) {}

  get client() {
    return this.minio.client;
  }

  async upload(
    file: Express.Multer.File,
    baseBucket: string = this.baseBucket,
  ) {
    file.originalname = Buffer.from(file.originalname, 'latin1').toString(
      'utf8',
    );
    const temp_fileName = file.originalname;
    const hashedFileName = crypto
      .createHash('md5')
      .update(temp_fileName)
      .digest('hex');
    const ext = file.originalname.substring(
      file.originalname.lastIndexOf('.'),
      file.originalname.length,
    );
    const filename = hashedFileName + ext;

    const fileName = `${filename}`;
    const fileBuffer = file.buffer;

    return new Promise<any>((resolve) => {
      this.client.putObject(baseBucket, fileName, fileBuffer, async (err) => {
        if (err) {
          throw new HttpException('Error upload file', HttpStatus.BAD_REQUEST);
        }
        // 上传成功回传文件信息
        resolve('上传成功');
      });
    });
  }

  async listAllFilesByBucket() {
    const tmpByBucket = await this.client.listObjectsV2(
      this.baseBucket,
      '',
      true,
    );
    return this.readData(tmpByBucket);
  }

  async deleteFile(objetName: string, baseBucket: string = this.baseBucket) {
    const tmp: any = await this.listAllFilesByBucket();
    const names = tmp?.map((i) => i.name);
    if (!names.includes(objetName)) {
      throw new HttpException(
        '删除失败,文件不存在',
        HttpStatus.SERVICE_UNAVAILABLE,
      );
    }
    return this.client.removeObject(baseBucket, objetName, async (err) => {
      if (err) {
        throw new HttpException('删除失败,请重试', HttpStatus.BAD_REQUEST);
      }
    });
  }

  async download(fileName) {
    return await this.client.getObject(this.baseBucket, fileName);
  }

  readData = async (stream) =>
    new Promise((resolve, reject) => {
      const a = [];
      stream
        .on('data', function (row) {
          a.push(row);
        })
        .on('end', function () {
          resolve(a);
        })
        .on('error', function (error) {
          reject(error);
        });
    });
}

参考

https://www.npmjs.com/package/nestjs-minio-client

https://blog.wangboweb.site/archives/603

0

评论区