@nocobase/plugin-file-manager 2.2.0-beta.8 → 2.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.
Files changed (97) hide show
  1. package/dist/client/UseOriginalUrlRadio.d.ts +12 -0
  2. package/dist/client/index.js +1 -1
  3. package/dist/client/schemas/storageTypes/ali-oss.d.ts +12 -0
  4. package/dist/client/schemas/storageTypes/common.d.ts +12 -0
  5. package/dist/client/schemas/storageTypes/index.d.ts +48 -0
  6. package/dist/client/schemas/storageTypes/local.d.ts +12 -0
  7. package/dist/client/schemas/storageTypes/s3.d.ts +12 -0
  8. package/dist/client/schemas/storageTypes/tx-cos.d.ts +12 -0
  9. package/dist/client-v2/125.9cdb7cf3ef4712ef.js +10 -0
  10. package/dist/client-v2/{229.bd72c2d7aa088310.js → 229.297c431f63a29cb2.js} +1 -1
  11. package/dist/client-v2/{336.1dd1b32466d0c778.js → 336.04d06f2cdcc3d0b5.js} +1 -1
  12. package/dist/client-v2/{43.eb45d53ba3e9828b.js → 43.1ec3e5bf1118d239.js} +1 -1
  13. package/dist/client-v2/450.d49b00f3c0388c74.js +10 -0
  14. package/dist/client-v2/{929.d7e783304cc1f236.js → 929.3744119eff70b3a3.js} +1 -1
  15. package/dist/client-v2/components/PublicAccessField.d.ts +19 -0
  16. package/dist/client-v2/components/UseOriginalUrlField.d.ts +25 -0
  17. package/dist/client-v2/components/index.d.ts +2 -0
  18. package/dist/client-v2/index.d.ts +4 -2
  19. package/dist/client-v2/index.js +1 -1
  20. package/dist/client-v2/interfaces/attachment.d.ts +76 -0
  21. package/dist/client-v2/models/DisplayPreviewFieldModel.d.ts +22 -0
  22. package/dist/client-v2/models/UploadActionModel.d.ts +26 -0
  23. package/dist/client-v2/previewer/filePreviewTypes.d.ts +15 -1
  24. package/dist/externalVersion.js +13 -10
  25. package/dist/locale/de-DE.json +7 -0
  26. package/dist/locale/en-US.json +7 -0
  27. package/dist/locale/es-ES.json +7 -0
  28. package/dist/locale/fr-FR.json +7 -0
  29. package/dist/locale/hu-HU.json +7 -0
  30. package/dist/locale/id-ID.json +7 -0
  31. package/dist/locale/it-IT.json +7 -0
  32. package/dist/locale/ja-JP.json +7 -0
  33. package/dist/locale/ko-KR.json +7 -0
  34. package/dist/locale/nl-NL.json +7 -0
  35. package/dist/locale/pt-BR.json +7 -0
  36. package/dist/locale/ru-RU.json +7 -0
  37. package/dist/locale/tr-TR.json +7 -0
  38. package/dist/locale/uk-UA.json +7 -0
  39. package/dist/locale/vi-VN.json +7 -0
  40. package/dist/locale/zh-CN.json +7 -0
  41. package/dist/locale/zh-TW.json +7 -0
  42. package/dist/node_modules/@aws-sdk/client-s3/dist-cjs/index.js +59 -116
  43. package/dist/node_modules/@aws-sdk/client-s3/package.json +1 -1
  44. package/dist/node_modules/@aws-sdk/lib-storage/dist-cjs/index.js +59 -116
  45. package/dist/node_modules/@aws-sdk/lib-storage/package.json +1 -1
  46. package/dist/node_modules/@aws-sdk/s3-request-presigner/LICENSE +201 -0
  47. package/dist/node_modules/@aws-sdk/s3-request-presigner/dist-cjs/index.js +11113 -0
  48. package/dist/node_modules/@aws-sdk/s3-request-presigner/dist-es/constants.js +9 -0
  49. package/dist/node_modules/@aws-sdk/s3-request-presigner/dist-es/getSignedUrl.js +66 -0
  50. package/dist/node_modules/@aws-sdk/s3-request-presigner/dist-es/index.js +2 -0
  51. package/dist/node_modules/@aws-sdk/s3-request-presigner/dist-es/presigner.js +58 -0
  52. package/dist/node_modules/@aws-sdk/s3-request-presigner/dist-types/constants.d.ts +9 -0
  53. package/dist/node_modules/@aws-sdk/s3-request-presigner/dist-types/getSignedUrl.d.ts +6 -0
  54. package/dist/node_modules/@aws-sdk/s3-request-presigner/dist-types/index.d.ts +2 -0
  55. package/dist/node_modules/@aws-sdk/s3-request-presigner/dist-types/presigner.d.ts +15 -0
  56. package/dist/node_modules/@aws-sdk/s3-request-presigner/dist-types/ts3.4/constants.d.ts +9 -0
  57. package/dist/node_modules/@aws-sdk/s3-request-presigner/dist-types/ts3.4/getSignedUrl.d.ts +11 -0
  58. package/dist/node_modules/@aws-sdk/s3-request-presigner/dist-types/ts3.4/index.d.ts +2 -0
  59. package/dist/node_modules/@aws-sdk/s3-request-presigner/dist-types/ts3.4/presigner.d.ts +40 -0
  60. package/dist/node_modules/@aws-sdk/s3-request-presigner/package.json +1 -0
  61. package/dist/node_modules/ali-oss/package.json +1 -1
  62. package/dist/node_modules/cos-nodejs-sdk-v5/package.json +1 -1
  63. package/dist/node_modules/mime-match/package.json +1 -1
  64. package/dist/node_modules/mime-types/package.json +1 -1
  65. package/dist/node_modules/mkdirp/package.json +1 -1
  66. package/dist/node_modules/url-join/package.json +1 -1
  67. package/dist/server/actions/attachments.js +21 -1
  68. package/dist/server/actions/get-file.d.ts +13 -0
  69. package/dist/server/actions/get-file.js +152 -0
  70. package/dist/server/actions/index.d.ts +1 -3
  71. package/dist/server/actions/index.js +8 -1
  72. package/dist/server/commands/repair-filenames.js +34 -14
  73. package/dist/server/file-access.d.ts +11 -0
  74. package/dist/server/file-access.js +134 -0
  75. package/dist/server/index.d.ts +2 -1
  76. package/dist/server/index.js +7 -1
  77. package/dist/server/resolve-file-access-filter.d.ts +11 -0
  78. package/dist/server/resolve-file-access-filter.js +63 -0
  79. package/dist/server/server.d.ts +23 -2
  80. package/dist/server/server.js +116 -15
  81. package/dist/server/storages/ali-oss.d.ts +2 -1
  82. package/dist/server/storages/ali-oss.js +12 -0
  83. package/dist/server/storages/index.d.ts +15 -2
  84. package/dist/server/storages/index.js +28 -5
  85. package/dist/server/storages/local.d.ts +2 -2
  86. package/dist/server/storages/local.js +9 -3
  87. package/dist/server/storages/s3.d.ts +2 -1
  88. package/dist/server/storages/s3.js +14 -0
  89. package/dist/server/storages/tx-cos.d.ts +2 -1
  90. package/dist/server/storages/tx-cos.js +27 -0
  91. package/dist/server/temporary-access.d.ts +31 -0
  92. package/dist/server/temporary-access.js +170 -0
  93. package/dist/server/utils.d.ts +27 -0
  94. package/dist/server/utils.js +83 -2
  95. package/package.json +5 -3
  96. package/dist/client-v2/125.0b8eef1f19b87042.js +0 -10
  97. package/dist/client-v2/450.c060947ac34d26e1.js +0 -10
