@kne/fastify-file-manager 3.0.4 → 3.0.7
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/index.js +21 -2
- package/libs/controllers/index.js +46 -5
- package/libs/services/folder.js +105 -34
- package/package.json +3 -3
package/index.js
CHANGED
|
@@ -23,9 +23,9 @@ module.exports = fp(
|
|
|
23
23
|
createAuthenticate: () => {
|
|
24
24
|
return [];
|
|
25
25
|
},
|
|
26
|
-
//
|
|
26
|
+
// 文件夹能力;插件内部自行注册 @kne/fastify-group,无需宿主再挂一份
|
|
27
27
|
enableFolder: true,
|
|
28
|
-
groupName: '
|
|
28
|
+
groupName: 'file-manager-folder',
|
|
29
29
|
// 上传自动挂载文件系统时的默认业务域
|
|
30
30
|
defaultFolderType: 'admin-file-system',
|
|
31
31
|
getAuthenticate: () => {
|
|
@@ -35,6 +35,25 @@ module.exports = fp(
|
|
|
35
35
|
options
|
|
36
36
|
);
|
|
37
37
|
await fs.ensureDir(options.root);
|
|
38
|
+
|
|
39
|
+
if (options.enableFolder !== false) {
|
|
40
|
+
const denyGroupHttp = () => [
|
|
41
|
+
async () => {
|
|
42
|
+
const err = new Error('Forbidden');
|
|
43
|
+
err.statusCode = 403;
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
];
|
|
47
|
+
await fastify.register(require('@kne/fastify-group'), {
|
|
48
|
+
name: options.groupName,
|
|
49
|
+
// 表名 = prefix + snakeCase(name + Model),默认 t_ + file_manager_folder_tag
|
|
50
|
+
dbTableNamePrefix: options.groupDbTableNamePrefix || 't_',
|
|
51
|
+
// 仅使用 group 的 models/services;HTTP API 一律 403,业务走 folder/*
|
|
52
|
+
prefix: options.groupPrefix || `${options.prefix}/folder-group`,
|
|
53
|
+
getAuthenticate: denyGroupHttp
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
fastify.register(require('@fastify/multipart'), options.multipart);
|
|
39
58
|
fastify.register(require('@kne/fastify-namespace'), {
|
|
40
59
|
name: 'fileManager',
|
|
@@ -356,12 +356,13 @@ module.exports = fp(async (fastify, options) => {
|
|
|
356
356
|
onRequest: folderOnRequest('read', 'query'),
|
|
357
357
|
schema: {
|
|
358
358
|
summary: '获取文件夹树',
|
|
359
|
-
description: '
|
|
359
|
+
description: '获取指定 type 的文件树;kind=folder 时仅返回文件夹节点;未传 type 时使用 defaultFolderType',
|
|
360
360
|
query: {
|
|
361
361
|
type: 'object',
|
|
362
362
|
properties: {
|
|
363
363
|
type: { type: 'string', description: '业务域类型,默认 admin-file-system' },
|
|
364
|
-
language: { type: 'string', description: '语言' }
|
|
364
|
+
language: { type: 'string', description: '语言' },
|
|
365
|
+
kind: { type: 'string', description: '传 folder 时仅返回文件夹树' }
|
|
365
366
|
}
|
|
366
367
|
}
|
|
367
368
|
}
|
|
@@ -371,6 +372,46 @@ module.exports = fp(async (fastify, options) => {
|
|
|
371
372
|
}
|
|
372
373
|
);
|
|
373
374
|
|
|
375
|
+
fastify.post(
|
|
376
|
+
`${options.prefix}/folder/list`,
|
|
377
|
+
{
|
|
378
|
+
onRequest: folderOnRequest('read', 'body'),
|
|
379
|
+
schema: {
|
|
380
|
+
summary: '分页获取目录子节点',
|
|
381
|
+
description: '按 parentId 分页列出直接子节点;文件夹优先,再按名称排序;未传 type 时使用 defaultFolderType',
|
|
382
|
+
body: {
|
|
383
|
+
type: 'object',
|
|
384
|
+
properties: {
|
|
385
|
+
type: { type: 'string', description: '业务域类型,默认 admin-file-system' },
|
|
386
|
+
parentId: { type: 'string', description: '父文件夹 id,根目录可不传' },
|
|
387
|
+
currentPage: { type: 'number', description: '当前页数' },
|
|
388
|
+
perPage: { type: 'number', description: '每页数量' },
|
|
389
|
+
keyword: { type: 'string', description: '按名称模糊搜索' },
|
|
390
|
+
language: { type: 'string', description: '语言' }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
async request => {
|
|
396
|
+
const { type, parentId, currentPage, perPage, keyword, language } = Object.assign(
|
|
397
|
+
{},
|
|
398
|
+
{
|
|
399
|
+
currentPage: 1,
|
|
400
|
+
perPage: 20
|
|
401
|
+
},
|
|
402
|
+
request.body
|
|
403
|
+
);
|
|
404
|
+
return await services.folder.getList({
|
|
405
|
+
type,
|
|
406
|
+
parentId,
|
|
407
|
+
currentPage,
|
|
408
|
+
perPage,
|
|
409
|
+
keyword,
|
|
410
|
+
language
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
);
|
|
414
|
+
|
|
374
415
|
fastify.post(
|
|
375
416
|
`${options.prefix}/folder/mkdir`,
|
|
376
417
|
{
|
|
@@ -401,13 +442,13 @@ module.exports = fp(async (fastify, options) => {
|
|
|
401
442
|
onRequest: getFolderAuthenticate('write'),
|
|
402
443
|
schema: {
|
|
403
444
|
summary: '上传文件到文件夹',
|
|
404
|
-
description: '上传文件实体并在 group
|
|
445
|
+
description: '上传文件实体并在 group 中创建文件节点。通过 path 定位目标目录(经 ensurePath 解析/创建;空字符串为根目录)',
|
|
405
446
|
query: {
|
|
406
447
|
type: 'object',
|
|
407
448
|
required: ['type'],
|
|
408
449
|
properties: {
|
|
409
450
|
type: { type: 'string', description: '业务域类型' },
|
|
410
|
-
|
|
451
|
+
path: { type: 'string', description: '目标文件夹路径(如 a/b/c);空字符串或不传表示根目录' },
|
|
411
452
|
namespace: { type: 'string', description: '文件命名空间' },
|
|
412
453
|
language: { type: 'string', description: '语言' }
|
|
413
454
|
}
|
|
@@ -421,7 +462,7 @@ module.exports = fp(async (fastify, options) => {
|
|
|
421
462
|
}
|
|
422
463
|
return await services.folder.upload({
|
|
423
464
|
type: request.query.type,
|
|
424
|
-
|
|
465
|
+
path: request.query.path,
|
|
425
466
|
namespace: request.query.namespace,
|
|
426
467
|
language: request.query.language,
|
|
427
468
|
file
|
package/libs/services/folder.js
CHANGED
|
@@ -5,10 +5,10 @@ module.exports = fp(async (fastify, fastifyOptions) => {
|
|
|
5
5
|
return;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
const groupName = fastifyOptions.groupName || '
|
|
8
|
+
const groupName = fastifyOptions.groupName || 'file-manager-folder';
|
|
9
9
|
const group = fastify[groupName];
|
|
10
10
|
if (!group?.services) {
|
|
11
|
-
throw new Error(
|
|
11
|
+
throw new Error(`文件夹功能未就绪:缺少内部 group 命名空间 fastify['${groupName}']`);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const { services } = fastify.fileManager;
|
|
@@ -109,13 +109,8 @@ module.exports = fp(async (fastify, fastifyOptions) => {
|
|
|
109
109
|
return ids;
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
if (fileIds.length === 0) {
|
|
115
|
-
return tree;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const uniqueIds = [...new Set(fileIds.map(String))];
|
|
112
|
+
const loadFileMap = async fileIds => {
|
|
113
|
+
const uniqueIds = [...new Set((fileIds || []).map(String).filter(Boolean))];
|
|
119
114
|
const fileMap = new Map();
|
|
120
115
|
await Promise.all(
|
|
121
116
|
uniqueIds.map(async id => {
|
|
@@ -123,33 +118,44 @@ module.exports = fp(async (fastify, fastifyOptions) => {
|
|
|
123
118
|
const file = await services.fileRecord.getFileInstance({ id });
|
|
124
119
|
fileMap.set(String(id), file);
|
|
125
120
|
} catch (e) {
|
|
126
|
-
//
|
|
121
|
+
// 文件记录缺失时仍返回节点
|
|
127
122
|
}
|
|
128
123
|
})
|
|
129
124
|
);
|
|
125
|
+
return fileMap;
|
|
126
|
+
};
|
|
130
127
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
128
|
+
const applyFileMeta = (node, fileMap) => {
|
|
129
|
+
const plain = { ...node };
|
|
130
|
+
const kind = getNodeKind(plain);
|
|
131
|
+
plain.options = Object.assign({}, plain.options, { kind });
|
|
132
|
+
if (kind === 'file' && plain.options.fileId) {
|
|
133
|
+
const file = fileMap.get(String(plain.options.fileId));
|
|
134
|
+
if (file) {
|
|
135
|
+
plain.options = Object.assign({}, plain.options, {
|
|
136
|
+
size: file.size,
|
|
137
|
+
mimetype: file.mimetype,
|
|
138
|
+
filename: file.filename
|
|
139
|
+
});
|
|
140
|
+
if (file.createdAt) {
|
|
141
|
+
plain.createdAt = file.createdAt;
|
|
142
|
+
}
|
|
143
|
+
if (file.updatedAt) {
|
|
144
|
+
plain.updatedAt = file.updatedAt;
|
|
149
145
|
}
|
|
150
146
|
}
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
}
|
|
148
|
+
return plain;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const enrichTree = async tree => {
|
|
152
|
+
const fileIds = collectFileIds(tree);
|
|
153
|
+
const fileMap = fileIds.length === 0 ? new Map() : await loadFileMap(fileIds);
|
|
154
|
+
|
|
155
|
+
const mapNode = node => {
|
|
156
|
+
const plain = applyFileMeta(node, fileMap);
|
|
157
|
+
if (node.children?.length) {
|
|
158
|
+
plain.children = node.children.map(mapNode);
|
|
153
159
|
}
|
|
154
160
|
return plain;
|
|
155
161
|
};
|
|
@@ -157,12 +163,75 @@ module.exports = fp(async (fastify, fastifyOptions) => {
|
|
|
157
163
|
return (tree || []).map(mapNode);
|
|
158
164
|
};
|
|
159
165
|
|
|
160
|
-
const
|
|
166
|
+
const enrichNodes = async nodes => {
|
|
167
|
+
const list = nodes || [];
|
|
168
|
+
const fileIds = list
|
|
169
|
+
.filter(node => getNodeKind(node) === 'file' && node.options?.fileId)
|
|
170
|
+
.map(node => node.options.fileId);
|
|
171
|
+
const fileMap = await loadFileMap(fileIds);
|
|
172
|
+
return list.map(node => applyFileMeta(node, fileMap));
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const filterFoldersOnly = tree =>
|
|
176
|
+
(tree || [])
|
|
177
|
+
.filter(node => getNodeKind(node) === 'folder')
|
|
178
|
+
.map(node => {
|
|
179
|
+
const plain = { ...node };
|
|
180
|
+
plain.options = Object.assign({}, plain.options, { kind: 'folder' });
|
|
181
|
+
plain.children = filterFoldersOnly(node.children);
|
|
182
|
+
return plain;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const sortSiblingNodes = (left, right) => {
|
|
186
|
+
const leftIsFolder = getNodeKind(left) === 'folder' ? 0 : 1;
|
|
187
|
+
const rightIsFolder = getNodeKind(right) === 'folder' ? 0 : 1;
|
|
188
|
+
if (leftIsFolder !== rightIsFolder) {
|
|
189
|
+
return leftIsFolder - rightIsFolder;
|
|
190
|
+
}
|
|
191
|
+
return String(left.name || '').localeCompare(String(right.name || ''), undefined, {
|
|
192
|
+
numeric: true,
|
|
193
|
+
sensitivity: 'base'
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const getTree = async ({ type, language, tenantId, kind }) => {
|
|
161
198
|
if (!type) {
|
|
162
199
|
throw new Error('必须传入类型');
|
|
163
200
|
}
|
|
164
201
|
const tree = await groupServices.groupList({ type, language, output: 'tree', tenantId });
|
|
165
|
-
|
|
202
|
+
const enriched = await enrichTree(tree);
|
|
203
|
+
if (kind === 'folder' || kind === 'foldersOnly') {
|
|
204
|
+
return filterFoldersOnly(enriched);
|
|
205
|
+
}
|
|
206
|
+
return enriched;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const getList = async ({ type, parentId, currentPage = 1, perPage = 20, keyword, language, tenantId }) => {
|
|
210
|
+
if (!type) {
|
|
211
|
+
throw new Error('必须传入类型');
|
|
212
|
+
}
|
|
213
|
+
const { Op } = fastify.sequelize.Sequelize;
|
|
214
|
+
const page = Math.max(1, Number(currentPage) || 1);
|
|
215
|
+
const size = Math.min(200, Math.max(1, Number(perPage) || 20));
|
|
216
|
+
const where = Object.assign(
|
|
217
|
+
{ type, parentId: parentId || null },
|
|
218
|
+
language ? { language } : {},
|
|
219
|
+
tenantId != null ? { tenantId } : {}
|
|
220
|
+
);
|
|
221
|
+
const trimmedKeyword = String(keyword || '').trim();
|
|
222
|
+
if (trimmedKeyword) {
|
|
223
|
+
where.name = { [Op.like]: `%${trimmedKeyword}%` };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const rows = await group.models.tag.findAll({ where });
|
|
227
|
+
const plainRows = rows
|
|
228
|
+
.map(item => (typeof item.get === 'function' ? item.get({ plain: true }) : item))
|
|
229
|
+
.sort(sortSiblingNodes);
|
|
230
|
+
const totalCount = plainRows.length;
|
|
231
|
+
const start = (page - 1) * size;
|
|
232
|
+
const pageRows = plainRows.slice(start, start + size);
|
|
233
|
+
const pageData = await enrichNodes(pageRows);
|
|
234
|
+
return { pageData, totalCount };
|
|
166
235
|
};
|
|
167
236
|
|
|
168
237
|
const mkdir = async ({ type, name, parentId, language, tenantId }) => {
|
|
@@ -184,7 +253,7 @@ module.exports = fp(async (fastify, fastifyOptions) => {
|
|
|
184
253
|
return typeof tag.get === 'function' ? tag.get({ plain: true }) : tag;
|
|
185
254
|
};
|
|
186
255
|
|
|
187
|
-
const upload = async ({ type,
|
|
256
|
+
const upload = async ({ type, path: folderPath = '', file, namespace, language, tenantId }) => {
|
|
188
257
|
if (!type) {
|
|
189
258
|
throw new Error('必须传入类型');
|
|
190
259
|
}
|
|
@@ -192,7 +261,8 @@ module.exports = fp(async (fastify, fastifyOptions) => {
|
|
|
192
261
|
throw new Error('不能获取到上传文件');
|
|
193
262
|
}
|
|
194
263
|
|
|
195
|
-
const targetParentId =
|
|
264
|
+
const targetParentId = await ensurePath({ type, path: folderPath, language, tenantId });
|
|
265
|
+
|
|
196
266
|
const uniqueName = await allocateUniqueSiblingName({
|
|
197
267
|
type,
|
|
198
268
|
parentId: targetParentId,
|
|
@@ -640,6 +710,7 @@ module.exports = fp(async (fastify, fastifyOptions) => {
|
|
|
640
710
|
Object.assign(services, {
|
|
641
711
|
folder: {
|
|
642
712
|
getTree,
|
|
713
|
+
getList,
|
|
643
714
|
mkdir,
|
|
644
715
|
upload,
|
|
645
716
|
remove,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kne/fastify-file-manager",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.7",
|
|
4
4
|
"description": "用于管理静态文件上传查看等",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
"sqlite3": "^5.1.7"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@kne/fastify-group": "*",
|
|
49
48
|
"@kne/fastify-namespace": "*",
|
|
50
49
|
"@kne/fastify-sequelize": "*",
|
|
51
50
|
"fastify-plugin": ">=5"
|
|
@@ -53,7 +52,8 @@
|
|
|
53
52
|
"dependencies": {
|
|
54
53
|
"@fastify/multipart": "^9.0.3",
|
|
55
54
|
"@fastify/static": "^8.1.1",
|
|
56
|
-
"
|
|
55
|
+
"@kne/fastify-group": "^0.2.5",
|
|
56
|
+
"compressing": "^2.1.1",
|
|
57
57
|
"fs-extra": "^11.2.0",
|
|
58
58
|
"glob": "^13.0.0",
|
|
59
59
|
"http-errors": "^2.0.0",
|