@kne/fastify-file-manager 1.1.2 → 1.2.0

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
@@ -1,6 +1,7 @@
1
1
  const fp = require('fastify-plugin');
2
2
  const path = require('path');
3
3
  const fs = require('fs-extra');
4
+ const packageJson = require('./package.json');
4
5
 
5
6
  module.exports = fp(
6
7
  async (fastify, options) => {
@@ -8,13 +9,16 @@ module.exports = fp(
8
9
  {
9
10
  root: path.join(process.cwd(), 'static'),
10
11
  namespace: 'default',
11
- prefix: '/api/static',
12
+ prefix: `/api/v${packageJson.version.split('.')[0]}/static`,
12
13
  dbTableNamePrefix: 't_file_manager_',
13
14
  multipart: {},
14
15
  static: {},
15
- authenticateFileRead: async () => {},
16
- authenticateFileMange: async () => {},
17
- authenticateFileUpload: async () => {}
16
+ ossAdapter: () => {
17
+ return {};
18
+ },
19
+ createAuthenticate: () => {
20
+ return [];
21
+ }
18
22
  },
19
23
  options
20
24
  );
@@ -5,7 +5,15 @@ module.exports = fp(async (fastify, options) => {
5
5
  fastify.post(
6
6
  `${options.prefix}/upload`,
7
7
  {
8
- onRequest: [options.authenticateFileUpload]
8
+ onRequest: options.createAuthenticate('file:write'),
9
+ schema: {
10
+ query: {
11
+ type: 'object',
12
+ properties: {
13
+ namespace: { type: 'string' }
14
+ }
15
+ }
16
+ }
9
17
  },
10
18
  async request => {
11
19
  const file = await request.file();
@@ -13,14 +21,17 @@ module.exports = fp(async (fastify, options) => {
13
21
  throw new Error('不能获取到上传文件');
14
22
  }
15
23
  //1. 保存到服务器目录 2.对接oss
16
- return await services.fileRecord.uploadToFileSystem({ file, namespace: options.namespace });
24
+ return await services.fileRecord.uploadToFileSystem({
25
+ file,
26
+ namespace: request.query.namespace || options.namespace
27
+ });
17
28
  }
18
29
  );
19
30
 
20
31
  fastify.get(
21
32
  `${options.prefix}/file-url/:id`,
22
33
  {
23
- onRequest: [options.authenticateFileRead],
34
+ onRequest: options.createAuthenticate('file:read'),
24
35
  schema: {
25
36
  params: {
26
37
  type: 'object',
@@ -33,14 +44,14 @@ module.exports = fp(async (fastify, options) => {
33
44
  },
34
45
  async request => {
35
46
  const { id } = request.params;
36
- return await services.fileRecord.getFileUrl({ id, namespace: options.namespace });
47
+ return await services.fileRecord.getFileUrl({ id });
37
48
  }
38
49
  );
39
50
 
40
51
  fastify.get(
41
52
  `${options.prefix}/file-id/:id`,
42
53
  {
43
- onRequest: [options.authenticateFileRead],
54
+ onRequest: options.createAuthenticate('file:read'),
44
55
  schema: {
45
56
  query: {
46
57
  type: 'object',
@@ -61,58 +72,116 @@ module.exports = fp(async (fastify, options) => {
61
72
  async (request, reply) => {
62
73
  const { id } = request.params;
63
74
  const { attachment, filename: targetFilename } = request.query;
64
- const { targetFileName, filename } = await services.fileRecord.getFileInfo({
65
- id,
66
- namespace: options.namespace
75
+ const { filePath, targetFile, filename, mimetype, ...props } = await services.fileRecord.getFileInfo({
76
+ id
67
77
  });
68
- return attachment ? reply.download(targetFileName, targetFilename || filename) : reply.sendFile(targetFileName);
78
+ if (targetFile) {
79
+ const outputFilename = encodeURIComponent(targetFilename || filename);
80
+ reply.header('Content-Type', mimetype);
81
+ reply.header('Content-Disposition', attachment ? `attachment; filename="${outputFilename}"` : `filename="${outputFilename}"`);
82
+ return reply.send(targetFile);
83
+ }
84
+ return attachment ? reply.download(filePath, targetFilename || filename) : reply.sendFile(filePath);
69
85
  }
70
86
  );
71
87
 
72
- fastify.get(
88
+ fastify.post(
73
89
  `${options.prefix}/file-list`,
74
90
  {
75
- onRequest: [options.authenticateFileMange],
91
+ onRequest: options.createAuthenticate('file:mange'),
76
92
  schema: {
77
- query: {}
93
+ body: {
94
+ type: 'object',
95
+ properties: {
96
+ perPage: { type: 'number' },
97
+ currentPage: { type: 'number' },
98
+ filter: {
99
+ type: 'object',
100
+ properties: {
101
+ namespace: { type: 'string' },
102
+ size: { type: 'array', items: { type: 'number' } },
103
+ filename: { type: 'string' }
104
+ }
105
+ }
106
+ }
107
+ }
78
108
  }
79
109
  },
80
110
  async request => {
81
- const { filter, perPage, currentPage } = Object.assign({}, request.query, {
82
- perPage: 20,
83
- currentPage: 1
84
- });
111
+ const { filter, perPage, currentPage } = Object.assign(
112
+ {},
113
+ {
114
+ perPage: 20,
115
+ currentPage: 1
116
+ },
117
+ request.body
118
+ );
85
119
  return await services.fileRecord.getFileList({
86
120
  filter,
87
- namespace: options.namespace,
88
121
  perPage,
89
122
  currentPage
90
123
  });
91
124
  }
92
125
  );
93
126
 
127
+ // Replace file
128
+
94
129
  fastify.post(
95
- `${options.prefix}/delete-file`,
130
+ `${options.prefix}/replace-file`,
96
131
  {
97
- onRequest: [options.authenticateFileMange],
132
+ onRequest: options.createAuthenticate('file:mange'),
133
+ schema: {
134
+ type: 'object',
135
+ properties: {
136
+ id: { type: 'string' }
137
+ }
138
+ }
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
+ }
147
+ );
148
+
149
+ fastify.post(
150
+ `${options.prefix}/rename-file`,
151
+ {
152
+ onRequest: options.createAuthenticate('file:mange'),
153
+ schema: {
154
+ type: 'object',
155
+ properties: {
156
+ id: { type: 'string' },
157
+ filename: { type: 'string' }
158
+ }
159
+ }
160
+ },
161
+ async request => {
162
+ await services.fileRecord.renameFile(request.body);
163
+ return {};
164
+ }
165
+ );
166
+
167
+ fastify.post(
168
+ `${options.prefix}/delete-files`,
169
+ {
170
+ onRequest: options.createAuthenticate('file:mange'),
98
171
  schema: {
99
172
  body: {
100
173
  type: 'object',
101
- required: ['id'],
174
+ required: ['ids'],
102
175
  properties: {
103
- id: { type: 'string' }
176
+ ids: { type: 'array', items: { type: 'string' } }
104
177
  }
105
178
  }
106
179
  }
107
180
  },
108
181
  async request => {
109
- const { id } = request.body;
110
- await services.fileRecord.deleteFile({ id, namespace: options.namespace });
182
+ const { ids } = request.body;
183
+ await services.fileRecord.deleteFiles({ ids });
111
184
  return {};
112
185
  }
113
186
  );
114
-
115
- fastify.get(`${options.prefix}`, async () => {
116
- return 'living';
117
- });
118
187
  });
@@ -22,7 +22,12 @@ module.exports = ({ DataTypes }) => {
22
22
  allowNull: false
23
23
  },
24
24
  encoding: DataTypes.STRING,
25
- mimetype: DataTypes.STRING
25
+ mimetype: DataTypes.STRING,
26
+ storageType: {
27
+ type: DataTypes.STRING,
28
+ allowNull: false,
29
+ comment: '存储类型:local本地文件系统,oss远程oss存储'
30
+ }
26
31
  },
27
32
  options: {
28
33
  indexes: [
@@ -2,59 +2,135 @@ const fp = require('fastify-plugin');
2
2
  const fs = require('fs-extra');
3
3
  const crypto = require('crypto');
4
4
  const path = require('path');
5
+ const { NotFound } = require('http-errors');
5
6
 
6
7
  module.exports = fp(async (fastify, options) => {
7
8
  const { models, services } = fastify.fileManager;
8
- const uploadToFileSystem = async ({ file, namespace }) => {
9
+ const { Op } = fastify.sequelize.Sequelize;
10
+ const uploadToFileSystem = async ({ id, file, namespace }) => {
9
11
  const { filename, encoding, mimetype } = file;
10
12
  const buffer = await file.toBuffer();
11
13
  const hash = crypto.createHash('md5');
12
14
  hash.update(buffer);
13
15
  const digest = hash.digest('hex');
14
16
  const extension = path.extname(filename);
15
- const filepath = path.resolve(options.root, `${digest}${extension}`);
16
- await fs.writeFile(filepath, buffer);
17
- const outputFile = await models.fileRecord.create({
18
- filename,
19
- namespace: namespace || options.namespace,
20
- encoding,
21
- mimetype,
22
- hash: digest,
23
- size: buffer.byteLength
24
- });
17
+
18
+ let storageType;
19
+ const ossServices = options.ossAdapter();
20
+ if (typeof ossServices.uploadFile === 'function') {
21
+ await ossServices.uploadFile({ file: buffer, filename: `${digest}${extension}` });
22
+ storageType = 'oss';
23
+ } else {
24
+ const filepath = path.resolve(options.root, `${digest}${extension}`);
25
+ await fs.writeFile(filepath, buffer);
26
+ storageType = 'local';
27
+ }
28
+
29
+ const outputFile = await (async create => {
30
+ if (!id) {
31
+ return await create();
32
+ }
33
+ const file = await models.fileRecord.findOne({ where: { uuid: id } });
34
+ if (!file) {
35
+ throw new Error('原文件不存在');
36
+ }
37
+ file.filename = filename;
38
+ file.encoding = encoding;
39
+ file.mimetype = mimetype;
40
+ file.hash = digest;
41
+ file.size = buffer.byteLength;
42
+ file.storageType = storageType;
43
+ await file.save();
44
+ return file;
45
+ })(() =>
46
+ models.fileRecord.create({
47
+ filename,
48
+ namespace: namespace || options.namespace,
49
+ encoding,
50
+ mimetype,
51
+ hash: digest,
52
+ size: buffer.byteLength,
53
+ storageType
54
+ })
55
+ );
25
56
  return Object.assign({}, outputFile.get({ plain: true }), { id: outputFile.uuid });
26
57
  };
27
58
 
28
59
  const getFileUrl = async ({ id, namespace }) => {
29
60
  const file = await models.fileRecord.findOne({
30
- where: { uuid: id, namespace: namespace || options.namespace }
61
+ where: { uuid: id }
31
62
  });
32
63
  if (!file) {
33
64
  throw new Error('文件不存在');
34
65
  }
35
66
  const extension = path.extname(file.filename);
36
- return `${options.prefix}/file/${file.hash}${extension}?filename=${file.filename}`;
67
+ const ossServices = options.ossAdapter();
68
+ if (file.storageType === 'oss' && typeof ossServices.getFileLink !== 'function') {
69
+ throw new Error('ossAdapter未正确配置无法读取oss类型存储文件');
70
+ }
71
+ if (file.storageType === 'oss') {
72
+ return await ossServices.getFileLink({ filename: `${file.hash}${extension}` });
73
+ }
74
+
75
+ const localPath = `${options.prefix}/file/${file.hash}${extension}?filename=${file.filename}`;
76
+
77
+ if (!(await fs.exists(localPath))) {
78
+ throw new NotFound();
79
+ }
80
+ return localPath;
37
81
  };
38
82
 
39
- const getFileInfo = async ({ id, namespace }) => {
83
+ const getFileInfo = async ({ id }) => {
40
84
  const file = await models.fileRecord.findOne({
41
- where: { uuid: id, namespace: namespace || options.namespace }
85
+ where: { uuid: id }
42
86
  });
43
87
  if (!file) {
44
88
  throw new Error('文件不存在');
45
89
  }
46
90
  const extension = path.extname(file.filename);
47
- return Object.assign({}, file, {
91
+ const targetFileName = `${file.hash}${extension}`;
92
+ const ossServices = options.ossAdapter();
93
+ if (file.storageType === 'oss' && typeof ossServices.downloadFile !== 'function') {
94
+ throw new Error('ossAdapter未正确配置无法读取oss类型存储文件');
95
+ }
96
+ let targetFile;
97
+ if (file.storageType === 'oss') {
98
+ targetFile = await ossServices.downloadFile({ filename: targetFileName });
99
+ }
100
+ return Object.assign({}, file.get({ pain: true }), {
48
101
  id: file.uuid,
49
- targetFileName: `${file.hash}${extension}`
102
+ filePath: targetFileName,
103
+ targetFile
50
104
  });
51
105
  };
52
106
 
53
- const getFileList = async ({ filter, namespace, currentPage, perPage }) => {
54
- const queryFilter = { namespace: namespace || options.namespace };
107
+ const getFileList = async ({ filter, currentPage, perPage }) => {
108
+ // namespace: namespace || options.namespace
109
+ const queryFilter = {};
110
+
111
+ if (filter?.filename) {
112
+ queryFilter.filename = {
113
+ [Op.like]: `%${filter.filename}%`
114
+ };
115
+ }
116
+ if (filter?.size && filter.size.length > 0) {
117
+ queryFilter.size = {};
118
+ if (filter.size[0]) {
119
+ queryFilter.size[Op.gt] = filter.size[0] * 1024;
120
+ }
121
+ if (filter.size[1]) {
122
+ queryFilter.size[Op.lt] = filter.size[1] * 1024;
123
+ }
124
+ }
125
+ if (filter?.namespace) {
126
+ queryFilter.namespace = {
127
+ [Op.like]: `%${filter.namespace}%`
128
+ };
129
+ }
130
+
55
131
  const { count, rows } = await models.fileRecord.findAndCountAll({
56
132
  where: queryFilter,
57
- offset: currentPage * (currentPage - 1),
133
+ offset: perPage * (currentPage - 1),
58
134
  limit: perPage
59
135
  });
60
136
  return {
@@ -63,15 +139,26 @@ module.exports = fp(async (fastify, options) => {
63
139
  };
64
140
  };
65
141
 
66
- const deleteFile = async ({ id, namespace }) => {
142
+ const deleteFiles = async ({ ids }) => {
143
+ await models.fileRecord.destroy({
144
+ where: {
145
+ uuid: {
146
+ [Op.in]: ids
147
+ }
148
+ }
149
+ });
150
+ };
151
+
152
+ const renameFile = async ({ id, filename }) => {
67
153
  const file = await models.fileRecord.findOne({
68
- where: { uuid: id, namespace: namespace || options.namespace }
154
+ where: { uuid: id }
69
155
  });
70
156
  if (!file) {
71
157
  throw new Error('文件不存在');
72
158
  }
73
-
74
- await file.destroy();
159
+ file.filename = filename;
160
+ await file.save();
75
161
  };
76
- services.fileRecord = { uploadToFileSystem, getFileUrl, getFileInfo, getFileList, deleteFile };
162
+
163
+ services.fileRecord = { uploadToFileSystem, getFileUrl, getFileInfo, getFileList, deleteFiles, renameFile };
77
164
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kne/fastify-file-manager",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "用于管理静态文件上传查看等",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -32,10 +32,13 @@
32
32
  },
33
33
  "homepage": "https://github.com/kne-union/fastify-file-manager#readme",
34
34
  "devDependencies": {
35
+ "@fastify/env": "^4.4.0",
36
+ "@kne/fastify-aliyun": "^1.1.1",
35
37
  "@kne/fastify-sequelize": "^2.0.1",
36
38
  "fastify": "^4.27.0",
37
39
  "husky": "^9.0.11",
38
40
  "prettier": "^3.2.5",
41
+ "qs": "^6.12.3",
39
42
  "sqlite3": "^5.1.7"
40
43
  },
41
44
  "dependencies": {
@@ -43,6 +46,7 @@
43
46
  "@fastify/static": "^7.0.4",
44
47
  "@kne/fastify-namespace": "^0.1.0",
45
48
  "fastify-plugin": "^4.5.1",
46
- "fs-extra": "^11.2.0"
49
+ "fs-extra": "^11.2.0",
50
+ "http-errors": "^2.0.0"
47
51
  }
48
52
  }