@@ -102,6 +102,28 @@ async function loadFileCollections(app) {
102
102
  await ((_a = repository.setApp) == null ? void 0 : _a.call(repository, app));
103
103
  await ((_b = repository.load) == null ? void 0 : _b.call(repository));
104
104
  }
105
+ function getColumnName(model, attribute) {
106
+ var _a;
107
+ return ((_a = model.rawAttributes[attribute]) == null ? void 0 : _a.field) || attribute;
108
+ }
109
+ function quotedColumn(app, model, attribute) {
110
+ return app.db.quoteIdentifier(getColumnName(model, attribute));
111
+ }
112
+ function quotedAlias(app, attribute) {
113
+ return app.db.quoteIdentifier(attribute);
114
+ }
115
+ async function findAttachmentRows(app, model, lastId, limit) {
116
+ const tableName = app.db.utils.quoteTable(model.getTableName());
117
+ const idColumn = quotedColumn(app, model, "id");
118
+ const columns = ["id", "path", "filename", "storageId", "url"].map((attribute) => `${quotedColumn(app, model, attribute)} as ${quotedAlias(app, attribute)}`).join(", ");
119
+ return await app.db.sequelize.query(
120
+ `select ${columns} from ${tableName}${lastId == null ? "" : ` where ${idColumn} > :lastId`} order by ${idColumn} asc limit ${limit}`,
121
+ {
122
+ replacements: { lastId },
123
+ type: import_sequelize.QueryTypes.SELECT
124
+ }
125
+ );
126
+ }
105
127
  async function repairCollectionAttachmentFilenames({
106
128
  app,
107
129
  fileManager,
@@ -115,22 +137,17 @@ async function repairCollectionAttachmentFilenames({
115
137
  if (!collection) {
116
138
  return;
117
139
  }
118
- const Model = collection.model;
140
+ const FileModel = collection.model;
119
141
  let lastId = null;
120
142
  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
- });
143
+ const rows = await findAttachmentRows(app, FileModel, lastId, Math.min(batchSize, limit - result.scanned));
127
144
  if (!rows.length) {
128
145
  break;
129
146
  }
130
147
  for (const row of rows) {
131
148
  result.scanned += 1;
132
- lastId = row.get("id");
133
- const record = row.toJSON();
149
+ lastId = row.id;
150
+ const record = row;
134
151
  if (!containsInvisibleChars(record.path) && !containsInvisibleChars(record.filename)) {
135
152
  continue;
136
153
  }
@@ -190,11 +207,14 @@ async function repairCollectionAttachmentFilenames({
190
207
  }
191
208
  if (apply) {
192
209
  await storageInstance.copy(record, newRecord);
193
- await row.update({
194
- path: newPath,
195
- filename: newFilename,
196
- url: updateUrlValue(record.url, oldKey, newKey)
197
- });
210
+ await FileModel.update(
211
+ {
212
+ path: newPath,
213
+ filename: newFilename,
214
+ url: updateUrlValue(record.url, oldKey, newKey)
215
+ },
216
+ { where: { id: lastId } }
217
+ );
198
218
  try {
199
219
  const [deleted] = await storageInstance.delete([record]);
200
220
  if (!deleted) {
@@ -0,0 +1,11 @@
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
+ import type { Context, Next } from '@nocobase/actions';
10
+ import type { PluginFileManagerServer } from './server';
11
+ export declare function createFileAccessMiddleware(plugin: PluginFileManagerServer): (ctx: Context, next: Next) => Promise<any>;
@@ -0,0 +1,134 @@
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 file_access_exports = {};
28
+ __export(file_access_exports, {
29
+ createFileAccessMiddleware: () => createFileAccessMiddleware
30
+ });
31
+ module.exports = __toCommonJS(file_access_exports);
32
+ var import_utils = require("./utils");
33
+ const DEFAULT_APP_NAME = "main";
34
+ const IDENTIFIER_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_-]*$/;
35
+ function stripPublicPath(path) {
36
+ const publicPath = (0, import_utils.trimPublicPath)(process.env.APP_PUBLIC_PATH);
37
+ if (publicPath && (path === publicPath || path.startsWith(`${publicPath}/`))) {
38
+ return path.slice(publicPath.length) || "/";
39
+ }
40
+ return path;
41
+ }
42
+ function parseFileAccessPath(path) {
43
+ const normalizedPath = stripPublicPath(path);
44
+ const segments = normalizedPath.split("/").filter(Boolean);
45
+ if (segments[0] !== "files") {
46
+ return null;
47
+ }
48
+ if (segments.length !== 5) {
49
+ throw Object.assign(new Error("Invalid file URL"), { status: 404 });
50
+ }
51
+ try {
52
+ const fileIdSegment = decodeURIComponent(segments[4]);
53
+ const extnameIndex = fileIdSegment.lastIndexOf(".");
54
+ const extname = extnameIndex > 0 ? (0, import_utils.normalizeFileAccessExtname)(fileIdSegment.slice(extnameIndex)) : "";
55
+ const params = {
56
+ appName: decodeURIComponent(segments[1]),
57
+ dataSourceKey: decodeURIComponent(segments[2]),
58
+ collectionName: decodeURIComponent(segments[3]),
59
+ id: extname ? fileIdSegment.slice(0, extnameIndex) : fileIdSegment,
60
+ extname: extname || void 0
61
+ };
62
+ if (!IDENTIFIER_PATTERN.test(params.appName) || !IDENTIFIER_PATTERN.test(params.dataSourceKey) || !IDENTIFIER_PATTERN.test(params.collectionName)) {
63
+ throw Object.assign(new Error("Invalid file URL"), { status: 404 });
64
+ }
65
+ return params;
66
+ } catch (error) {
67
+ throw Object.assign(new Error("Invalid file URL"), { status: 404 });
68
+ }
69
+ }
70
+ function getApiBasePath(dataSource) {
71
+ var _a, _b;
72
+ return (0, import_utils.trimPublicPath)((_b = (_a = dataSource.resourceManager) == null ? void 0 : _a.options) == null ? void 0 : _b.prefix);
73
+ }
74
+ function createFileAccessMiddleware(plugin) {
75
+ return async function handleFileAccess(ctx, next) {
76
+ const pathParams = parseFileAccessPath(ctx.path);
77
+ if (!pathParams) {
78
+ return next();
79
+ }
80
+ if (!["GET", "HEAD"].includes(ctx.method)) {
81
+ return ctx.throw(405);
82
+ }
83
+ const temporaryAccessToken = ctx.query["temporaryAccessToken"];
84
+ const params = {
85
+ ...pathParams,
86
+ preview: ctx.query.preview === "1",
87
+ temporaryAccess: typeof temporaryAccessToken === "string",
88
+ temporaryAccessToken: typeof temporaryAccessToken === "string" ? temporaryAccessToken : void 0
89
+ };
90
+ if (params.preview && params.temporaryAccess) {
91
+ return ctx.throw(404);
92
+ }
93
+ if (params.appName !== (plugin.app.name || DEFAULT_APP_NAME)) {
94
+ return ctx.throw(404);
95
+ }
96
+ const dataSource = plugin.app.dataSourceManager.get(params.dataSourceKey);
97
+ if (!dataSource) {
98
+ return ctx.throw(404);
99
+ }
100
+ const originalPath = ctx.path;
101
+ const originalDataSourceHeader = ctx.req.headers["x-data-source"];
102
+ const originalDb = ctx.db;
103
+ const originalOptionalAuth = ctx.state.optionalAuth;
104
+ const originalFileAccess = ctx.state.fileAccess;
105
+ const originalSkipAuthCheck = ctx.skipAuthCheck;
106
+ try {
107
+ ctx.path = `${getApiBasePath(dataSource)}/${encodeURIComponent(
108
+ params.collectionName
109
+ )}:getFile/${encodeURIComponent(params.id)}`;
110
+ ctx.req.headers["x-data-source"] = params.dataSourceKey;
111
+ ctx.db = plugin.app.db;
112
+ ctx.state.optionalAuth = !params.temporaryAccess;
113
+ ctx.skipAuthCheck = params.temporaryAccess ? true : originalSkipAuthCheck;
114
+ ctx.state.fileAccess = params;
115
+ await plugin.app.dataSourceManager.middleware()(ctx, async () => {
116
+ });
117
+ } finally {
118
+ ctx.path = originalPath;
119
+ if (originalDataSourceHeader === void 0) {
120
+ delete ctx.req.headers["x-data-source"];
121
+ } else {
122
+ ctx.req.headers["x-data-source"] = originalDataSourceHeader;
123
+ }
124
+ ctx.db = originalDb;
125
+ ctx.state.optionalAuth = originalOptionalAuth;
126
+ ctx.state.fileAccess = originalFileAccess;
127
+ ctx.skipAuthCheck = originalSkipAuthCheck;
128
+ }
129
+ };
130
+ }
131
+ // Annotate the CommonJS export names for ESM import in node:
132
+ 0 && (module.exports = {
133
+ createFileAccessMiddleware
134
+ });
@@ -9,6 +9,7 @@
9
9
  import { StorageEngine } from 'multer';
10
10
  export * from '../constants';
11
11
  export { AttachmentModel, default, PluginFileManagerServer, StorageModel } from './server';
12
+ export type { FileAccessAuthorizeParams, FileAccessAuthorizer } from './server';
12
13
  export { cloudFilenameGetter } from './utils';
13
- export { StorageType, GetFileStreamOptions } from './storages';
14
+ export { appendDownloadResponse, getDownloadContentDisposition, StorageType, GetFileStreamOptions, GetFileURLOptions, } from './storages';
14
15
  export { StorageEngine };
@@ -39,12 +39,15 @@ var server_exports = {};
39
39
  __export(server_exports, {
40
40
  AttachmentModel: () => import_server.AttachmentModel,
41
41
  GetFileStreamOptions: () => import_storages.GetFileStreamOptions,
42
+ GetFileURLOptions: () => import_storages.GetFileURLOptions,
42
43
  PluginFileManagerServer: () => import_server.PluginFileManagerServer,
43
44
  StorageEngine: () => import_multer.StorageEngine,
44
45
  StorageModel: () => import_server.StorageModel,
45
46
  StorageType: () => import_storages.StorageType,
47
+ appendDownloadResponse: () => import_storages.appendDownloadResponse,
46
48
  cloudFilenameGetter: () => import_utils.cloudFilenameGetter,
47
- default: () => import_server.default
49
+ default: () => import_server.default,
50
+ getDownloadContentDisposition: () => import_storages.getDownloadContentDisposition
48
51
  });
49
52
  module.exports = __toCommonJS(server_exports);
50
53
  var import_multer = require("multer");
@@ -56,10 +59,13 @@ var import_storages = require("./storages");
56
59
  0 && (module.exports = {
57
60
  AttachmentModel,
58
61
  GetFileStreamOptions,
62
+ GetFileURLOptions,
59
63
  PluginFileManagerServer,
60
64
  StorageEngine,
61
65
  StorageModel,
62
66
  StorageType,
67
+ appendDownloadResponse,
63
68
  cloudFilenameGetter,
69
+ getDownloadContentDisposition,
64
70
  ...require("../constants")
65
71
  });
@@ -0,0 +1,11 @@
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
+ import type { Context } from '@nocobase/actions';
10
+ import type { Collection } from '@nocobase/database';
11
+ export declare function resolveFileAccessFilter(ctx: Context, collection: Collection, filter: object): Promise<object>;
@@ -0,0 +1,63 @@
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 __create = Object.create;
11
+ var __defProp = Object.defineProperty;
12
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
13
+ var __getOwnPropNames = Object.getOwnPropertyNames;
14
+ var __getProtoOf = Object.getPrototypeOf;
15
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
16
+ var __export = (target, all) => {
17
+ for (var name in all)
18
+ __defProp(target, name, { get: all[name], enumerable: true });
19
+ };
20
+ var __copyProps = (to, from, except, desc) => {
21
+ if (from && typeof from === "object" || typeof from === "function") {
22
+ for (let key of __getOwnPropNames(from))
23
+ if (!__hasOwnProp.call(to, key) && key !== except)
24
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
25
+ }
26
+ return to;
27
+ };
28
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
29
+ // If the importer is in node compatibility mode or this is not an ESM
30
+ // file that has been converted to a CommonJS file using a Babel-
31
+ // compatible transform (i.e. "__esModule" has not been set), then set
32
+ // "default" to the CommonJS "module.exports" for node compatibility.
33
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
34
+ mod
35
+ ));
36
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
37
+ var resolve_file_access_filter_exports = {};
38
+ __export(resolve_file_access_filter_exports, {
39
+ resolveFileAccessFilter: () => resolveFileAccessFilter
40
+ });
41
+ module.exports = __toCommonJS(resolve_file_access_filter_exports);
42
+ var import_acl = require("@nocobase/acl");
43
+ var import_lodash = __toESM(require("lodash"));
44
+ async function resolveFileAccessFilter(ctx, collection, filter) {
45
+ try {
46
+ const permission = await ctx.dataSource.acl.resolveActionParams(ctx, {
47
+ resourceName: collection.name,
48
+ actionName: "view",
49
+ params: { filter }
50
+ });
51
+ const mergedParams = permission.mergedParams || { filter };
52
+ return import_lodash.default.cloneDeep(mergedParams.filter || filter);
53
+ } catch (error) {
54
+ if (error instanceof import_acl.NoPermissionError) {
55
+ ctx.throw(403, "No permissions");
56
+ }
57
+ throw error;
58
+ }
59
+ }
60
+ // Annotate the CommonJS export names for ESM import in node:
61
+ 0 && (module.exports = {
62
+ resolveFileAccessFilter
63
+ });
@@ -7,12 +7,14 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  /// <reference types="node" />
10
+ import type { Context } from '@nocobase/actions';
10
11
  import { Model, Transactionable } from '@nocobase/database';
11
12
  import { Plugin } from '@nocobase/server';
12
13
  import { Registry } from '@nocobase/utils';
13
14
  import { Readable } from 'stream';
14
- import { AttachmentModel, GetFileStreamOptions, StorageClassType, StorageModel } from './storages';
15
+ import { AttachmentModel, GetFileStreamOptions, GetFileURLOptions, StorageClassType, StorageModel } from './storages';
15
16
  export type * from './storages';
17
+ type AttachmentRecord = Model & AttachmentModel;
16
18
  export type FileRecordOptions = {
17
19
  collectionName: string;
18
20
  filePath: string;
@@ -26,12 +28,26 @@ export type UploadFileOptions = {
26
28
  subPath?: string;
27
29
  documentRoot?: string;
28
30
  };
31
+ export type FileAccessAuthorizeParams = {
32
+ appName: string;
33
+ dataSourceKey: string;
34
+ collectionName: string;
35
+ id: string;
36
+ preview: boolean;
37
+ };
38
+ export type FileAccessAuthorizer = {
39
+ name: string;
40
+ authorize: (ctx: Context, params: FileAccessAuthorizeParams) => Promise<boolean | null | undefined> | boolean | null | undefined;
41
+ };
29
42
  export declare class PluginFileManagerServer extends Plugin {
30
43
  storageTypes: Registry<StorageClassType>;
31
44
  storagesCache: Map<string | number, StorageModel>;
45
+ protected fileAccessAuthorizers: Registry<FileAccessAuthorizer>;
32
46
  static staticImport(): Promise<void>;
33
47
  afterDestroy: (record: Model, options: any) => Promise<void>;
34
48
  registerStorageType(type: string, Type: StorageClassType): void;
49
+ registerFileAccessAuthorizer(authorizer: FileAccessAuthorizer): void;
50
+ authorizeFileAccess(ctx: Context, params: FileAccessAuthorizeParams): Promise<boolean>;
35
51
  createFileRecord(options: FileRecordOptions): Promise<any>;
36
52
  parseStorage(instance: any): any;
37
53
  uploadFile(options: UploadFileOptions): Promise<{
@@ -51,7 +67,12 @@ export declare class PluginFileManagerServer extends Plugin {
51
67
  handleSyncMessage(message: any): Promise<void>;
52
68
  beforeLoad(): Promise<void>;
53
69
  load(): Promise<void>;
54
- getFileURL(file: AttachmentModel, preview?: boolean): Promise<any>;
70
+ setFileResponseURLs(record: AttachmentRecord, collectionName: string): Promise<void>;
71
+ getPermanentFileURL(file: AttachmentModel, preview?: boolean, options?: {
72
+ dataSourceKey?: string;
73
+ collectionName?: string;
74
+ }): any;
75
+ getFileURL(file: AttachmentModel, preview?: boolean, options?: GetFileURLOptions): Promise<any>;
55
76
  isPublicAccessStorage(storageName: any): Promise<boolean>;
56
77
  getFileStream(file: AttachmentModel, options?: GetFileStreamOptions): Promise<{
57
78
  stream: Readable;
@@ -48,6 +48,7 @@ var import_server = require("@nocobase/server");
48
48
  var import_utils = require("@nocobase/utils");
49
49
  var import_constants = require("../constants");
50
50
  var import_actions = __toESM(require("./actions"));
51
+ var import_file_access = require("./file-access");
51
52
  var import_attachment_interface = require("./interfaces/attachment-interface");
52
53
  var import_ali_oss = __toESM(require("./storages/ali-oss"));
53
54
  var import_local = __toESM(require("./storages/local"));
@@ -55,7 +56,20 @@ var import_s3 = __toESM(require("./storages/s3"));
55
56
  var import_tx_cos = __toESM(require("./storages/tx-cos"));
56
57
  var import_utils2 = require("./utils");
57
58
  var import_repair_filenames = require("./commands/repair-filenames");
59
+ var import_temporary_access = require("./temporary-access");
58
60
  const DEFAULT_STORAGE_TYPE = import_constants.STORAGE_TYPE_LOCAL;
61
+ const DEFAULT_APP_NAME = "main";
62
+ const DEFAULT_DATA_SOURCE_KEY = "main";
63
+ class FileModel extends import_database.Model {
64
+ toJSON() {
65
+ const values = super.toJSON();
66
+ const local = this.get("local");
67
+ if (typeof local === "boolean") {
68
+ values.local = local;
69
+ }
70
+ return values;
71
+ }
72
+ }
59
73
  class FileDeleteError extends Error {
60
74
  data;
61
75
  constructor(message, data) {
@@ -64,9 +78,15 @@ class FileDeleteError extends Error {
64
78
  this.data = data;
65
79
  }
66
80
  }
81
+ function supportsFileStream(dataSource) {
82
+ return Boolean(
83
+ dataSource && typeof dataSource === "object" && typeof dataSource.getFileStream === "function"
84
+ );
85
+ }
67
86
  class PluginFileManagerServer extends import_server.Plugin {
68
87
  storageTypes = new import_utils.Registry();
69
88
  storagesCache = /* @__PURE__ */ new Map();
89
+ fileAccessAuthorizers = new import_utils.Registry();
70
90
  static async staticImport() {
71
91
  import_server.Application.addCommand(import_repair_filenames.registerRepairFilenamesCommand);
72
92
  }
@@ -99,6 +119,17 @@ class PluginFileManagerServer extends import_server.Plugin {
99
119
  registerStorageType(type, Type) {
100
120
  this.storageTypes.register(type, Type);
101
121
  }
122
+ registerFileAccessAuthorizer(authorizer) {
123
+ this.fileAccessAuthorizers.register(authorizer.name, authorizer);
124
+ }
125
+ async authorizeFileAccess(ctx, params) {
126
+ for (const authorizer of this.fileAccessAuthorizers.getValues()) {
127
+ if (await authorizer.authorize(ctx, params)) {
128
+ return true;
129
+ }
130
+ }
131
+ return false;
132
+ }
102
133
  async createFileRecord(options) {
103
134
  const { values, storageName, subPath, collectionName, filePath, transaction } = options;
104
135
  const collection = this.db.getCollection(collectionName);
@@ -190,7 +221,8 @@ class PluginFileManagerServer extends import_server.Plugin {
190
221
  }
191
222
  }
192
223
  async beforeLoad() {
193
- this.db.registerModels({ FileModel: import_database.Model });
224
+ (0, import_temporary_access.getTemporaryFileAccessExpiresIn)();
225
+ this.db.registerModels({ FileModel });
194
226
  this.db.on("beforeDefineCollection", (options) => {
195
227
  if (options.template === "file") {
196
228
  options.model = "FileModel";
@@ -200,7 +232,27 @@ class PluginFileManagerServer extends import_server.Plugin {
200
232
  if (collection.options.template !== "file") {
201
233
  return;
202
234
  }
235
+ collection.setField("local", {
236
+ type: "virtual",
237
+ hidden: true
238
+ });
239
+ const idField = collection.getField("id");
240
+ if (idField) {
241
+ idField.options.deletable = false;
242
+ idField.options.updatable = false;
243
+ }
244
+ const extnameField = collection.getField("extname");
245
+ if (extnameField) {
246
+ extnameField.options.updatable = false;
247
+ }
248
+ collection.model.afterCreate(async (model) => {
249
+ await this.setFileResponseURLs(model, collection.name);
250
+ });
203
251
  collection.model.beforeUpdate((model) => {
252
+ if (model.changed("extname")) {
253
+ model.set("extname", model.previous("extname"));
254
+ model.changed("extname", false);
255
+ }
204
256
  if (!model.changed("url") || !model.changed("preview")) {
205
257
  return;
206
258
  }
@@ -278,7 +330,16 @@ class PluginFileManagerServer extends import_server.Plugin {
278
330
  this.app.acl.addFixedParams("attachments", "update", ownMerger);
279
331
  this.app.acl.addFixedParams("attachments", "create", ownMerger);
280
332
  this.app.acl.addFixedParams("attachments", "destroy", ownMerger);
333
+ this.app.resourcer.define({
334
+ name: "attachments",
335
+ actions: {
336
+ list(ctx) {
337
+ ctx.throw(404);
338
+ }
339
+ }
340
+ });
281
341
  this.app.db.interfaceManager.registerInterfaceType("attachment", import_attachment_interface.AttachmentInterface);
342
+ this.app.use((0, import_file_access.createFileAccessMiddleware)(this), { tag: "fileAccess", before: "dataSource", after: "dataWrapping" });
282
343
  this.db.on("afterFind", async (instances) => {
283
344
  var _a, _b, _c;
284
345
  if (!instances) {
@@ -290,32 +351,67 @@ class PluginFileManagerServer extends import_server.Plugin {
290
351
  const collection = this.db.getCollection(name);
291
352
  if ((collection == null ? void 0 : collection.name) === "attachments" || ((_c = collection == null ? void 0 : collection.options) == null ? void 0 : _c.template) === "file") {
292
353
  for (const record of records) {
293
- const url = await this.getFileURL(record);
294
- const previewUrl = await this.getFileURL(record, true);
295
- record.set("url", url);
296
- record.set("preview", previewUrl);
297
- record.dataValues.preview = previewUrl;
354
+ await this.setFileResponseURLs(record, collection.name);
298
355
  }
299
356
  }
300
357
  }
301
358
  });
302
359
  }
303
- async getFileURL(file, preview = false) {
360
+ async setFileResponseURLs(record, collectionName) {
361
+ var _a;
362
+ const storage = this.storagesCache.get(record.get("storageId"));
363
+ const useOriginalUrl = Boolean((_a = storage == null ? void 0 : storage.options) == null ? void 0 : _a.useOriginalUrl);
364
+ const [url, previewUrl] = useOriginalUrl ? await Promise.all([this.getFileURL(record), this.getFileURL(record, true)]) : [
365
+ this.getPermanentFileURL(record, false, { collectionName }),
366
+ this.getPermanentFileURL(record, true, { collectionName })
367
+ ];
368
+ record.set("url", url);
369
+ record.set("preview", previewUrl);
370
+ record.dataValues.preview = previewUrl;
371
+ if (storage == null ? void 0 : storage.type) {
372
+ record.set("local", storage.type === import_constants.STORAGE_TYPE_LOCAL);
373
+ }
374
+ }
375
+ getPermanentFileURL(file, preview = false, options = {}) {
304
376
  if (!file.storageId) {
305
377
  return (0, import_utils2.encodeURL)(file.url);
306
378
  }
379
+ const publicPath = (0, import_utils2.getFilePublicBasePath)();
380
+ const appName = this.app.name || DEFAULT_APP_NAME;
381
+ const dataSourceKey = options.dataSourceKey || DEFAULT_DATA_SOURCE_KEY;
382
+ const collectionName = options.collectionName || (0, import_utils2.getRecordCollectionName)(file);
383
+ const id = (0, import_utils2.getFileAccessPathSegment)(file.id, file.extname);
384
+ const url = `${publicPath}/files/${encodeURIComponent(String(appName))}/${encodeURIComponent(
385
+ String(dataSourceKey)
386
+ )}/${encodeURIComponent(String(collectionName))}/${id}`;
387
+ return preview ? `${url}?preview=1` : url;
388
+ }
389
+ async getFileURL(file, preview = false, options = {}) {
390
+ if (!file.storageId) {
391
+ return (0, import_utils2.encodeURL)(file.url);
392
+ }
393
+ const storageFile = (0, import_utils2.getFilePlainObject)(file);
394
+ if ((0, import_utils2.isPermanentFileAccessURL)(storageFile.url, storageFile, this.app.name || DEFAULT_APP_NAME)) {
395
+ storageFile.url = void 0;
396
+ }
307
397
  const storage = this.storagesCache.get(file.storageId);
308
398
  if (!storage) {
309
- return (0, import_utils2.encodeURL)(file.url);
399
+ throw new Error("[file-manager] no linked or default storage provided");
310
400
  }
311
401
  const storageType = this.storageTypes.get(storage.type);
402
+ if (!storageType) {
403
+ throw new Error(`[file-manager] storage type "${storage.type}" is not defined`);
404
+ }
312
405
  return new storageType(storage).getFileURL(
313
- file,
314
- Boolean(file.mimetype && (0, import_mime_match.default)(file.mimetype, "image/*") && preview && storage.options.thumbnailRule)
406
+ storageFile,
407
+ Boolean(
408
+ storageFile.mimetype && (0, import_mime_match.default)(storageFile.mimetype, "image/*") && preview && storage.options.thumbnailRule
409
+ ),
410
+ options
315
411
  );
316
412
  }
317
413
  async isPublicAccessStorage(storageName) {
318
- var _a;
414
+ var _a, _b;
319
415
  const storageRepository = this.db.getRepository("storages");
320
416
  const storages = await storageRepository.findOne({
321
417
  filter: { default: true }
@@ -331,12 +427,17 @@ class PluginFileManagerServer extends import_server.Plugin {
331
427
  });
332
428
  }
333
429
  storage = this.parseStorage(storage);
334
- if (["local", "ali-oss", "s3", "tx-cos"].includes(storage.type)) {
335
- return true;
336
- }
337
- return !!((_a = storage.options) == null ? void 0 : _a.public);
430
+ return Boolean(((_a = storage.options) == null ? void 0 : _a.public) || ((_b = storage.options) == null ? void 0 : _b.useOriginalUrl));
338
431
  }
339
432
  async getFileStream(file, options) {
433
+ var _a;
434
+ const dataSourceKey = (_a = file.source) == null ? void 0 : _a.dataSourceKey;
435
+ if (dataSourceKey && dataSourceKey !== "main") {
436
+ const dataSource = this.app.dataSourceManager.get(dataSourceKey);
437
+ if (supportsFileStream(dataSource)) {
438
+ return dataSource.getFileStream(file, options);
439
+ }
440
+ }
340
441
  if (!file.storageId) {
341
442
  throw new Error("File storageId not found");
342
443
  }
@@ -6,7 +6,7 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
- import { AttachmentModel, StorageType } from '.';
9
+ import { AttachmentModel, GetFileURLOptions, StorageType } from '.';
10
10
  declare function getRandomFilename(req: any, file: any, cb: any): void;
11
11
  declare class AliYunOssStorage {
12
12
  client: any;
@@ -34,6 +34,7 @@ export default class extends StorageType {
34
34
  };
35
35
  };
36
36
  make(): AliYunOssStorage;
37
+ getFileURL(file: AttachmentModel, preview?: boolean, options?: GetFileURLOptions): Promise<any>;
37
38
  exists(record: AttachmentModel): Promise<boolean>;
38
39
  copy(source: AttachmentModel, target: AttachmentModel): Promise<void>;
39
40
  delete(records: AttachmentModel[]): Promise<[number, AttachmentModel[]]>;
@@ -123,6 +123,18 @@ class ali_oss_default extends import__.StorageType {
123
123
  filename: (0, import_utils.cloudFilenameGetter)(this.storage)
124
124
  });
125
125
  }
126
+ async getFileURL(file, preview = false, options = {}) {
127
+ if (!options.download) {
128
+ return super.getFileURL(file, preview, options);
129
+ }
130
+ const { client } = this.make();
131
+ return client.signatureUrl((0, import_utils.getFileKey)(file), {
132
+ expires: this.storage.options.signedUrlExpires || 900,
133
+ response: {
134
+ "content-disposition": (0, import__.getDownloadContentDisposition)(file.filename)
135
+ }
136
+ });
137
+ }
126
138
  async exists(record) {
127
139
  const { client } = this.make();
128
140
  try {