@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 CHANGED
@@ -23,9 +23,9 @@ module.exports = fp(
23
23
  createAuthenticate: () => {
24
24
  return [];
25
25
  },
26
- // 文件夹能力依赖 @kne/fastify-group,需先注册;设为 false 可关闭
26
+ // 文件夹能力;插件内部自行注册 @kne/fastify-group,无需宿主再挂一份
27
27
  enableFolder: true,
28
- groupName: 'group',
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: '基于 fastify-group 获取指定 type 的文件树;未传 type 时使用 defaultFolderType',
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
- parentId: { type: 'string', description: '父文件夹 id' },
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
- parentId: request.query.parentId,
465
+ path: request.query.path,
425
466
  namespace: request.query.namespace,
426
467
  language: request.query.language,
427
468
  file
@@ -5,10 +5,10 @@ module.exports = fp(async (fastify, fastifyOptions) => {
5
5
  return;
6
6
  }
7
7
 
8
- const groupName = fastifyOptions.groupName || 'group';
8
+ const groupName = fastifyOptions.groupName || 'file-manager-folder';
9
9
  const group = fastify[groupName];
10
10
  if (!group?.services) {
11
- throw new Error(`文件夹功能需要先注册 @kne/fastify-group(fastify.${groupName})`);
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 enrichTree = async tree => {
113
- const fileIds = collectFileIds(tree);
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
- const mapNode = node => {
132
- const plain = { ...node };
133
- const kind = getNodeKind(plain);
134
- plain.options = Object.assign({}, plain.options, { kind });
135
- if (kind === 'file' && plain.options.fileId) {
136
- const file = fileMap.get(String(plain.options.fileId));
137
- if (file) {
138
- plain.options = Object.assign({}, plain.options, {
139
- size: file.size,
140
- mimetype: file.mimetype,
141
- filename: file.filename
142
- });
143
- if (file.createdAt) {
144
- plain.createdAt = file.createdAt;
145
- }
146
- if (file.updatedAt) {
147
- plain.updatedAt = file.updatedAt;
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
- if (plain.children?.length) {
152
- plain.children = plain.children.map(mapNode);
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 getTree = async ({ type, language, tenantId }) => {
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
- return enrichTree(tree);
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, parentId, file, namespace, language, tenantId }) => {
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 = parentId || null;
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.4",
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
- "compressing": "^2.0.0",
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",