@kne/fastify-file-manager 1.2.4 → 2.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +286 -3
- package/index.js +2 -1
- package/libs/controllers/index.js +111 -150
- package/libs/services/file-record.js +49 -6
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -13,6 +13,24 @@
|
|
|
13
13
|
npm i --save @kne/fastify-file-manager
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
|
|
17
|
+
### 概述
|
|
18
|
+
|
|
19
|
+
### 项目概述
|
|
20
|
+
|
|
21
|
+
Fastify File Manager 是一个基于 Fastify 框架构建的高性能文件管理服务,提供文件上传、存储、检索和管理功能。该项目采用模块化设计,支持多种存储后端,包括本地文件系统和云存储服务(如阿里云OSS、AWS S3等)。
|
|
22
|
+
|
|
23
|
+
#### 核心特性
|
|
24
|
+
|
|
25
|
+
- 🚀 **高性能文件处理**:基于 Fastify 的轻量级架构
|
|
26
|
+
- 🔒 **安全认证**:集成 JWT 认证和权限控制
|
|
27
|
+
- 📁 **多存储支持**:可配置本地存储或云存储
|
|
28
|
+
- 🏷️ **文件分类**:通过命名空间(namespace)组织文件
|
|
29
|
+
- 📊 **元数据管理**:记录文件大小、类型、上传时间等信息
|
|
30
|
+
- 🔍 **文件检索**:支持按命名空间查询文件列表
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
16
34
|
### 示例
|
|
17
35
|
|
|
18
36
|
#### 示例代码
|
|
@@ -21,7 +39,272 @@ npm i --save @kne/fastify-file-manager
|
|
|
21
39
|
|
|
22
40
|
### API
|
|
23
41
|
|
|
24
|
-
|
|
|
25
|
-
|
|
26
|
-
|
|
|
42
|
+
| 参数 | 类型 | 默认值 | 必填 | 描述 |
|
|
43
|
+
|-----------------------------|----------|--------------------------------------|----|---------------------------------------------------------------------|
|
|
44
|
+
| **基础配置** | | | | |
|
|
45
|
+
| `root` | string | `path.join(process.cwd(), 'static')` | 否 | 文件存储根目录路径 |
|
|
46
|
+
| `namespace` | string | `'default'` | 否 | 默认文件分类命名空间 |
|
|
47
|
+
| `prefix` | string | `/api/v${majorVersion}/static` | 否 | API路由前缀,自动从package.json提取主版本号 |
|
|
48
|
+
| `dbTableNamePrefix` | string | `'t_file_manager_'` | 否 | 数据库表名前缀 |
|
|
49
|
+
| **文件上传** | | | | |
|
|
50
|
+
| `multipart.limits.fileSize` | number | `524288000` (500MB) | 否 | 单个文件最大上传大小 |
|
|
51
|
+
| **静态文件服务** | | | | |
|
|
52
|
+
| `static` | object | `{}` | 否 | 透传[@fastify/static](https://github.com/fastify/fastify-static)的所有配置 |
|
|
53
|
+
| **适配器配置** | | | | |
|
|
54
|
+
| `ossAdapter` | function | `() => {}` | 否 | OSS适配器工厂函数,需返回OSS配置对象 |
|
|
55
|
+
| `createAuthenticate` | function | `() => []` | 否 | 认证中间件工厂函数 |
|
|
56
|
+
|
|
57
|
+
### 配置示例
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
const options = {
|
|
61
|
+
root: '/data/uploads', // 自定义存储目录
|
|
62
|
+
namespace: 'user_files', // 业务隔离命名空间
|
|
63
|
+
multipart: {
|
|
64
|
+
limits: {
|
|
65
|
+
fileSize: 100 * 1024 * 1024 // 调整为100MB
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
ossAdapter: () => {
|
|
69
|
+
/** 需要注册 '@kne/fastify-aliyun' 插件
|
|
70
|
+
* fastify.register(require('@kne/fastify-aliyun'), {
|
|
71
|
+
prefix: `${apiPrefix}/aliyun`,
|
|
72
|
+
oss: {
|
|
73
|
+
baseDir: 'leapin-setting',
|
|
74
|
+
region: fastify.config.OSS_REGION,
|
|
75
|
+
accessKeyId: fastify.config.OSS_ACCESS_KEY_ID,
|
|
76
|
+
accessKeySecret: fastify.config.OSS_ACCESS_KEY_SECRET,
|
|
77
|
+
bucket: fastify.config.OSS_BUCKET
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
* */
|
|
81
|
+
return fastify.aliyun.services.oss;
|
|
82
|
+
},
|
|
83
|
+
createAuthenticate: (requiredPermission) => [
|
|
84
|
+
fastify.jwtVerify,
|
|
85
|
+
checkPermission(requiredPermission)
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 配置说明
|
|
91
|
+
|
|
92
|
+
1. **版本兼容性**
|
|
93
|
+
`prefix` 自动从 `package.json` 提取主版本号(如 `1.2.3` → `/api/v1/static`)
|
|
94
|
+
|
|
95
|
+
2. **存储目录**
|
|
96
|
+
- 默认会在项目根目录创建 `static` 文件夹
|
|
97
|
+
- 生产环境建议设置为绝对路径(如 `/var/www/uploads`)
|
|
98
|
+
|
|
99
|
+
3. **权限控制**
|
|
100
|
+
`createAuthenticate` 应返回 Fastify 钩子数组,典型实现:
|
|
101
|
+
```javascript
|
|
102
|
+
createAuthenticate: (perm) => [
|
|
103
|
+
fastify.authenticate,
|
|
104
|
+
(req, reply, done) => {
|
|
105
|
+
if(!req.user.permissions.includes(perm)) {
|
|
106
|
+
return reply.code(403).send()
|
|
107
|
+
}
|
|
108
|
+
done()
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 文件上传接口
|
|
114
|
+
|
|
115
|
+
#### `POST /api/v1/static/upload`
|
|
116
|
+
|
|
117
|
+
上传文件到服务器或配置的存储服务
|
|
118
|
+
|
|
119
|
+
##### 认证要求
|
|
120
|
+
|
|
121
|
+
- 需要 `file:write` 权限
|
|
122
|
+
- 通过 JWT 认证
|
|
123
|
+
|
|
124
|
+
##### 请求参数
|
|
125
|
+
|
|
126
|
+
| 参数 | 位置 | 类型 | 必填 | 描述 | 示例 |
|
|
127
|
+
|-----------|-------|--------|----|----------|----------------|
|
|
128
|
+
| namespace | query | string | 否 | 文件分类命名空间 | `user-avatars` |
|
|
129
|
+
| file | body | file | 是 | 要上传的文件 | - |
|
|
130
|
+
|
|
131
|
+
##### 请求示例
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
curl -X POST \
|
|
135
|
+
-H "Authorization: Bearer <JWT_TOKEN>" \
|
|
136
|
+
-F "file=@test.jpg" \
|
|
137
|
+
"http://localhost:3000/api/v1/static/upload?namespace=user-avatars"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
##### 响应状态码
|
|
141
|
+
|
|
142
|
+
| 状态码 | 描述 |
|
|
143
|
+
|-----|-----------|
|
|
144
|
+
| 200 | 文件上传成功 |
|
|
145
|
+
| 400 | 无效请求或缺少文件 |
|
|
146
|
+
| 401 | 未授权访问 |
|
|
147
|
+
| 413 | 文件大小超过限制 |
|
|
148
|
+
| 500 | 服务器错误 |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### 文件列表查询
|
|
153
|
+
|
|
154
|
+
#### `POST /api/v1/static/file-list`
|
|
155
|
+
|
|
156
|
+
查询指定条件下的文件列表(支持分页和筛选)
|
|
157
|
+
|
|
158
|
+
#### 认证要求
|
|
159
|
+
|
|
160
|
+
- 需要 `file:mange` 权限
|
|
161
|
+
- 通过 JWT 认证
|
|
162
|
+
|
|
163
|
+
#### 请求参数
|
|
164
|
+
|
|
165
|
+
##### Body 参数 (application/json)
|
|
166
|
+
|
|
167
|
+
| 参数 | 类型 | 必填 | 描述 | 默认值 | 示例 |
|
|
168
|
+
|--------------------|----------|----|---------------------|-----|-------------------|
|
|
169
|
+
| `perPage` | number | 否 | 每页显示数量 | 20 | `10` |
|
|
170
|
+
| `currentPage` | number | 否 | 当前页码 | 1 | `2` |
|
|
171
|
+
| `filter` | object | 否 | 筛选条件 | - | - |
|
|
172
|
+
| `filter.namespace` | string | 否 | 文件分类命名空间 | - | `"user-docs"` |
|
|
173
|
+
| `filter.size` | number[] | 否 | 文件大小范围[min,max](字节) | - | `[1024, 1048576]` |
|
|
174
|
+
| `filter.filename` | string | 否 | 文件名模糊匹配 | - | `"report.pdf"` |
|
|
175
|
+
|
|
176
|
+
##### 请求示例
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
curl -X POST \
|
|
180
|
+
-H "Authorization: Bearer <JWT_TOKEN>" \
|
|
181
|
+
-H "Content-Type: application/json" \
|
|
182
|
+
-d '{
|
|
183
|
+
"perPage": 10,
|
|
184
|
+
"currentPage": 2,
|
|
185
|
+
"filter": {
|
|
186
|
+
"namespace": "project-files",
|
|
187
|
+
"size": [1024, 1048576],
|
|
188
|
+
"filename": "report"
|
|
189
|
+
}
|
|
190
|
+
}' \
|
|
191
|
+
"http://localhost:3000/api/v1/static/file-list"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
##### 响应状态码
|
|
195
|
+
|
|
196
|
+
| 状态码 | 描述 |
|
|
197
|
+
|-----|---------|
|
|
198
|
+
| 200 | 查询成功 |
|
|
199
|
+
| 400 | 参数验证失败 |
|
|
200
|
+
| 401 | 未授权访问 |
|
|
201
|
+
| 500 | 服务器内部错误 |
|
|
202
|
+
|
|
203
|
+
##### 筛选逻辑说明
|
|
204
|
+
|
|
205
|
+
1. **命名空间筛选**:精确匹配传入的 `namespace` 值
|
|
206
|
+
2. **文件大小筛选**:
|
|
207
|
+
- 数组第一个元素为最小值
|
|
208
|
+
- 数组第二个元素为最大值
|
|
209
|
+
3. **文件名筛选**:使用 `LIKE %value%` 模糊匹配
|
|
210
|
+
|
|
211
|
+
##### 分页说明
|
|
212
|
+
|
|
213
|
+
- 分页计算:`offset = (currentPage - 1) * perPage`
|
|
214
|
+
- 建议每页数量不超过 100 条
|
|
215
|
+
- 页码从 1 开始计数
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### 文件删除接口
|
|
220
|
+
|
|
221
|
+
#### `DELETE /api/v1/static/:fileId`
|
|
222
|
+
|
|
223
|
+
删除指定文件
|
|
224
|
+
|
|
225
|
+
##### 认证要求
|
|
226
|
+
|
|
227
|
+
- 需要 `file:write` 权限
|
|
228
|
+
|
|
229
|
+
##### 请求参数
|
|
230
|
+
|
|
231
|
+
| 参数 | 位置 | 类型 | 必填 | 描述 | 示例 |
|
|
232
|
+
|--------|------|--------|----|----------|----------|
|
|
233
|
+
| fileId | path | string | 是 | 要删除的文件ID | `abc123` |
|
|
234
|
+
|
|
235
|
+
##### 请求示例
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
curl -X DELETE \
|
|
239
|
+
-H "Authorization: Bearer <JWT_TOKEN>" \
|
|
240
|
+
"http://localhost:3000/api/v1/static/abc123"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
##### 响应状态码
|
|
244
|
+
|
|
245
|
+
| 状态码 | 描述 |
|
|
246
|
+
|-----|--------|
|
|
247
|
+
| 204 | 文件删除成功 |
|
|
248
|
+
| 404 | 文件不存在 |
|
|
249
|
+
| 500 | 服务器错误 |
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
### 文件信息接口
|
|
254
|
+
|
|
255
|
+
#### `GET /api/v1/static/:fileId`
|
|
256
|
+
|
|
257
|
+
获取文件详细信息
|
|
258
|
+
|
|
259
|
+
##### 认证要求
|
|
260
|
+
|
|
261
|
+
- 需要 `file:read` 权限
|
|
262
|
+
|
|
263
|
+
##### 请求参数
|
|
264
|
+
|
|
265
|
+
| 参数 | 位置 | 类型 | 必填 | 描述 | 示例 |
|
|
266
|
+
|--------|------|--------|----|----------|----------|
|
|
267
|
+
| fileId | path | string | 是 | 要查询的文件ID | `abc123` |
|
|
268
|
+
|
|
269
|
+
##### 请求示例
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
curl -X GET \
|
|
273
|
+
-H "Authorization: Bearer <JWT_TOKEN>" \
|
|
274
|
+
"http://localhost:3000/api/v1/static/abc123"
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
##### 响应示例
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{
|
|
281
|
+
"id": "abc123",
|
|
282
|
+
"name": "document.pdf",
|
|
283
|
+
"size": 102400,
|
|
284
|
+
"mimeType": "application/pdf",
|
|
285
|
+
"createdAt": "2023-01-01T00:00:00Z",
|
|
286
|
+
"url": "/api/v1/static/file/documents/abc123.pdf"
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
### 接口使用说明
|
|
293
|
+
|
|
294
|
+
1. **认证方式**:
|
|
295
|
+
- 所有接口都需要在 Header 中添加 `Authorization: Bearer <JWT_TOKEN>`
|
|
296
|
+
- JWT 需要包含相应的权限声明
|
|
297
|
+
|
|
298
|
+
2. **命名空间规则**:
|
|
299
|
+
- 命名空间支持字母、数字和下划线组合
|
|
300
|
+
- 未指定时使用默认命名空间
|
|
301
|
+
|
|
302
|
+
3. **文件大小限制**:
|
|
303
|
+
- 默认最大 500MB
|
|
304
|
+
- 可在服务端配置中调整
|
|
305
|
+
|
|
306
|
+
4. **错误处理**:
|
|
307
|
+
- 所有错误响应都包含标准格式的 error 字段
|
|
308
|
+
- 客户端应检查状态码而非仅依赖响应体
|
|
27
309
|
|
|
310
|
+
> 注意:实际 API 路径前缀会根据配置的 `prefix` 参数变化
|
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const fp = require('fastify-plugin');
|
|
2
|
-
const path = require('path');
|
|
2
|
+
const path = require('node:path');
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const packageJson = require('./package.json');
|
|
5
5
|
|
|
@@ -31,6 +31,7 @@ module.exports = fp(
|
|
|
31
31
|
fastify.register(require('@kne/fastify-namespace'), {
|
|
32
32
|
name: 'fileManager',
|
|
33
33
|
options,
|
|
34
|
+
singleton: true,
|
|
34
35
|
modules: [
|
|
35
36
|
[
|
|
36
37
|
'models',
|
|
@@ -2,186 +2,147 @@ const fp = require('fastify-plugin');
|
|
|
2
2
|
|
|
3
3
|
module.exports = fp(async (fastify, options) => {
|
|
4
4
|
const { services } = fastify.fileManager;
|
|
5
|
-
fastify.post(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
query: {
|
|
11
|
-
type: 'object',
|
|
12
|
-
properties: {
|
|
13
|
-
namespace: { type: 'string' }
|
|
14
|
-
}
|
|
5
|
+
fastify.post(`${options.prefix}/upload`, {
|
|
6
|
+
onRequest: options.createAuthenticate('file:write'), schema: {
|
|
7
|
+
summary: '上传文件', description: '上传单个文件到服务器或配置的存储服务', query: {
|
|
8
|
+
type: 'object', properties: {
|
|
9
|
+
namespace: { type: 'string', description: '文件分类命名空间' }
|
|
15
10
|
}
|
|
16
11
|
}
|
|
17
|
-
},
|
|
18
|
-
async request => {
|
|
19
|
-
const file = await request.file();
|
|
20
|
-
if (!file) {
|
|
21
|
-
throw new Error('不能获取到上传文件');
|
|
22
|
-
}
|
|
23
|
-
//1. 保存到服务器目录 2.对接oss
|
|
24
|
-
return await services.fileRecord.uploadToFileSystem({
|
|
25
|
-
file,
|
|
26
|
-
namespace: request.query.namespace || options.namespace
|
|
27
|
-
});
|
|
28
12
|
}
|
|
29
|
-
|
|
13
|
+
}, async request => {
|
|
14
|
+
const file = await request.file();
|
|
15
|
+
if (!file) {
|
|
16
|
+
throw new Error('不能获取到上传文件');
|
|
17
|
+
}
|
|
18
|
+
return await services.fileRecord.uploadToFileSystem({
|
|
19
|
+
file, namespace: request.query.namespace || options.namespace
|
|
20
|
+
});
|
|
21
|
+
});
|
|
30
22
|
|
|
31
|
-
fastify.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
type: 'object',
|
|
38
|
-
required: ['id'],
|
|
39
|
-
properties: {
|
|
40
|
-
id: { type: 'string' }
|
|
41
|
-
}
|
|
23
|
+
fastify.post(`${options.prefix}/uploadUrl`, {
|
|
24
|
+
onRequest: options.createAuthenticate('file:write'), schema: {
|
|
25
|
+
summary: '上传URL文件', description: '上传单个文件到服务器或配置的存储服务', body: {
|
|
26
|
+
type: 'object', properties: {
|
|
27
|
+
url: { type: 'string', description: '文件url' },
|
|
28
|
+
namespace: { type: 'string', description: '文件分类命名空间' }
|
|
42
29
|
}
|
|
43
30
|
}
|
|
44
|
-
},
|
|
45
|
-
async request => {
|
|
46
|
-
const { id } = request.params;
|
|
47
|
-
return await services.fileRecord.getFileUrl({ id });
|
|
48
31
|
}
|
|
49
|
-
|
|
32
|
+
}, async request => {
|
|
33
|
+
return await services.fileRecord.uploadFromUrl({
|
|
34
|
+
url: request.body.url, namespace: request.body.namespace || options.namespace
|
|
35
|
+
});
|
|
36
|
+
});
|
|
50
37
|
|
|
51
|
-
fastify.get(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
query: {
|
|
57
|
-
type: 'object',
|
|
58
|
-
properties: {
|
|
59
|
-
attachment: { type: 'boolean' },
|
|
60
|
-
filename: { type: 'string' }
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
params: {
|
|
64
|
-
type: 'object',
|
|
65
|
-
required: ['id'],
|
|
66
|
-
properties: {
|
|
67
|
-
id: { type: 'string' }
|
|
68
|
-
}
|
|
38
|
+
fastify.get(`${options.prefix}/file-url/:id`, {
|
|
39
|
+
onRequest: options.createAuthenticate('file:read'), schema: {
|
|
40
|
+
summary: '获取文件url', description: '获取文件url', params: {
|
|
41
|
+
type: 'object', required: ['id'], properties: {
|
|
42
|
+
id: { type: 'string', description: '文件id' }
|
|
69
43
|
}
|
|
70
44
|
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
45
|
+
}
|
|
46
|
+
}, async request => {
|
|
47
|
+
const { id } = request.params;
|
|
48
|
+
return await services.fileRecord.getFileUrl({ id });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
fastify.get(`${options.prefix}/file-id/:id`, {
|
|
52
|
+
onRequest: options.createAuthenticate('file:read'), schema: {
|
|
53
|
+
summary: '获取文件信息', description: '获取文件信息', query: {
|
|
54
|
+
type: 'object', properties: {
|
|
55
|
+
attachment: { type: 'boolean', description: '是否下载' },
|
|
56
|
+
filename: { type: 'string', description: '下载文件名' }
|
|
57
|
+
}
|
|
58
|
+
}, params: {
|
|
59
|
+
type: 'object', required: ['id'], properties: {
|
|
60
|
+
id: { type: 'string', description: '文件id' }
|
|
61
|
+
}
|
|
83
62
|
}
|
|
84
|
-
return attachment ? reply.download(filePath, targetFilename || filename) : reply.sendFile(filePath);
|
|
85
63
|
}
|
|
86
|
-
)
|
|
64
|
+
}, async (request, reply) => {
|
|
65
|
+
const { id } = request.params;
|
|
66
|
+
const { attachment, filename: targetFilename } = request.query;
|
|
67
|
+
const { filePath, targetFile, filename, mimetype, ...props } = await services.fileRecord.getFileInfo({
|
|
68
|
+
id
|
|
69
|
+
});
|
|
70
|
+
if (targetFile) {
|
|
71
|
+
const outputFilename = encodeURIComponent(targetFilename || filename);
|
|
72
|
+
reply.header('Content-Type', mimetype);
|
|
73
|
+
reply.header('Content-Disposition', attachment ? `attachment; filename="${outputFilename}"` : `filename="${outputFilename}"`);
|
|
74
|
+
return reply.send(targetFile);
|
|
75
|
+
}
|
|
76
|
+
return attachment ? reply.download(filePath, targetFilename || filename) : reply.sendFile(filePath);
|
|
77
|
+
});
|
|
87
78
|
|
|
88
|
-
fastify.post(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
type: 'object',
|
|
100
|
-
properties: {
|
|
101
|
-
namespace: { type: 'string' },
|
|
102
|
-
size: { type: 'array', items: { type: 'number' } },
|
|
103
|
-
filename: { type: 'string' }
|
|
104
|
-
}
|
|
79
|
+
fastify.post(`${options.prefix}/file-list`, {
|
|
80
|
+
onRequest: options.createAuthenticate('file:mange'), schema: {
|
|
81
|
+
summary: '获取文件列表', description: '查询指定命名空间下的文件列表', body: {
|
|
82
|
+
type: 'object', properties: {
|
|
83
|
+
perPage: { type: 'number', description: '每页数量' },
|
|
84
|
+
currentPage: { type: 'number', description: '当前页数' },
|
|
85
|
+
filter: {
|
|
86
|
+
type: 'object', properties: {
|
|
87
|
+
namespace: { type: 'string', description: '文件分类命名空间' },
|
|
88
|
+
size: { type: 'array', items: { type: 'number' }, description: '文件大小' },
|
|
89
|
+
filename: { type: 'string', description: '文件名' }
|
|
105
90
|
}
|
|
106
91
|
}
|
|
107
92
|
}
|
|
108
93
|
}
|
|
109
|
-
},
|
|
110
|
-
async request => {
|
|
111
|
-
const { filter, perPage, currentPage } = Object.assign(
|
|
112
|
-
{},
|
|
113
|
-
{
|
|
114
|
-
perPage: 20,
|
|
115
|
-
currentPage: 1
|
|
116
|
-
},
|
|
117
|
-
request.body
|
|
118
|
-
);
|
|
119
|
-
return await services.fileRecord.getFileList({
|
|
120
|
-
filter,
|
|
121
|
-
perPage,
|
|
122
|
-
currentPage
|
|
123
|
-
});
|
|
124
94
|
}
|
|
125
|
-
|
|
95
|
+
}, async request => {
|
|
96
|
+
const { filter, perPage, currentPage } = Object.assign({}, {
|
|
97
|
+
perPage: 20, currentPage: 1
|
|
98
|
+
}, request.body);
|
|
99
|
+
return await services.fileRecord.getFileList({
|
|
100
|
+
filter, perPage, currentPage
|
|
101
|
+
});
|
|
102
|
+
});
|
|
126
103
|
|
|
127
104
|
// Replace file
|
|
128
105
|
|
|
129
|
-
fastify.post(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
type: 'object',
|
|
135
|
-
properties: {
|
|
136
|
-
id: { type: 'string' }
|
|
106
|
+
fastify.post(`${options.prefix}/replace-file`, {
|
|
107
|
+
onRequest: options.createAuthenticate('file:mange'), schema: {
|
|
108
|
+
summary: '替换文件', description: '替换文件', query: {
|
|
109
|
+
type: 'object', properties: {
|
|
110
|
+
id: { type: 'string', description: '文件id' }
|
|
137
111
|
}
|
|
138
112
|
}
|
|
139
|
-
},
|
|
140
|
-
async request => {
|
|
141
|
-
const file = await request.file();
|
|
142
|
-
if (!file) {
|
|
143
|
-
throw new Error('不能获取到上传文件');
|
|
144
|
-
}
|
|
145
|
-
return await services.fileRecord.uploadToFileSystem({ id: request.query.id, file });
|
|
146
113
|
}
|
|
147
|
-
|
|
114
|
+
}, async request => {
|
|
115
|
+
const file = await request.file();
|
|
116
|
+
if (!file) {
|
|
117
|
+
throw new Error('不能获取到上传文件');
|
|
118
|
+
}
|
|
119
|
+
return await services.fileRecord.uploadToFileSystem({ id: request.query.id, file });
|
|
120
|
+
});
|
|
148
121
|
|
|
149
|
-
fastify.post(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
type: 'object',
|
|
155
|
-
properties: {
|
|
156
|
-
id: { type: 'string' },
|
|
157
|
-
filename: { type: 'string' }
|
|
122
|
+
fastify.post(`${options.prefix}/rename-file`, {
|
|
123
|
+
onRequest: options.createAuthenticate('file:mange'), schema: {
|
|
124
|
+
summary: '重命名文件', description: '重命名文件', body: {
|
|
125
|
+
type: 'object', properties: {
|
|
126
|
+
id: { type: 'string', description: '文件id' }, filename: { type: 'string', description: '新文件名' }
|
|
158
127
|
}
|
|
159
128
|
}
|
|
160
|
-
},
|
|
161
|
-
async request => {
|
|
162
|
-
await services.fileRecord.renameFile(request.body);
|
|
163
|
-
return {};
|
|
164
129
|
}
|
|
165
|
-
|
|
130
|
+
}, async request => {
|
|
131
|
+
await services.fileRecord.renameFile(request.body);
|
|
132
|
+
return {};
|
|
133
|
+
});
|
|
166
134
|
|
|
167
|
-
fastify.post(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
body: {
|
|
173
|
-
type: 'object',
|
|
174
|
-
required: ['ids'],
|
|
175
|
-
properties: {
|
|
176
|
-
ids: { type: 'array', items: { type: 'string' } }
|
|
177
|
-
}
|
|
135
|
+
fastify.post(`${options.prefix}/delete-files`, {
|
|
136
|
+
onRequest: options.createAuthenticate('file:mange'), schema: {
|
|
137
|
+
summary: '删除文件', description: '删除文件', body: {
|
|
138
|
+
type: 'object', required: ['ids'], properties: {
|
|
139
|
+
ids: { type: 'array', items: { type: 'string' }, description: '文件id列表' }
|
|
178
140
|
}
|
|
179
141
|
}
|
|
180
|
-
},
|
|
181
|
-
async request => {
|
|
182
|
-
const { ids } = request.body;
|
|
183
|
-
await services.fileRecord.deleteFiles({ ids });
|
|
184
|
-
return {};
|
|
185
142
|
}
|
|
186
|
-
|
|
143
|
+
}, async request => {
|
|
144
|
+
const { ids } = request.body;
|
|
145
|
+
await services.fileRecord.deleteFiles({ ids });
|
|
146
|
+
return {};
|
|
147
|
+
});
|
|
187
148
|
});
|
|
@@ -9,20 +9,35 @@ module.exports = fp(async (fastify, options) => {
|
|
|
9
9
|
const { Op } = fastify.sequelize.Sequelize;
|
|
10
10
|
const uploadToFileSystem = async ({ id, file, namespace }) => {
|
|
11
11
|
const { filename, encoding, mimetype } = file;
|
|
12
|
-
const buffer = await file.toBuffer();
|
|
13
12
|
const hash = crypto.createHash('md5');
|
|
14
|
-
hash.update(buffer);
|
|
15
|
-
const digest = hash.digest('hex');
|
|
16
13
|
const extension = path.extname(filename);
|
|
14
|
+
let buffer = Buffer.alloc(0);
|
|
15
|
+
|
|
16
|
+
// 使用流处理文件数据
|
|
17
|
+
const stream = file.createReadStream();
|
|
18
|
+
for await (const chunk of stream) {
|
|
19
|
+
hash.update(chunk);
|
|
20
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
21
|
+
}
|
|
22
|
+
const digest = hash.digest('hex');
|
|
17
23
|
|
|
18
24
|
let storageType;
|
|
19
25
|
const ossServices = options.ossAdapter();
|
|
20
26
|
if (typeof ossServices.uploadFile === 'function') {
|
|
21
|
-
|
|
27
|
+
// 使用流上传到OSS
|
|
28
|
+
const uploadStream = file.createReadStream();
|
|
29
|
+
await ossServices.uploadFileStream({ stream: uploadStream, filename: `${digest}${extension}` });
|
|
22
30
|
storageType = 'oss';
|
|
23
31
|
} else {
|
|
32
|
+
// 使用流写入本地文件
|
|
24
33
|
const filepath = path.resolve(options.root, `${digest}${extension}`);
|
|
25
|
-
|
|
34
|
+
const writeStream = fs.createWriteStream(filepath);
|
|
35
|
+
const readStream = file.createReadStream();
|
|
36
|
+
await new Promise((resolve, reject) => {
|
|
37
|
+
readStream.pipe(writeStream)
|
|
38
|
+
.on('finish', resolve)
|
|
39
|
+
.on('error', reject);
|
|
40
|
+
});
|
|
26
41
|
storageType = 'local';
|
|
27
42
|
}
|
|
28
43
|
|
|
@@ -56,6 +71,30 @@ module.exports = fp(async (fastify, options) => {
|
|
|
56
71
|
return Object.assign({}, outputFile.get({ plain: true }), { id: outputFile.uuid });
|
|
57
72
|
};
|
|
58
73
|
|
|
74
|
+
const uploadFromUrl = async ({ id, url, namespace }) => {
|
|
75
|
+
const response = await fetch(url);
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new Error('下载文件失败');
|
|
78
|
+
}
|
|
79
|
+
const chunks = [];
|
|
80
|
+
for await (const chunk of response.body) {
|
|
81
|
+
chunks.push(chunk);
|
|
82
|
+
}
|
|
83
|
+
const buffer = Buffer.concat(chunks);
|
|
84
|
+
const tempFile = {
|
|
85
|
+
filename: path.basename(url).split('?')[0],
|
|
86
|
+
mimetype: response.headers.get('content-type'),
|
|
87
|
+
encoding: 'binary',
|
|
88
|
+
createReadStream: () => {
|
|
89
|
+
const readable = new require('stream').Readable();
|
|
90
|
+
readable.push(buffer);
|
|
91
|
+
readable.push(null);
|
|
92
|
+
return readable;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
return await uploadToFileSystem({ id, file: tempFile, namespace });
|
|
96
|
+
};
|
|
97
|
+
|
|
59
98
|
const getFileUrl = async ({ id, namespace }) => {
|
|
60
99
|
const file = await models.fileRecord.findOne({
|
|
61
100
|
where: { uuid: id }
|
|
@@ -158,5 +197,9 @@ module.exports = fp(async (fastify, options) => {
|
|
|
158
197
|
await file.save();
|
|
159
198
|
};
|
|
160
199
|
|
|
161
|
-
services
|
|
200
|
+
Object.assign(services, {
|
|
201
|
+
uploadToFileSystem, uploadFromUrl, getFileUrl, getFileInfo, getFileList, deleteFiles, renameFile,
|
|
202
|
+
// 兼容之前api,后面可能会删掉
|
|
203
|
+
fileRecord: { uploadToFileSystem, uploadFromUrl, getFileUrl, getFileInfo, getFileList, deleteFiles, renameFile }
|
|
204
|
+
});
|
|
162
205
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kne/fastify-file-manager",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-alpha.1",
|
|
4
4
|
"description": "用于管理静态文件上传查看等",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -32,20 +32,23 @@
|
|
|
32
32
|
},
|
|
33
33
|
"homepage": "https://github.com/kne-union/fastify-file-manager#readme",
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@fastify/env": "^
|
|
35
|
+
"@fastify/env": "^5.0.2",
|
|
36
36
|
"@kne/fastify-aliyun": "^1.1.1",
|
|
37
37
|
"@kne/fastify-sequelize": "^2.0.1",
|
|
38
|
-
"fastify": "^
|
|
38
|
+
"fastify": "^5.3.2",
|
|
39
39
|
"husky": "^9.0.11",
|
|
40
40
|
"prettier": "^3.2.5",
|
|
41
41
|
"qs": "^6.12.3",
|
|
42
42
|
"sqlite3": "^5.1.7"
|
|
43
43
|
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@kne/fastify-namespace": "*",
|
|
46
|
+
"@kne/fastify-sequelize": "*",
|
|
47
|
+
"fastify-plugin": ">=5"
|
|
48
|
+
},
|
|
44
49
|
"dependencies": {
|
|
45
|
-
"@fastify/multipart": "^
|
|
46
|
-
"@fastify/static": "^
|
|
47
|
-
"@kne/fastify-namespace": "^0.1.0",
|
|
48
|
-
"fastify-plugin": "^4.5.1",
|
|
50
|
+
"@fastify/multipart": "^9.0.3",
|
|
51
|
+
"@fastify/static": "^8.1.1",
|
|
49
52
|
"fs-extra": "^11.2.0",
|
|
50
53
|
"http-errors": "^2.0.0"
|
|
51
54
|
}
|