基于上一遍讲了如何利用nestjs 搭建一个restful风格的后端,现在接着讲如何结合minio实现文件的上传和下载。
文中涉及到的minio 知识点参考https://blog.csdn.net/zw52yany/article/details/101217708这遍文章
完整代码请看 https://github.com/zw-slime/book-be
user和picture是一对多的关系,利用TypeORM的ManyToOne JoinColumn 建立两个变直接的映射关系
@Entity()
export class PictureEntity {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(type => UserEntity, user => user.pictures)
@JoinColumn()
owner: UserEntity;
@Column({
nullable: false,
})
bucketName: string;
@Column({
nullable: false,
unique: true,
})
fileName: string;
@Column({
nullable: false,
})
fileType: string;
@Column({
default: '',
})
originName: string;
@Column({
default: false,
})
isPublic: boolean;
}
@Entity({ name: 'user' })
export class UserEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({
type: 'varchar',
length: 255,
unique: true,
})
name: string;
@Column({
type: 'varchar',
length: 255,
})
password: string;
@Column({
type: 'varchar',
length: 255,
})
email: string;
@Column({
type: 'varchar',
length: 255,
})
salt: string;
@Column({
name: 'create_at',
type: 'timestamp',
onUpdate: 'CURRENT_TIMESTAMP',
})
createAt: Date;
@OneToMany(type => PictureEntity, picture => picture.owner)
pictures: PictureEntity[];
}
(1)FileInterceptor绑定上传的文件字段,然后通过@UploadedFile() file 可以获得file
(2)利用hash 生成文件名,在利用minio提供的sdk putObject 上传到minio服务器上
(3)最后利用typeOrm提供的方法上传到数据库
controller的代码如下:
@Post('upload-file')
@UseInterceptors(FileInterceptor('file'))
async uploadMinio(@UploadedFile() file, @Req() req) {
const temp = req.header('authorization').split(' ');
const userId = (this.authService.decode(temp[temp.length - 1]) as any)
.userId;
const fileHash = hash({
name: file.originalname,
time: new Date().getTime(),
});
await this.minioService.uploadFile(fileHash, file.buffer, file.mimetype);
await this.pictureSerive.addPicture({
userId,
fileName: fileHash,
fileType: file.mimetype,
originName: file.originalname,
});
return { fileName: fileHash };
}
minioService 上传文件的代码如下: 先判断bucker存不存在,不存在就新建
async uploadFile(fileName, file: Buffer, type) {
this.initial();
if (!this.minioClient.bucketExists(BucketName)) {
await this.createBucket();
}
const metaData = {
'Content-Type': type,
};
try {
return await this.minioClient.putObject(
BucketName,
fileName,
file,
1024,
metaData,
);
} catch (e) {
throw new BadRequestException(e.message);
}
}
pictureService的保存文件到数据库的代码如下:这里存的是一条记录,并不是真实的文件,而是文件对应的minio object name,同时通过owner关联user 表
async addPicture(params: AddPictureParams) {
try {
return await this.pictureRepository.save({
owner: {
id: params.userId,
},
fileName: params.fileName,
bucketName: BucketName,
fileType: params.fileType,
originName: params.originName,
});
} catch (e) {
throw new BadRequestException(e.message);
}
}
(1)先判断用户对图片有没有权限,有权限就返回获得的数据,没权限就抛异常
(2)再通过minio sdk提供的方法获得文件流
(3)写入文件流,写入完成response返回下载文件流
@Get('/:id')
async get(@Param('id') id: string, @Res() res: Response, @Req() req) {
// 查询pic数据库判断用户对图片有没有权限
const pic = await this.pictureSerive.canReadPicture(
id,
req.header('authorization'),
);
// 获取文件后缀名
let ext = '';
if (pic[0].originName.indexOf('.') > -1) {
ext = pic[0].originName.split('.')[
pic[0].originName.split('.').length - 1
];
}
// 返回文件下载流
const readerStream = await this.minioService.getObject(id);
readerStream.on('data', chunk => {
res.write(chunk, 'binary');
});
res.set({
'Content-Type': 'application/octet-stream',
'Content-Disposition': 'attachment; filename=' + id + '.' + ext,
});
readerStream.on('end', () => {
res.end();
});
readerStream.on('error', err => {
console.log(err.stack);
throw new BadRequestException(err.stack);
});
}
async getObject(fileName) {
this.initial();
try {
return await this.minioClient.getObject(BucketName, fileName);
} catch (e) {
throw new BadRequestException(e.message);
}
}
这里涉及到跨表的关联查询,实现方法: find({relations: [‘实体里面定义的关联字段’]}),具体代码如下
// 查询pic数据库判断用户对图片有没有权限
canReadPicture(fileName: string, authorization: string) {
return new Promise(async (resolve, reject) => {
const temp = authorization.split(' ');
const userId = (this.authService.decode(temp[temp.length - 1]) as any)
.userId;
const pic = await this.findPictureByFileName(fileName);
if (!pic || pic.length <= 0) {
return reject(new BadRequestException('该文件不存在'));
}
if (pic[0].owner.id !== userId && !pic[0].isPublic) {
return reject(new NOPicturePermissionException());
}
return resolve(pic);
});
}
async findPictureByFileName(fileName: string) {
try {
return await this.pictureRepository.find({
where: { fileName },
relations: ['owner'],
});
} catch (e) {
throw new BadRequestException(e.message);
}
}
最后测试得到的 http://localhost:3000/picture/d6a5f9e569f2875bee9322f10a0d21eb0421d760 会下载文件
(1)同上判断对文件有木有权限
(2)删除数据库记录
(3)删除minio文件
@Post('delete')
async delete(@Body() param: DeletePictureValidator, @Req() req) {
// 查询pic数据库判断用户对图片有没有权限
const pic = await this.pictureSerive.canReadPicture(
param.fileName,
req.header('authorization'),
);
await this.pictureSerive.deletePictureByFileName(param.fileName);
await this.minioService.deleteObject(param.fileName);
return { fileName: param.fileName };
}
async deletePictureByFileName(fileName: string) {
try {
return await this.pictureRepository.delete({ fileName });
} catch (e) {
throw new BadRequestException(e.message);
}
}
async deleteObject(fileName) {
this.initial();
try {
return await this.minioClient.removeObject(BucketName, fileName);
} catch (e) {
throw new BadRequestException(e.message);
}
}
@Get('list')
async findAllPicture(@Req() req) {
return await this.pictureSerive.findMyPictureList(
req.header('authorization'),
);
}
// 根据userId查询picture list
async findMyPictureList(authorization: string) {
const temp = authorization.split(' ');
const userId = (this.authService.decode(temp[temp.length - 1]) as any)
.userId;
try {
return await this.pictureRepository.find({ owner: { id: userId } });
} catch (e) {
throw new BadRequestException(e.message);
}
}