@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 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
  {
@@ -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 }) => {
@@ -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.4",
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
- "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",