项目概述
文件存储管理系统是一个功能完整的文件上传、下载、管理和预览解决方案,采用现代化的微服务架构设计,支持文件夹管理、预签名上传、批量操作等功能。
核心特性
- 预签名上传: 支持AWS S3预签名URL,直接上传到MinIO
- 文件夹管理: 完整的文件夹创建、重命名、删除和浏览功能
- 文件预览: 支持图片、PDF、文本等多种格式预览
- 批量操作: 批量上传、下载、删除和移动文件
- 权限控制: JWT认证和细粒度的访问控制
- 存储统计: 实时存储使用情况和配额监控
基础信息
基础URL
http://www.fox360.cn:3000 // 后端API服务器
http://www.fox360.cn:9002 // MinIO代理(前端可访问)
http://www.fox360.cn:9002 // MinIO代理(前端可访问)
环境架构
| 组件 | 地址 | 说明 |
|---|---|---|
| 前端应用 | 192.168.39.152:5173 | React + Vite 应用 |
| 后端API | 192.168.39.152:3000 | NestJS + TypeORM |
| Nginx代理 | www.fox360.cn:9002 | MinIO集群代理 |
| MinIO集群 | www.fox360.cn:9000/9001 | HTTPS对象存储 |
认证方式
所有API请求需要在Header中携带JWT Token:
Authorization: Bearer <access_token>
Token获取
POST
/api/auth/login
请求体
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| username | string | 是 | 用户名 |
| password | string | 是 | 密码 |
响应示例
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "user-uuid",
"username": "testuser",
"email": "test@example.com"
}
}
文件夹管理接口 New
✅ 新增完整的文件夹管理功能,支持文件夹内上传文件
创建文件夹
POST
/api/folders
请求体
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| name | string | 是 | 文件夹名称 |
| parentId | string | null | 否 | 父文件夹ID,null表示根目录 |
| description | string | 否 | 描述信息 |
| metadata | object | 否 | 自定义元数据 |
响应示例
{
"filename": "新建文件夹",
"originalName": "新建文件夹",
"mimetype": "application/folder",
"size": 0,
"status": "completed",
"parentId": null,
"id": "ea887dc5-7643-41e7-9b8c-046ef0a6506b",
"createdAt": "2026-01-18T17:36:11.211Z",
"updatedAt": "2026-01-18T17:36:11.211Z"
}
获取文件夹内容
GET
/api/folders/{folderId}/files
查询参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | number | 否 | 页码,默认1 |
| limit | number | 否 | 每页数量,默认20 |
| sort | string | 否 | 排序字段,默认updatedAt |
| order | string | 否 | 排序方向,默认desc |
获取文件夹树
GET
/api/folders/tree
重命名文件夹
PUT
/api/folders/{folderId}
请求体
{
"name": "新文件夹名"
}
删除文件夹
DELETE
/api/folders/{folderId}
文件上传接口
支持预签名URL上传和文件夹内上传,前端直接上传到MinIO
生成预签名URL(支持文件夹)
POST
/api/presigned/public/generate-upload-url
请求体
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| filename | string | 是 | 文件名 |
| contentType | string | 是 | 文件类型 |
| size | number | 否 | 文件大小(字节) |
| expiresIn | number | 否 | URL过期时间(秒),默认3600 |
| parentId | string | 否 | 父文件夹ID(用于文件夹内上传) |
| prefix | string | 否 | 存储路径前缀 |
响应示例
{
"success": true,
"data": {
"url": "http://www.fox360.cn:9002/smart-upload/uploads/...",
"key": "uploads/user-uuid/folders/folder-id/2026/01/19/.../filename.txt",
"uploadId": "upload-1768758781612",
"expiresIn": 3600,
"filename": "filename.txt",
"contentType": "text/plain",
"parentId": "folder-id", // ✅ 返回父文件夹ID
"expiresAt": "2026-01-19T10:30:00.000Z"
}
}
文件夹内上传URL(专用接口)
POST
/api/presigned/folders/{folderId}/upload-url
路径参数
| 参数名 | 说明 |
|---|---|
| folderId | 文件夹ID |
请求体
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| filename | string | 是 | 文件名 |
| contentType | string | 否 | 文件类型,默认application/octet-stream |
| expiresIn | number | 否 | URL过期时间,默认3600秒 |
完成上传记录
POST
/api/presigned/public/complete-upload
请求体
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| fileKey | string | 是 | 文件在MinIO中的key |
| filename | string | 是 | 原始文件名 |
| mimetype | string | 是 | 文件MIME类型 |
| size | number | 是 | 文件大小(字节) |
| parentId | string | 否 | 父文件夹ID(用于文件夹内上传) |
| hash | string | 否 | 文件哈希值(用于秒传) |
| etag | string | 否 | MinIO返回的ETag |
| metadata | object | 否 | 自定义元数据 |
文件下载接口
下载单个文件
GET
/api/files/{fileId}/download
响应示例
{
"url": "http://www.fox360.cn:9002/smart-upload/...",
"cached": false
}
批量下载
POST
/api/files/batch/download
请求体
{
"fileIds": [
"file-id-1",
"file-id-2",
"file-id-3"
]
}
文件管理接口
获取文件列表
GET
/api/files
查询参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | number | 否 | 页码,默认1 |
| limit | number | 否 | 每页数量,默认20 |
| parentId | string | null | 否 | 父文件夹ID,null表示根目录 |
| sort | string | 否 | 排序字段,默认updatedAt |
| order | string | 否 | 排序方向,默认desc |
响应示例
{
"data": [
{
"id": "file-id",
"filename": "document.pdf",
"originalName": "document.pdf",
"size": 1024000,
"mimetype": "application/pdf",
"isFolder": false,
"parentId": "folder-id", // 父文件夹ID
"url": "http://...",
"createdAt": "2026-01-18T17:36:11.211Z",
"updatedAt": "2026-01-18T17:36:11.211Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 95,
"totalPages": 5,
"hasNext": true,
"hasPrev": false
}
}
获取文件信息
GET
/api/files/{fileId}
重命名文件
PUT
/api/files/{fileId}
请求体
{
"filename": "新文件名.txt"
}
删除文件
DELETE
/api/files/{fileId}
移动文件/文件夹
PUT
/api/files/{itemId}/move
查询参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| parentId | string | null | 是 | 目标文件夹ID,null表示根目录 |
统计信息接口
存储统计
GET
/api/statistics/storage
文件类型分布
GET
/api/statistics/storage/type-distribution
存储配额
GET
/api/statistics/storage/quota
最近活动
GET
/api/statistics/storage/recent-activity
查询参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | number | 否 | 页码,默认1 |
| pageSize | number | 否 | 每页大小,默认5 |
批量操作接口
批量生成上传URL
POST
/api/presigned/batch-generate
请求体
{
"files": [
{
"filename": "file1.txt",
"contentType": "text/plain"
},
{
"filename": "file2.jpg",
"contentType": "image/jpeg"
}
],
"expiresIn": 3600,
"parentId": "folder-id" // 可选:指定父文件夹
}
批量文件夹内上传URL
POST
/api/presigned/folders/{folderId}/batch-generate
批量更新文件
PUT
/api/files/batch/update
批量删除
POST
/api/files/batch/delete
错误码说明
| 状态码 | 说明 | 常见原因 |
|---|---|---|
| 200 | 请求成功 | 正常响应 |
| 201 | 创建成功 | 资源创建成功 |
| 400 | 请求参数错误 | 缺少必要参数、参数格式错误 |
| 401 | 未授权 | Token无效、过期或未提供 |
| 403 | 禁止访问 | 权限不足、无权访问该资源 |
| 404 | 资源不存在 | 文件、文件夹不存在 |
| 413 | 请求实体过大 | 上传文件超过大小限制 |
| 415 | 不支持的媒体类型 | 文件类型不被支持 |
| 500 | 服务器内部错误 | 后端服务异常 |
| 502 | 网关错误 | MinIO服务不可达 |
| 503 | 服务不可用 | 服务维护或过载 |
使用示例
前端上传示例(JavaScript)
// 1. 获取预签名URL(支持文件夹内上传)
async function getUploadUrl(file, parentId = null) {
const response = await fetch('/api/presigned/public/generate-upload-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'your-api-key' // 可选
},
body: JSON.stringify({
filename: file.name,
contentType: file.type,
size: file.size,
parentId: parentId // 指定上传到哪个文件夹
})
});
return await response.json();
}
// 2. 上传到MinIO
async function uploadToMinIO(presignedUrl, file) {
const response = await fetch(presignedUrl, {
method: 'PUT',
headers: {
'Content-Type': file.type
},
body: file
});
if (!response.ok) {
throw new Error(`上传失败: ${response.status}`);
}
return response.headers.get('ETag');
}
// 3. 完成后端记录
async function completeUpload(fileKey, filename, size, mimetype, parentId, etag) {
const response = await fetch('/api/presigned/public/complete-upload', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileKey,
filename,
size,
mimetype,
parentId, // 记录父文件夹关系
etag
})
});
return await response.json();
}
文件夹内上传示例
// 在指定文件夹内上传文件
async function uploadToFolder(file, folderId) {
// 使用文件夹专用接口
const response = await fetch(`/api/presigned/folders/${folderId}/upload-url`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token'
},
body: JSON.stringify({
filename: file.name,
contentType: file.type,
expiresIn: 3600
})
});
const result = await response.json();
if (result.success) {
// 上传到MinIO...
await uploadToMinIO(result.data.url, file);
// 完成后端记录(parentId已包含在URL中)
await completeUpload(
result.data.key,
file.name,
file.size,
file.type,
folderId, // 传递文件夹ID
result.etag
);
}
}
系统架构
架构图
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 前端应用 │ │ Nginx代理 │ │ MinIO集群 │
│ (React+Vite) │────▶ (www.fox360.cn: │────▶ (www.fox360.cn: │
│ 192.168.39.152:│ │ 9002) │ │ 9000/9001) │
│ 5173 │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 后端API │ │ 数据库 │ │ Redis缓存 │
│ (NestJS) │◀───▶ (PostgreSQL) │◀───▶ (缓存服务) │
│ 192.168.39.152:│ │ │ │ │
│ 3000 │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
文件路径规则
根目录上传
uploads/{userId}/{year}/{month}/{day}/{timestamp}-{random}-{filename}
文件夹内上传
uploads/{userId}/folders/{folderId}/{year}/{month}/{day}/{timestamp}-{random}-{filename}
注意:系统自动处理路径中的双斜杠问题,确保路径正确性。