@kne/fastify-file-manager 3.0.4 → 3.0.6
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 +43 -2
- package/libs/services/folder.js +102 -32
- 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
|
{
|
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 }) => {
|
|
@@ -640,6 +709,7 @@ module.exports = fp(async (fastify, fastifyOptions) => {
|
|
|
640
709
|
Object.assign(services, {
|
|
641
710
|
folder: {
|
|
642
711
|
getTree,
|
|
712
|
+
getList,
|
|
643
713
|
mkdir,
|
|
644
714
|
upload,
|
|
645
715
|
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.6",
|
|
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",
|