@nocobase/plugin-file-manager 2.0.57 → 2.0.59
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/dist/client/867ada653cd02a3e.mjs +6 -0
- package/dist/client/index.js +1 -1
- package/dist/client/previewer/filePreviewTypes.d.ts +3 -0
- package/dist/externalVersion.js +8 -8
- package/dist/locale/de-DE.json +3 -0
- package/dist/locale/en-US.json +3 -0
- package/dist/locale/es-ES.json +3 -0
- package/dist/locale/fr-FR.json +3 -0
- package/dist/locale/hu-HU.json +4 -1
- package/dist/locale/id-ID.json +4 -1
- package/dist/locale/it-IT.json +3 -0
- package/dist/locale/ja-JP.json +3 -0
- package/dist/locale/ko-KR.json +3 -0
- package/dist/locale/nl-NL.json +3 -0
- package/dist/locale/pt-BR.json +3 -0
- package/dist/locale/ru-RU.json +3 -0
- package/dist/locale/tr-TR.json +3 -0
- package/dist/locale/uk-UA.json +3 -0
- package/dist/locale/vi-VN.json +4 -1
- package/dist/locale/zh-CN.json +3 -0
- package/dist/locale/zh-TW.json +3 -0
- package/dist/node_modules/@aws-sdk/client-s3/package.json +1 -1
- package/dist/node_modules/@aws-sdk/lib-storage/package.json +1 -1
- package/dist/node_modules/ali-oss/package.json +1 -1
- package/dist/node_modules/cos-nodejs-sdk-v5/package.json +1 -1
- package/dist/node_modules/mime-match/package.json +1 -1
- package/dist/node_modules/mime-types/package.json +1 -1
- package/dist/node_modules/mkdirp/package.json +1 -1
- package/dist/node_modules/url-join/package.json +1 -1
- package/dist/server/commands/repair-filenames.d.ts +55 -0
- package/dist/server/commands/repair-filenames.js +283 -0
- package/dist/server/server.d.ts +1 -0
- package/dist/server/server.js +4 -0
- package/dist/server/storages/ali-oss.d.ts +3 -1
- package/dist/server/storages/ali-oss.js +23 -2
- package/dist/server/storages/index.d.ts +3 -0
- package/dist/server/storages/index.js +6 -0
- package/dist/server/storages/local.d.ts +2 -0
- package/dist/server/storages/local.js +18 -0
- package/dist/server/storages/s3.d.ts +2 -0
- package/dist/server/storages/s3.js +26 -0
- package/dist/server/storages/tx-cos.d.ts +2 -0
- package/dist/server/storages/tx-cos.js +27 -0
- package/dist/server/utils.js +12 -2
- package/package.json +5 -3
- package/dist/client/301.4bc6687c4a9cf8e0.js +0 -15
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var repair_filenames_exports = {};
|
|
28
|
+
__export(repair_filenames_exports, {
|
|
29
|
+
getRepairedAttachmentValues: () => getRepairedAttachmentValues,
|
|
30
|
+
registerRepairFilenamesCommand: () => registerRepairFilenamesCommand,
|
|
31
|
+
repairAttachmentFilenames: () => repairAttachmentFilenames,
|
|
32
|
+
replaceInvisibleChars: () => replaceInvisibleChars
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(repair_filenames_exports);
|
|
35
|
+
var import_sequelize = require("sequelize");
|
|
36
|
+
var import_utils = require("../utils");
|
|
37
|
+
function containsInvisibleChars(value) {
|
|
38
|
+
return Array.from(value || "").some((char) => {
|
|
39
|
+
const code = char.charCodeAt(0);
|
|
40
|
+
return code <= 31 || code >= 127 && code <= 159;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function replaceInvisibleChars(value) {
|
|
44
|
+
return Array.from(value || "").map((char) => {
|
|
45
|
+
const code = char.charCodeAt(0);
|
|
46
|
+
return code <= 31 || code >= 127 && code <= 159 ? "-" : char;
|
|
47
|
+
}).join("");
|
|
48
|
+
}
|
|
49
|
+
function getRepairedAttachmentValues(record) {
|
|
50
|
+
return {
|
|
51
|
+
path: replaceInvisibleChars(record.path),
|
|
52
|
+
filename: replaceInvisibleChars(record.filename)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function updateUrlValue(url, oldKey, newKey) {
|
|
56
|
+
if (!url) {
|
|
57
|
+
return url;
|
|
58
|
+
}
|
|
59
|
+
const encodedOldKey = oldKey.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
60
|
+
const encodedNewKey = newKey.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
61
|
+
return url.split(oldKey).join(newKey).split(encodedOldKey).join(encodedNewKey);
|
|
62
|
+
}
|
|
63
|
+
function formatItem(item) {
|
|
64
|
+
return {
|
|
65
|
+
collectionName: item.collectionName,
|
|
66
|
+
id: item.id,
|
|
67
|
+
storageId: item.storageId,
|
|
68
|
+
storageType: item.storageType,
|
|
69
|
+
oldKey: item.oldKey,
|
|
70
|
+
newKey: item.newKey,
|
|
71
|
+
sourceExists: item.sourceExists,
|
|
72
|
+
targetExists: item.targetExists,
|
|
73
|
+
status: item.status,
|
|
74
|
+
reason: item.reason
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function getFileCollectionNames(app) {
|
|
78
|
+
var _a;
|
|
79
|
+
const repository = app.db.getRepository("collections") || ((_a = app.db.getCollection("collections")) == null ? void 0 : _a.repository);
|
|
80
|
+
if (repository) {
|
|
81
|
+
const collections = await repository.find({
|
|
82
|
+
filter: {
|
|
83
|
+
"options.template": "file"
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return Array.from(/* @__PURE__ */ new Set(["attachments", ...collections.map((collection) => collection.get("name"))]));
|
|
87
|
+
}
|
|
88
|
+
return Array.from(
|
|
89
|
+
/* @__PURE__ */ new Set([
|
|
90
|
+
"attachments",
|
|
91
|
+
...Array.from(app.db.collections.values()).filter((collection) => collection.options.template === "file").map((collection) => collection.name)
|
|
92
|
+
])
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
async function loadFileCollections(app) {
|
|
96
|
+
var _a, _b;
|
|
97
|
+
const collection = app.db.getCollection("collections");
|
|
98
|
+
if (!collection) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const repository = collection.repository;
|
|
102
|
+
await ((_a = repository.setApp) == null ? void 0 : _a.call(repository, app));
|
|
103
|
+
await ((_b = repository.load) == null ? void 0 : _b.call(repository));
|
|
104
|
+
}
|
|
105
|
+
async function repairCollectionAttachmentFilenames({
|
|
106
|
+
app,
|
|
107
|
+
fileManager,
|
|
108
|
+
collectionName,
|
|
109
|
+
result,
|
|
110
|
+
apply,
|
|
111
|
+
batchSize,
|
|
112
|
+
limit
|
|
113
|
+
}) {
|
|
114
|
+
const collection = app.db.getCollection(collectionName);
|
|
115
|
+
if (!collection) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const Model = collection.model;
|
|
119
|
+
let lastId = null;
|
|
120
|
+
while (result.scanned < limit) {
|
|
121
|
+
const rows = await Model.findAll({
|
|
122
|
+
attributes: ["id", "path", "filename", "storageId", "url"],
|
|
123
|
+
where: lastId ? { id: { [import_sequelize.Op.gt]: lastId } } : void 0,
|
|
124
|
+
order: [["id", "ASC"]],
|
|
125
|
+
limit: Math.min(batchSize, limit - result.scanned)
|
|
126
|
+
});
|
|
127
|
+
if (!rows.length) {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
for (const row of rows) {
|
|
131
|
+
result.scanned += 1;
|
|
132
|
+
lastId = row.get("id");
|
|
133
|
+
const record = row.toJSON();
|
|
134
|
+
if (!containsInvisibleChars(record.path) && !containsInvisibleChars(record.filename)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const storage = fileManager.storagesCache.get(record.storageId);
|
|
138
|
+
const { path: newPath, filename: newFilename } = getRepairedAttachmentValues(record);
|
|
139
|
+
const oldKey = (0, import_utils.getFileKey)(record);
|
|
140
|
+
const newRecord = { ...record, path: newPath, filename: newFilename };
|
|
141
|
+
const newKey = (0, import_utils.getFileKey)(newRecord);
|
|
142
|
+
const item = {
|
|
143
|
+
collectionName,
|
|
144
|
+
id: lastId,
|
|
145
|
+
storageId: record.storageId,
|
|
146
|
+
storageType: (storage == null ? void 0 : storage.type) || "",
|
|
147
|
+
oldPath: record.path || "",
|
|
148
|
+
oldFilename: record.filename,
|
|
149
|
+
newPath,
|
|
150
|
+
newFilename,
|
|
151
|
+
oldKey,
|
|
152
|
+
newKey,
|
|
153
|
+
status: "pending"
|
|
154
|
+
};
|
|
155
|
+
result.candidates.push(item);
|
|
156
|
+
if (!storage) {
|
|
157
|
+
item.status = "skipped";
|
|
158
|
+
item.reason = "storage_not_found";
|
|
159
|
+
result.skipped.push(item);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (oldKey === newKey) {
|
|
163
|
+
item.status = "skipped";
|
|
164
|
+
item.reason = "unchanged";
|
|
165
|
+
result.skipped.push(item);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const StorageClass = fileManager.storageTypes.get(storage.type);
|
|
170
|
+
if (!StorageClass) {
|
|
171
|
+
item.status = "skipped";
|
|
172
|
+
item.reason = "storage_type_not_found";
|
|
173
|
+
result.skipped.push(item);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const storageInstance = new StorageClass(storage);
|
|
177
|
+
item.sourceExists = await storageInstance.exists(record);
|
|
178
|
+
if (!item.sourceExists) {
|
|
179
|
+
item.status = "skipped";
|
|
180
|
+
item.reason = "source_not_found";
|
|
181
|
+
result.skipped.push(item);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
item.targetExists = await storageInstance.exists(newRecord);
|
|
185
|
+
if (item.targetExists) {
|
|
186
|
+
item.status = "skipped";
|
|
187
|
+
item.reason = "target_exists";
|
|
188
|
+
result.skipped.push(item);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (apply) {
|
|
192
|
+
await storageInstance.copy(record, newRecord);
|
|
193
|
+
await row.update({
|
|
194
|
+
path: newPath,
|
|
195
|
+
filename: newFilename,
|
|
196
|
+
url: updateUrlValue(record.url, oldKey, newKey)
|
|
197
|
+
});
|
|
198
|
+
try {
|
|
199
|
+
const [deleted] = await storageInstance.delete([record]);
|
|
200
|
+
if (!deleted) {
|
|
201
|
+
item.reason = "delete_old_failed";
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
item.reason = `delete_old_failed: ${error.message}`;
|
|
205
|
+
}
|
|
206
|
+
item.status = "repaired";
|
|
207
|
+
result.repaired.push(item);
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
item.status = "failed";
|
|
211
|
+
item.reason = error.message;
|
|
212
|
+
result.failed.push(item);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function repairAttachmentFilenames(app, fileManager, options = {}) {
|
|
218
|
+
const apply = !!options.apply;
|
|
219
|
+
const batchSize = options.batchSize || 500;
|
|
220
|
+
const limit = options.limit || Number.POSITIVE_INFINITY;
|
|
221
|
+
const result = {
|
|
222
|
+
dryRun: !apply,
|
|
223
|
+
scanned: 0,
|
|
224
|
+
candidates: [],
|
|
225
|
+
repaired: [],
|
|
226
|
+
skipped: [],
|
|
227
|
+
failed: []
|
|
228
|
+
};
|
|
229
|
+
if (!fileManager.storagesCache.size) {
|
|
230
|
+
await fileManager.loadStorages();
|
|
231
|
+
}
|
|
232
|
+
await loadFileCollections(app);
|
|
233
|
+
for (const collectionName of await getFileCollectionNames(app)) {
|
|
234
|
+
if (result.scanned >= limit) {
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
await repairCollectionAttachmentFilenames({
|
|
238
|
+
app,
|
|
239
|
+
fileManager,
|
|
240
|
+
collectionName,
|
|
241
|
+
result,
|
|
242
|
+
apply,
|
|
243
|
+
batchSize,
|
|
244
|
+
limit
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
function registerRepairFilenamesCommand(app) {
|
|
250
|
+
const command = app.findCommand("file-manager") || app.command("file-manager");
|
|
251
|
+
command.command("repair-filenames").preload().option("--apply", "rename objects and update attachment records").option("--batch-size [batchSize]", "batch size for scanning attachments").option("--limit [limit]", "maximum number of attachment records to scan").action(async (options) => {
|
|
252
|
+
const fileManager = app.pm.get("file-manager");
|
|
253
|
+
const result = await repairAttachmentFilenames(app, fileManager, {
|
|
254
|
+
apply: !!options.apply,
|
|
255
|
+
batchSize: options.batchSize ? Number(options.batchSize) : void 0,
|
|
256
|
+
limit: options.limit ? Number(options.limit) : void 0
|
|
257
|
+
});
|
|
258
|
+
console.log(
|
|
259
|
+
JSON.stringify(
|
|
260
|
+
{
|
|
261
|
+
dryRun: result.dryRun,
|
|
262
|
+
scanned: result.scanned,
|
|
263
|
+
candidates: result.candidates.length,
|
|
264
|
+
repaired: result.repaired.length,
|
|
265
|
+
skipped: result.skipped.length,
|
|
266
|
+
failed: result.failed.length
|
|
267
|
+
},
|
|
268
|
+
null,
|
|
269
|
+
2
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
if (result.candidates.length) {
|
|
273
|
+
console.table(result.candidates.map(formatItem));
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
278
|
+
0 && (module.exports = {
|
|
279
|
+
getRepairedAttachmentValues,
|
|
280
|
+
registerRepairFilenamesCommand,
|
|
281
|
+
repairAttachmentFilenames,
|
|
282
|
+
replaceInvisibleChars
|
|
283
|
+
});
|
package/dist/server/server.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type UploadFileOptions = {
|
|
|
27
27
|
export declare class PluginFileManagerServer extends Plugin {
|
|
28
28
|
storageTypes: Registry<StorageClassType>;
|
|
29
29
|
storagesCache: Map<string | number, StorageModel>;
|
|
30
|
+
static staticImport(): Promise<void>;
|
|
30
31
|
afterDestroy: (record: Model, options: any) => Promise<void>;
|
|
31
32
|
registerStorageType(type: string, Type: StorageClassType): void;
|
|
32
33
|
createFileRecord(options: FileRecordOptions): Promise<any>;
|
package/dist/server/server.js
CHANGED
|
@@ -54,6 +54,7 @@ var import_local = __toESM(require("./storages/local"));
|
|
|
54
54
|
var import_s3 = __toESM(require("./storages/s3"));
|
|
55
55
|
var import_tx_cos = __toESM(require("./storages/tx-cos"));
|
|
56
56
|
var import_utils2 = require("./utils");
|
|
57
|
+
var import_repair_filenames = require("./commands/repair-filenames");
|
|
57
58
|
const DEFAULT_STORAGE_TYPE = import_constants.STORAGE_TYPE_LOCAL;
|
|
58
59
|
class FileDeleteError extends Error {
|
|
59
60
|
data;
|
|
@@ -66,6 +67,9 @@ class FileDeleteError extends Error {
|
|
|
66
67
|
class PluginFileManagerServer extends import_server.Plugin {
|
|
67
68
|
storageTypes = new import_utils.Registry();
|
|
68
69
|
storagesCache = /* @__PURE__ */ new Map();
|
|
70
|
+
static async staticImport() {
|
|
71
|
+
import_server.Application.addCommand(import_repair_filenames.registerRepairFilenamesCommand);
|
|
72
|
+
}
|
|
69
73
|
afterDestroy = async (record, options) => {
|
|
70
74
|
var _a;
|
|
71
75
|
const { collection } = record.constructor;
|
|
@@ -18,7 +18,7 @@ declare class AliYunOssStorage {
|
|
|
18
18
|
filename?: typeof getRandomFilename;
|
|
19
19
|
});
|
|
20
20
|
_handleFile(req: any, file: any, cb: any): any;
|
|
21
|
-
_removeFile(req: any, file: any, cb: any): any
|
|
21
|
+
_removeFile(req: any, file: any, cb: any): Promise<any>;
|
|
22
22
|
}
|
|
23
23
|
export default class extends StorageType {
|
|
24
24
|
static defaults(): {
|
|
@@ -34,6 +34,8 @@ export default class extends StorageType {
|
|
|
34
34
|
};
|
|
35
35
|
};
|
|
36
36
|
make(): AliYunOssStorage;
|
|
37
|
+
exists(record: AttachmentModel): Promise<boolean>;
|
|
38
|
+
copy(source: AttachmentModel, target: AttachmentModel): Promise<void>;
|
|
37
39
|
delete(records: AttachmentModel[]): Promise<[number, AttachmentModel[]]>;
|
|
38
40
|
}
|
|
39
41
|
export {};
|
|
@@ -90,11 +90,16 @@ class AliYunOssStorage {
|
|
|
90
90
|
});
|
|
91
91
|
}).catch(cb);
|
|
92
92
|
}
|
|
93
|
-
_removeFile(req, file, cb) {
|
|
93
|
+
async _removeFile(req, file, cb) {
|
|
94
94
|
if (!this.client) {
|
|
95
95
|
return cb(ERROR_NO_CLIENT);
|
|
96
96
|
}
|
|
97
|
-
|
|
97
|
+
try {
|
|
98
|
+
const result = await this.client.delete(file.filename);
|
|
99
|
+
cb(null, result);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
cb(error);
|
|
102
|
+
}
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
class ali_oss_default extends import__.StorageType {
|
|
@@ -118,6 +123,22 @@ class ali_oss_default extends import__.StorageType {
|
|
|
118
123
|
filename: (0, import_utils.cloudFilenameGetter)(this.storage)
|
|
119
124
|
});
|
|
120
125
|
}
|
|
126
|
+
async exists(record) {
|
|
127
|
+
const { client } = this.make();
|
|
128
|
+
try {
|
|
129
|
+
await client.head((0, import_utils.getFileKey)(record));
|
|
130
|
+
return true;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (["NoSuchKey", "NotFoundError"].includes(error.name)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async copy(source, target) {
|
|
139
|
+
const { client } = this.make();
|
|
140
|
+
await client.copy((0, import_utils.getFileKey)(target), (0, import_utils.getFileKey)(source));
|
|
141
|
+
}
|
|
121
142
|
async delete(records) {
|
|
122
143
|
const { client } = this.make();
|
|
123
144
|
const { deleted } = await client.deleteMulti(records.map(import_utils.getFileKey));
|
|
@@ -24,6 +24,7 @@ export interface StorageModel {
|
|
|
24
24
|
settings?: Record<string, any>;
|
|
25
25
|
}
|
|
26
26
|
export interface AttachmentModel {
|
|
27
|
+
id?: number;
|
|
27
28
|
title: string;
|
|
28
29
|
filename: string;
|
|
29
30
|
mimetype?: string;
|
|
@@ -38,6 +39,8 @@ export declare abstract class StorageType {
|
|
|
38
39
|
constructor(storage: StorageModel);
|
|
39
40
|
abstract make(): StorageEngine;
|
|
40
41
|
abstract delete(records: AttachmentModel[]): [number, AttachmentModel[]] | Promise<[number, AttachmentModel[]]>;
|
|
42
|
+
exists(record: AttachmentModel): Promise<boolean>;
|
|
43
|
+
copy(source: AttachmentModel, target: AttachmentModel): Promise<void>;
|
|
41
44
|
getFileKey(record: AttachmentModel): any;
|
|
42
45
|
getFileData(file: any, meta?: {}): {
|
|
43
46
|
title: string;
|
|
@@ -52,6 +52,12 @@ class StorageType {
|
|
|
52
52
|
return {};
|
|
53
53
|
}
|
|
54
54
|
static filenameKey;
|
|
55
|
+
async exists(record) {
|
|
56
|
+
throw new Error(`Storage type "${this.storage.type}" does not support object existence checks`);
|
|
57
|
+
}
|
|
58
|
+
async copy(source, target) {
|
|
59
|
+
throw new Error(`Storage type "${this.storage.type}" does not support object copy`);
|
|
60
|
+
}
|
|
55
61
|
getFileKey(record) {
|
|
56
62
|
return (0, import_utils2.getFileKey)(record);
|
|
57
63
|
}
|
|
@@ -27,6 +27,8 @@ export default class extends StorageType {
|
|
|
27
27
|
};
|
|
28
28
|
};
|
|
29
29
|
make(): multer.StorageEngine;
|
|
30
|
+
exists(record: AttachmentModel): Promise<boolean>;
|
|
31
|
+
copy(source: AttachmentModel, target: AttachmentModel): Promise<void>;
|
|
30
32
|
delete(records: AttachmentModel[]): Promise<[number, AttachmentModel[]]>;
|
|
31
33
|
getFileURL(file: AttachmentModel, preview?: boolean): Promise<any>;
|
|
32
34
|
getFileStream(file: AttachmentModel): Promise<{
|
|
@@ -92,6 +92,24 @@ class local_default extends import__.StorageType {
|
|
|
92
92
|
filename: (0, import_utils2.diskFilenameGetter)(this.storage)
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
|
+
async exists(record) {
|
|
96
|
+
try {
|
|
97
|
+
await import_promises.default.stat(resolveSafePath(getDocumentRoot(this.storage), record.path, record.filename));
|
|
98
|
+
return true;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error.code === "ENOENT") {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async copy(source, target) {
|
|
107
|
+
const documentRoot = getDocumentRoot(this.storage);
|
|
108
|
+
const sourcePath = resolveSafePath(documentRoot, source.path, source.filename);
|
|
109
|
+
const targetPath = resolveSafePath(documentRoot, target.path, target.filename);
|
|
110
|
+
await import_promises.default.mkdir(import_path.default.dirname(targetPath), { recursive: true });
|
|
111
|
+
await import_promises.default.copyFile(sourcePath, targetPath);
|
|
112
|
+
}
|
|
95
113
|
async delete(records) {
|
|
96
114
|
const documentRoot = getDocumentRoot(this.storage);
|
|
97
115
|
let count = 0;
|
|
@@ -32,5 +32,7 @@ export default class extends StorageType {
|
|
|
32
32
|
deleteS3Objects(bucketName: string, objects: string[]): Promise<{
|
|
33
33
|
Deleted: any[];
|
|
34
34
|
}>;
|
|
35
|
+
exists(record: AttachmentModel): Promise<boolean>;
|
|
36
|
+
copy(source: AttachmentModel, target: AttachmentModel): Promise<void>;
|
|
35
37
|
delete(records: AttachmentModel[]): Promise<[number, AttachmentModel[]]>;
|
|
36
38
|
}
|
|
@@ -173,6 +173,32 @@ class s3_default extends import__.StorageType {
|
|
|
173
173
|
Deleted
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
|
+
async exists(record) {
|
|
177
|
+
try {
|
|
178
|
+
await this.client.send(
|
|
179
|
+
new import_client_s3.HeadObjectCommand({
|
|
180
|
+
Bucket: this.storage.options.bucket,
|
|
181
|
+
Key: this.getFileKey(record)
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
return true;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (["NotFound", "NoSuchKey", "NoSuchBucket"].includes(error.name)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async copy(source, target) {
|
|
193
|
+
const sourceKey = this.getFileKey(source);
|
|
194
|
+
await this.client.send(
|
|
195
|
+
new import_client_s3.CopyObjectCommand({
|
|
196
|
+
Bucket: this.storage.options.bucket,
|
|
197
|
+
Key: this.getFileKey(target),
|
|
198
|
+
CopySource: `${this.storage.options.bucket}/${sourceKey.split("/").map((segment) => encodeURIComponent(segment)).join("/")}`
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
}
|
|
176
202
|
async delete(records) {
|
|
177
203
|
const { Deleted } = await this.deleteS3Objects(
|
|
178
204
|
this.storage.options.bucket,
|
|
@@ -36,6 +36,8 @@ export default class extends StorageType {
|
|
|
36
36
|
};
|
|
37
37
|
static filenameKey: string;
|
|
38
38
|
make(): TxCosStorage;
|
|
39
|
+
exists(record: AttachmentModel): Promise<boolean>;
|
|
40
|
+
copy(source: AttachmentModel, target: AttachmentModel): Promise<void>;
|
|
39
41
|
delete(records: AttachmentModel[]): Promise<[number, AttachmentModel[]]>;
|
|
40
42
|
}
|
|
41
43
|
export {};
|
|
@@ -160,6 +160,33 @@ class tx_cos_default extends import__.StorageType {
|
|
|
160
160
|
filename: (0, import_utils.cloudFilenameGetter)(this.storage)
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
|
+
async exists(record) {
|
|
164
|
+
const { cos } = this.make();
|
|
165
|
+
try {
|
|
166
|
+
await (0, import_util.promisify)(cos.headObject).call(cos, {
|
|
167
|
+
Region: this.storage.options.Region,
|
|
168
|
+
Bucket: this.storage.options.Bucket,
|
|
169
|
+
Key: (0, import_utils.getFileKey)(record)
|
|
170
|
+
});
|
|
171
|
+
return true;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (["NoSuchKey", "NotFound"].includes(error.name)) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async copy(source, target) {
|
|
180
|
+
const { cos } = this.make();
|
|
181
|
+
const sourceKey = (0, import_utils.getFileKey)(source);
|
|
182
|
+
const copySource = `${this.storage.options.Bucket}.cos.${this.storage.options.Region}.myqcloud.com/${sourceKey.split("/").map((segment) => encodeURIComponent(segment)).join("/")}`;
|
|
183
|
+
await (0, import_util.promisify)(cos.putObjectCopy).call(cos, {
|
|
184
|
+
Region: this.storage.options.Region,
|
|
185
|
+
Bucket: this.storage.options.Bucket,
|
|
186
|
+
Key: (0, import_utils.getFileKey)(target),
|
|
187
|
+
CopySource: copySource
|
|
188
|
+
});
|
|
189
|
+
}
|
|
163
190
|
async delete(records) {
|
|
164
191
|
const { cos } = this.make();
|
|
165
192
|
const { Deleted } = await (0, import_util.promisify)(cos.deleteMultipleObject).call(cos, {
|
package/dist/server/utils.js
CHANGED
|
@@ -48,6 +48,13 @@ var import_utils = require("@nocobase/utils");
|
|
|
48
48
|
var import_crypto = __toESM(require("crypto"));
|
|
49
49
|
var import_path = __toESM(require("path"));
|
|
50
50
|
var import_url_join = __toESM(require("url-join"));
|
|
51
|
+
const INVALID_FILENAME_CHARS = /* @__PURE__ */ new Set(["<", ">", "?", "*", "|", ":", '"', "\\", "/"]);
|
|
52
|
+
function sanitizeFilename(value) {
|
|
53
|
+
return Array.from(value).map((char) => {
|
|
54
|
+
const code = char.charCodeAt(0);
|
|
55
|
+
return code < 32 || code === 127 || INVALID_FILENAME_CHARS.has(char) ? "-" : char;
|
|
56
|
+
}).join("");
|
|
57
|
+
}
|
|
51
58
|
function normalizeOriginalname(file) {
|
|
52
59
|
const originalname = file == null ? void 0 : file.originalname;
|
|
53
60
|
if (!originalname) {
|
|
@@ -56,6 +63,9 @@ function normalizeOriginalname(file) {
|
|
|
56
63
|
if (Buffer.isBuffer(originalname)) {
|
|
57
64
|
return originalname.toString("utf8");
|
|
58
65
|
}
|
|
66
|
+
if (Array.from(originalname).some((char) => char.charCodeAt(0) > 255)) {
|
|
67
|
+
return originalname;
|
|
68
|
+
}
|
|
59
69
|
const decoded = Buffer.from(originalname, "binary").toString("utf8");
|
|
60
70
|
if (decoded.includes("\uFFFD")) {
|
|
61
71
|
return originalname;
|
|
@@ -64,13 +74,13 @@ function normalizeOriginalname(file) {
|
|
|
64
74
|
}
|
|
65
75
|
function getFilename(req, file, cb) {
|
|
66
76
|
const originalname = normalizeOriginalname(file);
|
|
67
|
-
const baseName = import_path.default.basename(originalname
|
|
77
|
+
const baseName = import_path.default.basename(sanitizeFilename(originalname), import_path.default.extname(originalname));
|
|
68
78
|
cb(null, `${baseName}-${(0, import_utils.uid)(6)}${import_path.default.extname(originalname)}`);
|
|
69
79
|
}
|
|
70
80
|
function getOriginalFilename(file) {
|
|
71
81
|
const originalname = normalizeOriginalname(file);
|
|
72
82
|
const extname = import_path.default.extname(originalname);
|
|
73
|
-
const baseName = import_path.default.basename(originalname
|
|
83
|
+
const baseName = import_path.default.basename(sanitizeFilename(originalname), extname);
|
|
74
84
|
return `${baseName}${extname}`;
|
|
75
85
|
}
|
|
76
86
|
const cloudFilenameGetter = (storage) => (req, file, cb) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/plugin-file-manager",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.59",
|
|
4
4
|
"displayName": "File manager",
|
|
5
5
|
"displayName.ru-RU": "Менеджер файлов",
|
|
6
6
|
"displayName.zh-CN": "文件管理器",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
"antd": "5.x",
|
|
28
28
|
"axios": "^1.7.0",
|
|
29
29
|
"cos-nodejs-sdk-v5": "^2.11.14",
|
|
30
|
-
"file-type": "^21.0.0",
|
|
31
30
|
"koa-static": "^5.0.0",
|
|
32
31
|
"mime-match": "^1.0.2",
|
|
33
32
|
"mime-types": "^3.0.1",
|
|
@@ -41,6 +40,9 @@
|
|
|
41
40
|
"supertest": "^6.1.6",
|
|
42
41
|
"url-join": "4.0.1"
|
|
43
42
|
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"file-type": "^21.0.0"
|
|
45
|
+
},
|
|
44
46
|
"peerDependencies": {
|
|
45
47
|
"@nocobase/actions": "2.x",
|
|
46
48
|
"@nocobase/client": "2.x",
|
|
@@ -53,5 +55,5 @@
|
|
|
53
55
|
"Collections",
|
|
54
56
|
"Collection fields"
|
|
55
57
|
],
|
|
56
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "8b3a0cfadf6cb2da4a7e9213ef0d8d092d885c4a"
|
|
57
59
|
}
|