@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 +8 -4
- package/libs/controllers/index.js +96 -27
- package/libs/models/file-record.js +6 -1
- package/libs/services/file-record.js +112 -25
- package/package.json +6 -2
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: '/
|
|
12
|
+
prefix: `/api/v${packageJson.version.split('.')[0]}/static`,
|
|
12
13
|
dbTableNamePrefix: 't_file_manager_',
|
|
13
14
|
multipart: {},
|
|
14
15
|
static: {},
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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:
|
|
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({
|
|
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:
|
|
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
|
|
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:
|
|
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 {
|
|
65
|
-
id
|
|
66
|
-
namespace: options.namespace
|
|
75
|
+
const { filePath, targetFile, filename, mimetype, ...props } = await services.fileRecord.getFileInfo({
|
|
76
|
+
id
|
|
67
77
|
});
|
|
68
|
-
|
|
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.
|
|
88
|
+
fastify.post(
|
|
73
89
|
`${options.prefix}/file-list`,
|
|
74
90
|
{
|
|
75
|
-
onRequest:
|
|
91
|
+
onRequest: options.createAuthenticate('file:mange'),
|
|
76
92
|
schema: {
|
|
77
|
-
|
|
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(
|
|
82
|
-
|
|
83
|
-
|
|
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}/
|
|
130
|
+
`${options.prefix}/replace-file`,
|
|
96
131
|
{
|
|
97
|
-
onRequest:
|
|
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: ['
|
|
174
|
+
required: ['ids'],
|
|
102
175
|
properties: {
|
|
103
|
-
|
|
176
|
+
ids: { type: 'array', items: { type: 'string' } }
|
|
104
177
|
}
|
|
105
178
|
}
|
|
106
179
|
}
|
|
107
180
|
},
|
|
108
181
|
async request => {
|
|
109
|
-
const {
|
|
110
|
-
await services.fileRecord.
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
83
|
+
const getFileInfo = async ({ id }) => {
|
|
40
84
|
const file = await models.fileRecord.findOne({
|
|
41
|
-
where: { uuid: id
|
|
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
|
-
|
|
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
|
-
|
|
102
|
+
filePath: targetFileName,
|
|
103
|
+
targetFile
|
|
50
104
|
});
|
|
51
105
|
};
|
|
52
106
|
|
|
53
|
-
const getFileList = async ({ filter,
|
|
54
|
-
|
|
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:
|
|
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
|
|
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
|
|
154
|
+
where: { uuid: id }
|
|
69
155
|
});
|
|
70
156
|
if (!file) {
|
|
71
157
|
throw new Error('文件不存在');
|
|
72
158
|
}
|
|
73
|
-
|
|
74
|
-
await file.
|
|
159
|
+
file.filename = filename;
|
|
160
|
+
await file.save();
|
|
75
161
|
};
|
|
76
|
-
|
|
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.
|
|
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
|
}
|