前言
在之前的文章中我们通过群晖Docker搭建了私有的MinIO文件服务,在参考中已经放置了链接,这里我们利用Nestjs调用MinIO的API实现在MinIO文件的上传、下载、删除功能。
新建Minio模块
假设我们已经新建好了Nestjs项目,只需要新建一个Minio模块即可,使用命令nest g resource modules/minioClient --no-spec
快速新建一个模块,并且会将该模块自动导入到App.module中,可以选择不生成curd接口,执行后生成如下三个文件。
设置基础配置信息
在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('上传成功');
});
});
}
删除接口
在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);
}
});
获取文件列表接口
在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);
}
下载接口
在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);
}
总结
通过以上的内容,就可以实现使用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);
});
});
}
评论区