@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.
Files changed (51) hide show
  1. package/dist/client/867ada653cd02a3e.mjs +6 -0
  2. package/dist/client/index.js +1 -1
  3. package/dist/client-v2/867ada653cd02a3e.mjs +6 -0
  4. package/dist/client-v2/index.js +1 -1
  5. package/dist/externalVersion.js +9 -9
  6. package/dist/locale/de-DE.json +3 -0
  7. package/dist/locale/en-US.json +3 -0
  8. package/dist/locale/es-ES.json +3 -0
  9. package/dist/locale/fr-FR.json +3 -0
  10. package/dist/locale/hu-HU.json +4 -1
  11. package/dist/locale/id-ID.json +4 -1
  12. package/dist/locale/it-IT.json +3 -0
  13. package/dist/locale/ja-JP.json +3 -0
  14. package/dist/locale/ko-KR.json +3 -0
  15. package/dist/locale/nl-NL.json +3 -0
  16. package/dist/locale/pt-BR.json +3 -0
  17. package/dist/locale/ru-RU.json +3 -0
  18. package/dist/locale/tr-TR.json +3 -0
  19. package/dist/locale/uk-UA.json +3 -0
  20. package/dist/locale/vi-VN.json +4 -1
  21. package/dist/locale/zh-CN.json +3 -0
  22. package/dist/locale/zh-TW.json +3 -0
  23. package/dist/node_modules/@aws-sdk/client-s3/package.json +1 -1
  24. package/dist/node_modules/@aws-sdk/lib-storage/package.json +1 -1
  25. package/dist/node_modules/ali-oss/package.json +1 -1
  26. package/dist/node_modules/cos-nodejs-sdk-v5/package.json +1 -1
  27. package/dist/node_modules/mime-match/package.json +1 -1
  28. package/dist/node_modules/mime-types/package.json +1 -1
  29. package/dist/node_modules/mkdirp/package.json +1 -1
  30. package/dist/node_modules/pdfjs-dist/package.json +1 -1
  31. package/dist/node_modules/url-join/package.json +1 -1
  32. package/dist/server/commands/repair-filenames.d.ts +55 -0
  33. package/dist/server/commands/repair-filenames.js +283 -0
  34. package/dist/server/server.d.ts +1 -0
  35. package/dist/server/server.js +4 -0
  36. package/dist/server/storages/ali-oss.d.ts +3 -1
  37. package/dist/server/storages/ali-oss.js +23 -2
  38. package/dist/server/storages/index.d.ts +3 -0
  39. package/dist/server/storages/index.js +6 -0
  40. package/dist/server/storages/local.d.ts +2 -0
  41. package/dist/server/storages/local.js +18 -0
  42. package/dist/server/storages/s3.d.ts +2 -0
  43. package/dist/server/storages/s3.js +26 -0
  44. package/dist/server/storages/tx-cos.d.ts +2 -0
  45. package/dist/server/storages/tx-cos.js +27 -0
  46. package/dist/server/utils.js +12 -2
  47. package/dist/shared/previewer/filePreviewTypes.d.ts +3 -0
  48. package/dist/shared/previewer/filePreviewTypes.js +86 -26
  49. package/package.json +5 -3
  50. package/dist/client/764.96d72bc6fd4adb28.js +0 -15
  51. 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, {
@@ -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.replace(/[<>?*|:"\\/]/g, "-"), import_path.default.extname(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.replace(/[<>?*|:"\\/]/g, "-"), extname);
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 [error, setError] = import_react.default.useState(false);
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
- setError(false);
427
+ setErrorCode(null);
406
428
  const init = async () => {
407
- const pdfjs = await loadPdfJs();
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
- const url = new URL(src, location.href);
413
- const response = await fetch(url, {
414
- credentials: url.origin === location.origin ? "include" : "omit",
415
- signal: session.abortController.signal
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 Error(`Failed to load PDF: ${response.status}`);
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
- session.worker = pdfjs.PDFWorker.create({
426
- port: new Worker(new URL("pdfjs-dist/build/pdf.worker.min.mjs", import_meta.url), { type: "module" })
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
- const metaPdf = await metaTask.promise;
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
- setError(true);
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 } }, error ? /* @__PURE__ */ import_react.default.createElement(
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.37",
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": "7132e5b83ecc0e42b54715eaf1429c72bcef34ae"
62
+ "gitHead": "d1c585108ff6e51c17b0b52bacb1a2d621d9c119"
61
63
  }