@nocobase/plugin-file-manager 2.1.0-beta.37 → 2.1.0-beta.38
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-v2/867ada653cd02a3e.mjs +6 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/externalVersion.js +9 -9
- 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/pdfjs-dist/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/dist/shared/previewer/filePreviewTypes.d.ts +3 -0
- package/dist/shared/previewer/filePreviewTypes.js +86 -26
- package/package.json +5 -3
- package/dist/client/764.96d72bc6fd4adb28.js +0 -15
- package/dist/client-v2/764.d5a27ea47a2d3239.js +0 -15
|
@@ -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
|
@@ -49,6 +49,13 @@ var import_utils = require("@nocobase/utils");
|
|
|
49
49
|
var import_crypto = __toESM(require("crypto"));
|
|
50
50
|
var import_path = __toESM(require("path"));
|
|
51
51
|
var import_url_join = __toESM(require("url-join"));
|
|
52
|
+
const INVALID_FILENAME_CHARS = /* @__PURE__ */ new Set(["<", ">", "?", "*", "|", ":", '"', "\\", "/"]);
|
|
53
|
+
function sanitizeFilename(value) {
|
|
54
|
+
return Array.from(value).map((char) => {
|
|
55
|
+
const code = char.charCodeAt(0);
|
|
56
|
+
return code < 32 || code === 127 || INVALID_FILENAME_CHARS.has(char) ? "-" : char;
|
|
57
|
+
}).join("");
|
|
58
|
+
}
|
|
52
59
|
function normalizeOriginalname(file) {
|
|
53
60
|
const originalname = file == null ? void 0 : file.originalname;
|
|
54
61
|
if (!originalname) {
|
|
@@ -57,6 +64,9 @@ function normalizeOriginalname(file) {
|
|
|
57
64
|
if (Buffer.isBuffer(originalname)) {
|
|
58
65
|
return originalname.toString("utf8");
|
|
59
66
|
}
|
|
67
|
+
if (Array.from(originalname).some((char) => char.charCodeAt(0) > 255)) {
|
|
68
|
+
return originalname;
|
|
69
|
+
}
|
|
60
70
|
const decoded = Buffer.from(originalname, "binary").toString("utf8");
|
|
61
71
|
if (decoded.includes("\uFFFD")) {
|
|
62
72
|
return originalname;
|
|
@@ -65,13 +75,13 @@ function normalizeOriginalname(file) {
|
|
|
65
75
|
}
|
|
66
76
|
function getFilename(req, file, cb) {
|
|
67
77
|
const originalname = normalizeOriginalname(file);
|
|
68
|
-
const baseName = import_path.default.basename(originalname
|
|
78
|
+
const baseName = import_path.default.basename(sanitizeFilename(originalname), import_path.default.extname(originalname));
|
|
69
79
|
cb(null, `${baseName}-${(0, import_utils.uid)(6)}${import_path.default.extname(originalname)}`);
|
|
70
80
|
}
|
|
71
81
|
function getOriginalFilename(file) {
|
|
72
82
|
const originalname = normalizeOriginalname(file);
|
|
73
83
|
const extname = import_path.default.extname(originalname);
|
|
74
|
-
const baseName = import_path.default.basename(originalname
|
|
84
|
+
const baseName = import_path.default.basename(sanitizeFilename(originalname), extname);
|
|
75
85
|
return `${baseName}${extname}`;
|
|
76
86
|
}
|
|
77
87
|
const cloudFilenameGetter = (storage) => (req, file, cb) => {
|
|
@@ -40,4 +40,7 @@ export declare const getDownloadFileName: (file: any, url?: string) => string;
|
|
|
40
40
|
export declare const getFallbackIcon: (file: any, url?: string) => string;
|
|
41
41
|
export declare const getPreviewThumbnailUrl: (file: any) => string;
|
|
42
42
|
export declare const wrapWithModalPreviewer: (Previewer: React.ComponentType<FilePreviewerProps>) => (props: FilePreviewerProps) => React.JSX.Element;
|
|
43
|
+
type PdfPreviewErrorCode = 'resources' | 'file' | 'document';
|
|
44
|
+
export declare const getPdfPreviewErrorCode: (error: unknown) => PdfPreviewErrorCode;
|
|
43
45
|
export declare const FilePreviewRenderer: (props: FilePreviewerProps) => React.JSX.Element;
|
|
46
|
+
export {};
|
|
@@ -44,6 +44,7 @@ __export(filePreviewTypes_exports, {
|
|
|
44
44
|
getFileExt: () => getFileExt,
|
|
45
45
|
getFileName: () => getFileName,
|
|
46
46
|
getFileUrl: () => getFileUrl,
|
|
47
|
+
getPdfPreviewErrorCode: () => getPdfPreviewErrorCode,
|
|
47
48
|
getPreviewFileUrl: () => getPreviewFileUrl,
|
|
48
49
|
getPreviewThumbnailUrl: () => getPreviewThumbnailUrl,
|
|
49
50
|
isActiveContentFile: () => isActiveContentFile,
|
|
@@ -374,6 +375,22 @@ const IframePreviewer = ({ file }) => {
|
|
|
374
375
|
return /* @__PURE__ */ import_react.default.createElement("iframe", { src, width: "100%", height: "100%", style: { border: "none" } });
|
|
375
376
|
};
|
|
376
377
|
let pdfjsPromise = null;
|
|
378
|
+
class PdfPreviewError extends Error {
|
|
379
|
+
code;
|
|
380
|
+
cause;
|
|
381
|
+
constructor(code, message, cause) {
|
|
382
|
+
super(message);
|
|
383
|
+
this.name = "PdfPreviewError";
|
|
384
|
+
this.code = code;
|
|
385
|
+
this.cause = cause;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const getPdfPreviewErrorCode = (error) => {
|
|
389
|
+
if (error instanceof PdfPreviewError) {
|
|
390
|
+
return error.code;
|
|
391
|
+
}
|
|
392
|
+
return "document";
|
|
393
|
+
};
|
|
377
394
|
const loadPdfJs = async () => {
|
|
378
395
|
if (!pdfjsPromise) {
|
|
379
396
|
pdfjsPromise = import("pdfjs-dist/build/pdf.mjs");
|
|
@@ -381,13 +398,18 @@ const loadPdfJs = async () => {
|
|
|
381
398
|
return pdfjsPromise;
|
|
382
399
|
};
|
|
383
400
|
const PDF_SCALE = 1.4;
|
|
401
|
+
const PDF_PREVIEW_ERROR_MESSAGES = {
|
|
402
|
+
resources: "PDF preview resources failed to load. Please refresh the page and check whether plugin static files are deployed correctly.",
|
|
403
|
+
file: "PDF preview failed to load the file. If the file is stored on another domain, configure CORS for the external storage to allow this site to read the file.",
|
|
404
|
+
document: "PDF preview failed. Please download the file to preview it."
|
|
405
|
+
};
|
|
384
406
|
const PdfPreviewer = ({ file }) => {
|
|
385
|
-
const { t } = (0, import_react_i18next.useTranslation)();
|
|
407
|
+
const { t } = (0, import_react_i18next.useTranslation)(import_locale.NAMESPACE);
|
|
386
408
|
const src = getFileUrl(file);
|
|
387
409
|
const pageElsRef = import_react.default.useRef(/* @__PURE__ */ new Map());
|
|
388
410
|
const sessionRef = import_react.default.useRef(null);
|
|
389
411
|
const [meta, setMeta] = import_react.default.useState(null);
|
|
390
|
-
const [
|
|
412
|
+
const [errorCode, setErrorCode] = import_react.default.useState(null);
|
|
391
413
|
import_react.default.useEffect(() => {
|
|
392
414
|
if (!src) {
|
|
393
415
|
return;
|
|
@@ -402,29 +424,55 @@ const PdfPreviewer = ({ file }) => {
|
|
|
402
424
|
};
|
|
403
425
|
sessionRef.current = session;
|
|
404
426
|
setMeta(null);
|
|
405
|
-
|
|
427
|
+
setErrorCode(null);
|
|
406
428
|
const init = async () => {
|
|
407
|
-
|
|
429
|
+
let pdfjs;
|
|
430
|
+
try {
|
|
431
|
+
pdfjs = await loadPdfJs();
|
|
432
|
+
} catch (error) {
|
|
433
|
+
throw new PdfPreviewError("resources", "Failed to load PDF.js resources.", error);
|
|
434
|
+
}
|
|
408
435
|
if (session.cancelled) {
|
|
409
436
|
return;
|
|
410
437
|
}
|
|
411
438
|
session.pdfjs = pdfjs;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
439
|
+
let url;
|
|
440
|
+
try {
|
|
441
|
+
url = new URL(src, location.href);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
throw new PdfPreviewError("file", "Invalid PDF file URL.", error);
|
|
444
|
+
}
|
|
445
|
+
let response;
|
|
446
|
+
try {
|
|
447
|
+
response = await fetch(url, {
|
|
448
|
+
credentials: url.origin === location.origin ? "include" : "omit",
|
|
449
|
+
signal: session.abortController.signal
|
|
450
|
+
});
|
|
451
|
+
} catch (error) {
|
|
452
|
+
throw new PdfPreviewError("file", "Failed to fetch PDF file.", error);
|
|
453
|
+
}
|
|
417
454
|
if (!response.ok) {
|
|
418
|
-
throw new
|
|
455
|
+
throw new PdfPreviewError("file", `Failed to fetch PDF file: ${response.status}`);
|
|
456
|
+
}
|
|
457
|
+
let data;
|
|
458
|
+
try {
|
|
459
|
+
data = new Uint8Array(await response.arrayBuffer());
|
|
460
|
+
} catch (error) {
|
|
461
|
+
throw new PdfPreviewError("file", "Failed to read PDF file response.", error);
|
|
419
462
|
}
|
|
420
|
-
const data = new Uint8Array(await response.arrayBuffer());
|
|
421
463
|
if (session.cancelled) {
|
|
422
464
|
return;
|
|
423
465
|
}
|
|
424
466
|
session.data = data;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
467
|
+
try {
|
|
468
|
+
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
|
469
|
+
"pdfjs-dist/build/pdf.worker.min.mjs",
|
|
470
|
+
import_meta.url
|
|
471
|
+
).toString();
|
|
472
|
+
session.worker = pdfjs.PDFWorker.create({});
|
|
473
|
+
} catch (error) {
|
|
474
|
+
throw new PdfPreviewError("resources", "Failed to load PDF.js worker.", error);
|
|
475
|
+
}
|
|
428
476
|
const metaTask = pdfjs.getDocument({
|
|
429
477
|
data: data.slice(0),
|
|
430
478
|
isEvalSupported: false,
|
|
@@ -433,7 +481,12 @@ const PdfPreviewer = ({ file }) => {
|
|
|
433
481
|
worker: session.worker
|
|
434
482
|
});
|
|
435
483
|
session.loadingTasks.push(metaTask);
|
|
436
|
-
|
|
484
|
+
let metaPdf;
|
|
485
|
+
try {
|
|
486
|
+
metaPdf = await metaTask.promise;
|
|
487
|
+
} catch (error) {
|
|
488
|
+
throw new PdfPreviewError("document", "Failed to parse PDF file.", error);
|
|
489
|
+
}
|
|
437
490
|
if (session.cancelled) {
|
|
438
491
|
return;
|
|
439
492
|
}
|
|
@@ -444,9 +497,15 @@ const PdfPreviewer = ({ file }) => {
|
|
|
444
497
|
}
|
|
445
498
|
setMeta({ numPages: metaPdf.numPages, width: viewport.width, height: viewport.height });
|
|
446
499
|
};
|
|
447
|
-
init().catch(() => {
|
|
500
|
+
init().catch((error) => {
|
|
448
501
|
if (!session.cancelled) {
|
|
449
|
-
|
|
502
|
+
const code = getPdfPreviewErrorCode(error);
|
|
503
|
+
console.warn("[file-manager] PDF preview failed", {
|
|
504
|
+
code,
|
|
505
|
+
src,
|
|
506
|
+
error
|
|
507
|
+
});
|
|
508
|
+
setErrorCode(code);
|
|
450
509
|
}
|
|
451
510
|
});
|
|
452
511
|
return () => {
|
|
@@ -528,6 +587,13 @@ const PdfPreviewer = ({ file }) => {
|
|
|
528
587
|
session.rendered.add(pageNumber);
|
|
529
588
|
(_a = session.observer) == null ? void 0 : _a.unobserve(wrapper);
|
|
530
589
|
} catch (renderError) {
|
|
590
|
+
if (!session.cancelled) {
|
|
591
|
+
console.warn("[file-manager] PDF page render failed", {
|
|
592
|
+
pageNumber,
|
|
593
|
+
src,
|
|
594
|
+
error: renderError
|
|
595
|
+
});
|
|
596
|
+
}
|
|
531
597
|
} finally {
|
|
532
598
|
task == null ? void 0 : task.destroy();
|
|
533
599
|
session.inFlight.delete(pageNumber);
|
|
@@ -551,18 +617,11 @@ const PdfPreviewer = ({ file }) => {
|
|
|
551
617
|
session.observer = void 0;
|
|
552
618
|
}
|
|
553
619
|
};
|
|
554
|
-
}, [meta]);
|
|
620
|
+
}, [meta, src]);
|
|
555
621
|
if (!src) {
|
|
556
622
|
return null;
|
|
557
623
|
}
|
|
558
|
-
return /* @__PURE__ */ import_react.default.createElement("div", { style: { width: "100%", minHeight: "100%", padding: 16 } },
|
|
559
|
-
import_antd.Alert,
|
|
560
|
-
{
|
|
561
|
-
type: "warning",
|
|
562
|
-
showIcon: true,
|
|
563
|
-
message: t("File type is not supported for previewing, please download it to preview.")
|
|
564
|
-
}
|
|
565
|
-
) : null, !meta && !error ? /* @__PURE__ */ import_react.default.createElement("div", { style: { display: "flex", justifyContent: "center", padding: 48 } }, /* @__PURE__ */ import_react.default.createElement(import_antd.Spin, null)) : null, meta ? Array.from({ length: meta.numPages }, (_, index) => index + 1).map((pageNumber) => /* @__PURE__ */ import_react.default.createElement(
|
|
624
|
+
return /* @__PURE__ */ import_react.default.createElement("div", { style: { width: "100%", minHeight: "100%", padding: 16 } }, errorCode ? /* @__PURE__ */ import_react.default.createElement(import_antd.Alert, { type: "warning", showIcon: true, message: t(PDF_PREVIEW_ERROR_MESSAGES[errorCode]) }) : null, !meta && !errorCode ? /* @__PURE__ */ import_react.default.createElement("div", { style: { display: "flex", justifyContent: "center", padding: 48 } }, /* @__PURE__ */ import_react.default.createElement(import_antd.Spin, null)) : null, meta ? Array.from({ length: meta.numPages }, (_, index) => index + 1).map((pageNumber) => /* @__PURE__ */ import_react.default.createElement(
|
|
566
625
|
"div",
|
|
567
626
|
{
|
|
568
627
|
key: pageNumber,
|
|
@@ -681,6 +740,7 @@ const FilePreviewRenderer = (props) => {
|
|
|
681
740
|
getFileExt,
|
|
682
741
|
getFileName,
|
|
683
742
|
getFileUrl,
|
|
743
|
+
getPdfPreviewErrorCode,
|
|
684
744
|
getPreviewFileUrl,
|
|
685
745
|
getPreviewThumbnailUrl,
|
|
686
746
|
isActiveContentFile,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/plugin-file-manager",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.38",
|
|
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",
|
|
@@ -57,5 +59,5 @@
|
|
|
57
59
|
"Collections",
|
|
58
60
|
"Collection fields"
|
|
59
61
|
],
|
|
60
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "d1c585108ff6e51c17b0b52bacb1a2d621d9c119"
|
|
61
63
|
}
|