本文详细介绍了如何在商城项目中使用 MinIO 和 Nest.js 实现文件存储功能。文章分为四个主要部分:
- 使用 Docker 部署 MinIO 服务
- MinIO 的基础配置(创建 bucket 和 AccessKey)
- 配置 MinIO 访问策略,实现文件的公开访问
- 在 Nest.js 中集成 MinIO,实现文件上传、下载、删除等功能
通过本文的实践,你将学会如何搭建一个完整的文件存储服务。
本文的完整项目代码在ibuy-admin-backend这个仓库中
1. 使用docker安装Minio
1.1. 拉取镜像
docker pull minio/minio
1.2. 创建数据卷并提升权限
mkdir -p /usr/local/minio/data /usr/local/minio/config
chmod -R 777 /usr/local/minio/data
chmod -R 777 /usr/local/minio/config
1.3. 使用minio/minio镜像启动容器
docker run \
--name minio \
-p 9000:9000 \
-p 9090:9090 \
-d \
-e "MINIO_ROOT_USER=minio" \
-e "MINIO_ROOT_PASSWORD=minio123" \
-v /usr/local/minio/data:/data \
-v /usr/local/minio/config:/root/.minio \
minio/minio server /data --console-address ":9090" --address ":9000"
- docker run:这是Docker命令行工具用来运行一个新容器的命令。
- --name minio:这个参数为容器指定了一个名称,这里名称被设置为minio。使用名称可以更方便地管理容器。
- -p 9000:9000:这个参数将容器内的9000端口映射到宿主机的9000端口。MinIO服务默认使用9000端口提供API服务。
- -p 9090:9090:这个参数将容器内的9090端口映射到宿主机的9090端口。这是MinIO的控制台(Console)端口,用于访问MinIO的图形用户界面。
- -d:这个参数告诉Docker以“detached”模式运行容器,即在后台运行。
- -e "MINIO_ROOT_USER=minio":设置环境变量MINIO_ROOT_USER,这是访问MinIO服务的用户名称,这里设置为minio。
- -e "MINIO_ROOT_PASSWORD=minio123":设置环境变量MINIO_ROOT_PASSWORD,这是访问MinIO服务的用户密码,这里设置为minio123。
- -v /usr/local/minio/data:/data:这个参数将宿主机的目录/usr/local/minio/data挂载到容器的/data目录。MinIO会将所有数据存储在这个目录。
- -v /usr/local/minio/config:/root/.minio:这个参数将宿主机的目录/usr/local/minio-config挂载到容器的/root/.minio目录。这个目录用于存储MinIO的配置文件和数据。
- minio/minio:这是要运行的Docker镜像的名称,这里使用的是官方发布的MinIO镜像。
- server /data:这是传递给MinIO程序的命令行参数,告诉MinIO以服务器模式运行,并且使用/data目录作为其数据存储位置。
- --console-address ":9090":这个参数指定MinIO控制台服务的监听地址和端口。在这个例子中,它设置为监听所有接口上的9090端口。
- --address ":9000":这个参数指定MinIO API服务的监听地址和端口。在这个例子中,它设置为监听所有接口上的9000端口。
然后就可以访问 http://localhost:9090 查看控制台
在使用127.0.0.1:9090时无法访问,应该是没有设置跨域。
2. 创建bucket和AccessKey
这个直接在很简单,直接在访问 http://localhost:9090在控制台操作即可。
注意,这里生成的AccessKey后续在nestjs中接入minio 的api时会用到,所以需要记录下来
3. 配置策略
经过上面的安装和配置,我们已经可以正常的访问minio了。并且我们创建了一个名为mall的bucket,用于存储商品相关的图片对象。不过,minio的资源访问,默认是需要通过AccessKey或者账号密码来访问的。但是我们是一个b2c的商城,在后台管理系统重上传的图需要能在公网访问。
但是默认的策略,公网无法访问。我们需要自己配置策略
3.1. 进入bucket 配置 策略
修改策略选择为Custom
然后配置如下
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::<account_id>:user/<user_name>"
]
},
"Action": [
"s3:PutObject",
"s3:AbortMultipartUpload",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::mall/*"
]
},
{
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::<account_id>:user/<user_name>"
]
},
"Action": [
"s3:ListBucket",
"s3:ListBucketMultipartUploads"
],
"Resource": [
"arn:aws:s3:::mall"
]
},
{
"Effect": "Allow",
"Principal": {
"AWS": [
"*"
]
},
"Action": [
"s3:GetBucketLocation",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::mall"
]
},
{
"Effect": "Allow",
"Principal": {
"AWS": [
"*"
]
},
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::mall/*"
]
}
]
}
3.2. 策略解析
这个策略一共有4个声明,主要做了以下事情
3.2.1.1. 1. 第一个声明:
{
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::<account_id>:user/<user_name>"
]
},
"Action": [
"s3:PutObject",
"s3:AbortMultipartUpload",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::mall/*"
]
}
-
Effect:
Allow— 允许执行指定的操作。 -
Principal:
arn:aws:iam::<account_id>:user/<user_name>— 指定这个策略适用的 IAM 用户。<account_id>和<user_name>需要替换为实际的 AWS 账户 ID 和用户名。 -
Action: 允许的操作:
s3:PutObject— 允许将对象(如文件)上传到桶中。s3:AbortMultipartUpload— 允许中止一个多部分上传操作。s3:DeleteObject— 允许删除桶中的对象。
-
Resource:
arn:aws:s3:::mall/*— 这个权限作用于mall存储桶中的所有对象(即桶中的所有文件)。
总结:该声明允许指定的 IAM 用户上传文件、删除文件,以及中止未完成的多部分上传操作,作用于 mall 存储桶中的所有文件。
2. 第二个声明:
{
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::<account_id>:user/<user_name>"
]
},
"Action": [
"s3:ListBucket",
"s3:ListBucketMultipartUploads"
],
"Resource": [
"arn:aws:s3:::mall"
]
}
-
Effect:
Allow— 允许执行指定的操作。 -
Principal:
arn:aws:iam::<account_id>:user/<user_name>— 同样是指定 IAM 用户。 -
Action: 允许的操作:
s3:ListBucket— 允许列出存储桶中的对象。s3:ListBucketMultipartUploads— 允许列出正在进行的多部分上传任务。
-
Resource:
arn:aws:s3:::mall— 这些操作只作用于mall存储桶本身,而不是桶中的具体文件。
总结:该声明允许指定的 IAM 用户列出 mall 存储桶中的文件,或者查看当前正在进行的多部分上传任务。
3. 第三个声明:
{
"Effect": "Allow",
"Principal": {
"AWS": [
"*"
]
},
"Action": [
"s3:GetBucketLocation",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::mall"
]
}
-
Effect:
Allow— 允许执行指定的操作。 -
Principal:
"*"— 这个策略适用于所有主体,即公开访问。 -
Action: 允许的操作:
s3:GetBucketLocation— 允许获取存储桶的位置。s3:ListBucket— 允许列出存储桶中的对象。
-
Resource:
arn:aws:s3:::mall— 这些操作作用于mall存储桶本身。
总结:该声明允许所有人(公共访问)获取 mall 存储桶的位置和列出桶中的文件列表(列出桶中文件的元数据)。
4. 第四个声明:
{
"Effect": "Allow",
"Principal": {
"AWS": [
"*"
]
},
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::mall/*"
]
}
- Effect:
Allow— 允许执行指定的操作。 - Principal:
"*"— 这个策略适用于所有主体,即公开访问。 - Action: 允许的操作
s3:GetObject— 允许获取存储桶中的对象。
- Resource:
arn:aws:s3:::mall/*— 这个权限作用于mall存储桶中的所有对象。
总结:该声明允许任何人公开访问 mall 存储桶中的所有对象(如图片、文件等)。
3.3. 配置匿名访问
进入 Anonymous 点击右上角的Add Access Rule 为mall添加一个只读匿名策略,无需验证身份。这样就可以通过 http://loclahost:9000/mall/file.png 来访问资源了
4. 使用nestjs接入minio
在这个File模块中我们需要提供
- 文件上传
- 下载
- 删除
- 获取目录结构
等功能。
4.1. 配置说明
需要在环境变量中配置以下MinIO相关参数:
MINIO_HOST=your-minio-host
MINIO_PORT=your-minio-port
MINIO_ACCESS_KEY=your-access-key
MINIO_SECRET_KEY=your-secret-key
4.2. 实现细节
4.2.1. 1. 安装依赖
npm install @nestjs/common @nestjs/config minio
4.2.2. 2. 模块结构
mall-service-file/
├── file.module.ts # 模块定义
├── file.service.ts # 服务实现
├── file.controller.ts # 控制器
4.2.3. 3. 核心代码实现
4.2.3.1. FileModule (file.module.ts)
import { Module } from '@nestjs/common';
import { FileService } from './file.service';
import { FileController } from './file.controller';
@Module({
providers: [FileService],
controllers: [FileController],
exports: [FileService],
})
export class FileModule {}
说明
- 导出 FileService 使其可以被其他模块使用
- 注册 FileController 处理文件相关的 HTTP 请求
4.2.3.2. FileService (file.service.ts)
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as Minio from 'minio';
import Result from '../../common/utils/Result';
@Injectable()
export class FileService {
private readonly minioClient: Minio.Client;
constructor(private readonly configService: ConfigService) {
this.minioClient = new Minio.Client({
endPoint: this.configService.get('MINIO_HOST'),
port: parseInt(this.configService.get('MINIO_PORT')),
useSSL: false,
accessKey: this.configService.get('MINIO_ACCESS_KEY'),
secretKey: this.configService.get('MINIO_SECRET_KEY'),
});
}
/**
* 上传文件
* @param bucketName 分组名
* @param objectName 资源名 eg. abc.jpg
* @param data 具体资源
* @param path 路径名
* @returns
*/
async uploadFile(
bucketName: string,
objectName: string,
data: Buffer,
path: string = '/',
) {
await this.minioClient.putObject(bucketName, `${path}/${objectName}`, data);
const HOST = this.configService.get('MINIO_HOST');
const PORT = parseInt(this.configService.get('MINIO_PORT'));
return new Result({
bucketName,
path,
objectName,
imgUrl: `http://${HOST}:${PORT}/${bucketName}/${path}/${objectName}`,
});
}
async readFileStream(
bucketName: string,
objectName: string,
path: string = '/',
): Promise<Result<any>> {
try {
// 获取 Minio 文件的可读流
const fileStream = await this.minioClient.getObject(
bucketName,
`${path}/${objectName}`,
);
// 返回一个 Promise,读取流的内容到 Buffer 中
const data = await new Promise<Buffer>((resolve, reject) => {
const buffers: Buffer[] = [];
fileStream.on('data', (chunk) => {
buffers.push(chunk); // 收集数据块
});
fileStream.on('error', (err) => {
reject(err); // 错误处理
});
fileStream.on('end', () => {
resolve(Buffer.concat(buffers)); // 合并数据块并返回
});
});
return new Result(data);
} catch (error) {
return new Result(null, '文件读取失败');
}
}
async deleteFile(
bucketName: string,
objectName: string,
path: string = '/',
): Promise<Result<null>> {
await this.minioClient.removeObject(bucketName, `${path}/${objectName}`);
return new Result(null, '删除成功');
}
/**
* 获取目录列表
* @param bucketName 分组名
* @returns director list
*/
async getDirectoryStructure(
bucketName: string,
): Promise<Result<{ data: string[] }>> {
const result = await this.listObjects(bucketName);
const directories = new Set<string>();
result.data.forEach((object) => {
const pathSegments = object.name.split('/');
if (pathSegments.length > 1) {
const directory = pathSegments.slice(0, -1).join('/');
directories.add(directory);
}
});
const data = Array.from(directories);
return new Result({ data });
}
private async listObjects(
bucketName: string,
): Promise<Result<Minio.BucketItem[]>> {
const data = await new Promise<Minio.BucketItem[]>((resolve, reject) => {
const objects: Minio.BucketItem[] = [];
const stream = this.minioClient.listObjects(bucketName, '', true);
stream.on('data', (obj) => {
objects.push(obj);
});
stream.on('error', (err) => {
reject(err);
});
stream.on('end', () => {
resolve(objects);
});
});
return new Result(data);
}
}
关键实现说明:
- MinIO 客户端初始化
- 通过 ConfigService 读取环境配置
- 创建 MinIO 客户端实例,支持 SSL 配置
- uploadFile 方法
- 入参:存储桶名称、文件名、文件数据、存储路径
- 返回:包含文件访问URL的对象
- 实现:使用 putObject 上传文件,生成访问URL
- readFileStream 方法
- 入参:存储桶名称、文件名、文件路径
- 返回:文件数据 Buffer
- 实现:使用 getObject 获取文件流并转换为 Buffer
- deleteFile 方法
- 入参:存储桶名称、文件名、文件路径
- 返回:null
- 实现:使用 removeObject 删除文件
- getDirectoryStructure 方法
- 入参:存储桶名称
- 返回:目录名称数组
- 实现:获取所有对象并解析路径结构
4.2.3.3. FileController (file.controller.ts)
import {
Controller,
Post,
Get,
Delete,
Query,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { FileService } from './file.service';
@Controller('file')
export class FileController {
constructor(private readonly fileService: FileService) {}
@Post('/upload')
@UseInterceptors(FileInterceptor('file'))
async uploadFile(@UploadedFile() file: any, @Query('path') path: string) {
return await this.fileService.uploadFile(
'mall',
file.originalname,
file.buffer,
path,
);
}
@Get('/download')
async readFileStream(@Query() query: any) {
return await this.fileService.readFileStream(
query.bucketName,
query.objectName,
query.path,
);
}
@Delete('/delete')
async deleteFile(@Query() query: any) {
return await this.fileService.deleteFile(
query.bucketName,
query.objectName,
query.path,
);
}
@Get('/list')
async getDirectoryStructure() {
return await this.fileService.getDirectoryStructure('mall');
}
}
关键实现说明:
- 文件上传接口
- 使用 FileInterceptor 处理文件上传
- 支持通过 query 参数指定存储路径
- 自动处理文件名和文件数据
- 文件下载接口
- 通过 query 参数接收文件信息
- 返回文件数据流
- 文件删除接口
- 通过 query 参数接收文件信息
- 返回删除操作结果
- 目录列表接口
- 无需参数
- 返回默认存储桶的目录结构
4.3. TODO
- 确保MinIO服务器已正确配置并运行
- 上传文件大小可能受到限制,后续需要在配置中适当调整
- 需要对文件类型进行限制和验证
- 暂未实现错误处理和重试机制
- 暂未配置MinIO的SSL访问