diff --git a/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx
index db05abb50e..b6cf3976f6 100644
--- a/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx
+++ b/packages/core/client/src/schema-component/antd/input/ReadPretty.tsx
@@ -200,32 +200,11 @@ export interface URLReadPrettyProps {
const ellipsisStyle = { textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap', display: 'block' };
-function encodeFileURL(url: string): string {
- if (!url) {
- return url;
- }
-
- if (url.includes('X-Amz-Content-Sha256')) {
- return url;
- }
-
- const parts = url.split('/');
- const filename = parts.pop();
- parts.push(encodeURIComponent(filename));
- const encodedURL = parts.join('/');
- return encodedURL;
-}
-
ReadPretty.URL = (props: URLReadPrettyProps) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const prefixCls = usePrefixCls('description-url', props);
const content = props.value && (
-
+
{props.value}
);
diff --git a/packages/core/client/src/schema-component/antd/upload/Upload.tsx b/packages/core/client/src/schema-component/antd/upload/Upload.tsx
index 37a2f1ca8e..728e1f70ad 100644
--- a/packages/core/client/src/schema-component/antd/upload/Upload.tsx
+++ b/packages/core/client/src/schema-component/antd/upload/Upload.tsx
@@ -32,7 +32,6 @@ import {
toValueItem as toValueItemDefault,
useBeforeUpload,
useUploadProps,
- encodeFileURL,
} from './shared';
import { useStyles } from './style';
import type { ComposedUpload, DraggerProps, DraggerV2Props, UploadProps } from './type';
@@ -43,10 +42,10 @@ attachmentFileTypes.add({
},
getThumbnailURL(file) {
if (file.preview) {
- return encodeFileURL(file.preview);
+ return file.preview;
}
if (file.url) {
- return encodeFileURL(`${file.url}${file.thumbnailRule || ''}`);
+ return file.url;
}
if (file.originFileObj) {
return URL.createObjectURL(file.originFileObj);
@@ -65,9 +64,9 @@ attachmentFileTypes.add({
return (
onSwitchIndex(null)}
onMovePrevRequest={() => onSwitchIndex((index + list.length - 1) % list.length)}
onMoveNextRequest={() => onSwitchIndex((index + 1) % list.length)}
@@ -95,7 +94,7 @@ const iframePreviewSupportedTypes = ['application/pdf', 'audio/*', 'image/*', 'v
function IframePreviewer({ index, list, onSwitchIndex }) {
const { t } = useTranslation();
const file = list[index];
- const url = encodeFileURL(file.url);
+ const url = file.url;
const onOpen = useCallback(
(e) => {
e.preventDefault();
@@ -264,7 +263,7 @@ function AttachmentListItem(props) {
) : null,
];
const wrappedItem = file.url ? (
-
+
{item}
) : (
diff --git a/packages/core/client/src/schema-component/antd/upload/index.ts b/packages/core/client/src/schema-component/antd/upload/index.ts
index 9fc60dfbce..cd0ff5dd7f 100644
--- a/packages/core/client/src/schema-component/antd/upload/index.ts
+++ b/packages/core/client/src/schema-component/antd/upload/index.ts
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-export { attachmentFileTypes, encodeFileURL } from './shared';
+export { attachmentFileTypes } from './shared';
export { useUploadStyles } from './style';
export * from './Upload';
diff --git a/packages/core/client/src/schema-component/antd/upload/shared.ts b/packages/core/client/src/schema-component/antd/upload/shared.ts
index b48a71054b..5bc559f988 100644
--- a/packages/core/client/src/schema-component/antd/upload/shared.ts
+++ b/packages/core/client/src/schema-component/antd/upload/shared.ts
@@ -267,20 +267,3 @@ export function useBeforeUpload(rules) {
[rules],
);
}
-
-export function encodeFileURL(url: string): string {
- if (!url) {
- return url;
- }
-
- if (url.includes('X-Amz-Content-Sha256')) {
- return url;
- }
-
- const [base, search = ''] = url.split('?');
- const parts = base.split('/');
- const filename = parts.pop();
- parts.push(encodeURIComponent(filename));
- const encodedURL = `${parts.join('/')}${search ? `?${search}` : ''}`;
- return encodedURL;
-}
diff --git a/packages/plugins/@nocobase/plugin-action-export/src/server/__tests__/export-to-xlsx.test.ts b/packages/plugins/@nocobase/plugin-action-export/src/server/__tests__/export-to-xlsx.test.ts
index 43844ca361..dea15554cc 100644
--- a/packages/plugins/@nocobase/plugin-action-export/src/server/__tests__/export-to-xlsx.test.ts
+++ b/packages/plugins/@nocobase/plugin-action-export/src/server/__tests__/export-to-xlsx.test.ts
@@ -7,14 +7,14 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
+import { BaseInterface } from '@nocobase/database';
import { createMockServer, MockServer } from '@nocobase/test';
import { uid } from '@nocobase/utils';
-import { XlsxExporter } from '../services/xlsx-exporter';
-import XLSX from 'xlsx';
import fs from 'fs';
-import path from 'path';
-import { BaseInterface } from '@nocobase/database';
import moment from 'moment';
+import path from 'path';
+import XLSX from 'xlsx';
+import { XlsxExporter } from '../services/xlsx-exporter';
XLSX.set_fs(fs);
@@ -391,13 +391,14 @@ describe('export to xlsx with preset', () => {
extname: '.png',
mimetype: 'image/png',
url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/test1.png',
+ storageId: 1,
},
{
title: 'nocobase-logo2',
filename: '682e5ad037dd02a0fe4800a3e91c283b.png',
extname: '.png',
mimetype: 'image/png',
- url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/test2.png',
+ storageId: 1,
},
],
},
@@ -432,7 +433,7 @@ describe('export to xlsx with preset', () => {
const firstUser = sheetData[1];
expect(firstUser[1]).toEqual(
- 'https://nocobase.oss-cn-beijing.aliyuncs.com/test1.png,https://nocobase.oss-cn-beijing.aliyuncs.com/test2.png',
+ 'https://nocobase.oss-cn-beijing.aliyuncs.com/test1.png,/storage/uploads/682e5ad037dd02a0fe4800a3e91c283b.png',
);
} finally {
fs.unlinkSync(xlsxFilePath);
diff --git a/packages/plugins/@nocobase/plugin-field-markdown-vditor/src/client/components/Edit.tsx b/packages/plugins/@nocobase/plugin-field-markdown-vditor/src/client/components/Edit.tsx
index ba70f6bc97..51adb9b37b 100644
--- a/packages/plugins/@nocobase/plugin-field-markdown-vditor/src/client/components/Edit.tsx
+++ b/packages/plugins/@nocobase/plugin-field-markdown-vditor/src/client/components/Edit.tsx
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { encodeFileURL, useAPIClient, useApp, withDynamicSchemaProps } from '@nocobase/client';
+import { useAPIClient, useApp, withDynamicSchemaProps } from '@nocobase/client';
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import Vditor from 'vditor';
import { defaultToolbar } from '../interfaces/markdown-vditor';
@@ -81,7 +81,7 @@ export const Edit = withDynamicSchemaProps((props) => {
data: {
errFiles: [],
succMap: {
- [response.data.filename]: encodeFileURL(response.data.url),
+ [response.data.filename]: response.data.url,
},
},
};
diff --git a/packages/plugins/@nocobase/plugin-file-manager/package.json b/packages/plugins/@nocobase/plugin-file-manager/package.json
index 3a89c17f12..ac7d84f3cd 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/package.json
+++ b/packages/plugins/@nocobase/plugin-file-manager/package.json
@@ -10,7 +10,7 @@
"homepage": "https://docs.nocobase.com/handbook/file-manager",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/file-manager",
"devDependencies": {
- "@aws-sdk/client-s3": "^3.750.0",
+ "@aws-sdk/client-s3": "3.750.0",
"@formily/antd-v5": "1.x",
"@formily/core": "2.x",
"@formily/react": "2.x",
@@ -29,7 +29,8 @@
"multer-s3": "^3.0.1",
"react": "^18.2.0",
"react-i18next": "^11.15.1",
- "supertest": "^6.1.6"
+ "supertest": "^6.1.6",
+ "url-join": "4.0.1"
},
"peerDependencies": {
"@nocobase/actions": "1.x",
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/FileModel.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/FileModel.ts
deleted file mode 100644
index 98586bff8f..0000000000
--- a/packages/plugins/@nocobase/plugin-file-manager/src/server/FileModel.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * This file is part of the NocoBase (R) project.
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
- * Authors: NocoBase Team.
- *
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
- * For more information, please refer to: https://www.nocobase.com/agreement.
- */
-
-import { Model } from '@nocobase/database';
-import { STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_LOCAL, STORAGE_TYPE_S3, STORAGE_TYPE_TX_COS } from '../constants';
-
-const currentStorage = [STORAGE_TYPE_LOCAL, STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_S3, STORAGE_TYPE_TX_COS];
-
-export class FileModel extends Model {
- public toJSON() {
- const json = super.toJSON();
- const fileStorages = this.constructor['database']?.['_fileStorages'];
- if (json.storageId && fileStorages && fileStorages.has(json.storageId)) {
- const storage = fileStorages.get(json.storageId);
- // 当前文件管理器内的存储类型拼接生成预览链接,其他文件存储自行处理
- if (currentStorage.includes(storage?.type) && storage?.options?.thumbnailRule) {
- json['preview'] = `${json['url']}${storage?.options?.thumbnailRule || ''}`;
- }
- if (storage?.options?.thumbnailRule) {
- json['thumbnailRule'] = storage?.options?.thumbnailRule;
- }
- if (storage?.type === 'local' && process.env.APP_PUBLIC_PATH) {
- json['url'] = process.env.APP_PUBLIC_PATH.replace(/\/$/g, '') + json.url;
- }
- }
- return json;
- }
-}
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/action.test.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/action.test.ts
index ec8dffe40e..a11639db3d 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/action.test.ts
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/action.test.ts
@@ -12,6 +12,7 @@ import path from 'path';
import querystring from 'querystring';
import { getApp } from '.';
import { FILE_FIELD_NAME, FILE_SIZE_LIMIT_DEFAULT, STORAGE_TYPE_LOCAL } from '../../constants';
+import PluginFileManagerServer from '../server';
const { LOCAL_STORAGE_BASE_URL, LOCAL_STORAGE_DEST = 'storage/uploads', APP_PORT = '13000' } = process.env;
@@ -52,6 +53,88 @@ describe('action', () => {
describe('create / upload', () => {
describe('default storage', () => {
+ it('should be create file record', async () => {
+ const Plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
+ const model = await Plugin.createFileRecord({
+ collectionName: 'attachments',
+ filePath: path.resolve(__dirname, './files/text.txt'),
+ });
+ const matcher = {
+ title: 'text',
+ extname: '.txt',
+ path: '',
+ // size: 13,
+ meta: {},
+ storageId: 1,
+ };
+ expect(model.toJSON()).toMatchObject(matcher);
+ });
+
+ it('should be local2 storage', async () => {
+ const storage = await StorageRepo.create({
+ values: {
+ name: 'local2',
+ type: STORAGE_TYPE_LOCAL,
+ baseUrl: DEFAULT_LOCAL_BASE_URL,
+ rules: {
+ size: 1024,
+ },
+ paranoid: true,
+ },
+ });
+ const Plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
+ const model = await Plugin.createFileRecord({
+ collectionName: 'attachments',
+ storageName: 'local2',
+ filePath: path.resolve(__dirname, './files/text.txt'),
+ });
+ const matcher = {
+ title: 'text',
+ extname: '.txt',
+ path: '',
+ // size: 13,
+ meta: {},
+ storageId: storage.id,
+ };
+ expect(model.toJSON()).toMatchObject(matcher);
+ });
+
+ it('should be custom values', async () => {
+ const Plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
+ const model = await Plugin.createFileRecord({
+ collectionName: 'attachments',
+ filePath: path.resolve(__dirname, './files/text.txt'),
+ values: {
+ size: 22,
+ },
+ });
+ const matcher = {
+ title: 'text',
+ extname: '.txt',
+ path: '',
+ size: 22,
+ meta: {},
+ storageId: 1,
+ };
+ expect(model.toJSON()).toMatchObject(matcher);
+ });
+
+ it('should be upload file', async () => {
+ const Plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
+ const data = await Plugin.uploadFile({
+ filePath: path.resolve(__dirname, './files/text.txt'),
+ documentRoot: 'storage/backups/test',
+ });
+ const matcher = {
+ title: 'text',
+ extname: '.txt',
+ path: '',
+ meta: {},
+ storageId: 1,
+ };
+ expect(data).toMatchObject(matcher);
+ });
+
it('upload file should be ok', async () => {
const { body } = await agent.resource('attachments').create({
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
@@ -125,10 +208,9 @@ describe('action', () => {
// 文件上传和解析是否正常
expect(body.data).toMatchObject(matcher);
// 文件的 url 是否正常生成
- expect(body.data.url).toBe(`${DEFAULT_LOCAL_BASE_URL}${body.data.path}/${body.data.filename}`);
+ const encodedFilename = querystring.escape(rawText);
+ expect(body.data.url).toContain(`${DEFAULT_LOCAL_BASE_URL}${body.data.path}/${encodedFilename}`);
- const encodedFilename = querystring.escape(rawFilename);
- // console.log('-----------', body.data, encodedFilename);
// 文件的 url 是否正常访问
// TODO: mock-server is not start within gateway, static url can not be accessed
// const res2 = await agent.get(`${DEFAULT_LOCAL_BASE_URL}${body.data.path}/${encodedFilename}`);
@@ -192,7 +274,7 @@ describe('action', () => {
});
it('upload to storage which is not default', async () => {
- const BASE_URL = `http://localhost:${APP_PORT}/storage/uploads/another`;
+ const BASE_URL = `/storage/uploads/another`;
const urlPath = 'test/path';
// 动态添加 storage
@@ -236,7 +318,7 @@ describe('action', () => {
});
it('path longer than 255', async () => {
- const BASE_URL = `http://localhost:${APP_PORT}/storage/uploads/another`;
+ const BASE_URL = `/storage/uploads/another`;
const urlPath =
'extreme-test/max-long-path-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890';
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/server.test.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/server.test.ts
index bfc30a4477..4866763b75 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/server.test.ts
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/server.test.ts
@@ -12,7 +12,7 @@ import path from 'path';
import { getApp } from '.';
import PluginFileManagerServer from '../server';
-import { STORAGE_TYPE_LOCAL, FILE_FIELD_NAME } from '../../constants';
+import { FILE_FIELD_NAME, STORAGE_TYPE_LOCAL } from '../../constants';
const { LOCAL_STORAGE_BASE_URL, LOCAL_STORAGE_DEST = 'storage/uploads', APP_PORT = '13000' } = process.env;
const DEFAULT_LOCAL_BASE_URL = LOCAL_STORAGE_BASE_URL || `/storage/uploads`;
@@ -131,23 +131,48 @@ describe('file manager > server', () => {
describe('getFileURL', () => {
it('local attachment without env', async () => {
const { body } = await agent.resource('attachments').create({
- [FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
+ [FILE_FIELD_NAME]: path.resolve(__dirname, './files/[]中文报告! 1%~50.4% (123) {$#}.txt'),
});
const url = await plugin.getFileURL(body.data);
expect(url).toBe(`${process.env.APP_PUBLIC_PATH?.replace(/\/$/g, '') || ''}${body.data.url}`);
});
- it('local attachment with env', async () => {
+ it('local (default with base url) attachment with env', async () => {
const originalPath = process.env.APP_PUBLIC_PATH;
- process.env.APP_PUBLIC_PATH = 'http://localhost';
+ process.env.APP_PUBLIC_PATH = '/app';
const { body } = await agent.resource('attachments').create({
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
});
const url = await plugin.getFileURL(body.data);
- expect(url).toBe(`http://localhost${body.data.url}`);
+ expect(url).toBe(`${process.env.APP_PUBLIC_PATH}/storage/uploads/${body.data.filename}`);
+
+ process.env.APP_PUBLIC_PATH = originalPath;
+ });
+
+ it('local (default without base url) attachment with env', async () => {
+ const storage = await StorageRepo.create({
+ values: {
+ name: 'local2',
+ type: STORAGE_TYPE_LOCAL,
+ rules: {
+ size: 1024,
+ },
+ default: true,
+ },
+ });
+
+ const originalPath = process.env.APP_PUBLIC_PATH;
+ process.env.APP_PUBLIC_PATH = '/nocobase/';
+
+ const { body } = await agent.resource('attachments').create({
+ [FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
+ });
+
+ const url = await plugin.getFileURL(body.data);
+ expect(url).toBe(`/nocobase/${body.data.filename}`);
process.env.APP_PUBLIC_PATH = originalPath;
});
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/actions/attachments.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/actions/attachments.ts
index a0897cbcd3..7dd2c025f9 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/server/actions/attachments.ts
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/actions/attachments.ts
@@ -28,38 +28,41 @@ function getFileFilter(storage) {
};
}
-export function getFileData(ctx: Context) {
+export async function getFileData(ctx: Context) {
const { [FILE_FIELD_NAME]: file, storage } = ctx;
if (!file) {
return ctx.throw(400, 'file validation failed');
}
- const StorageType = ctx.app.pm.get(Plugin).storageTypes.get(storage.type) as StorageClassType;
+ const plugin = ctx.app.pm.get(Plugin);
+ const StorageType = plugin.storageTypes.get(storage.type) as StorageClassType;
const { [StorageType.filenameKey || 'filename']: name } = file;
// make compatible filename across cloud service (with path)
const filename = Path.basename(name);
const extname = Path.extname(filename);
const path = (storage.path || '').replace(/^\/|\/$/g, '');
- const baseUrl = storage.baseUrl.replace(/\/+$/, '');
- const pathname = [path, filename].filter(Boolean).join('/');
- const storageInstance = new StorageType(storage);
+ let storageInstance = plugin.storagesCache.get(storage.id);
- return {
+ if (!storageInstance) {
+ await plugin.loadStorages();
+ storageInstance = plugin.storagesCache.get(storage.id);
+ }
+
+ const data = {
title: Buffer.from(file.originalname, 'latin1').toString('utf8').replace(extname, ''),
filename,
extname,
// TODO(feature): 暂时两者相同,后面 storage.path 模版化以后,这里只是 file 实际的 path
path,
size: file.size,
- // 直接缓存起来
- url: `${baseUrl}/${pathname}`,
mimetype: file.mimetype,
- // @ts-ignore
meta: ctx.request.body,
storageId: storage.id,
...(storageInstance.getFileData ? storageInstance.getFileData(file) : {}),
};
+
+ return data;
}
async function multipart(ctx: Context, next: Next) {
@@ -98,7 +101,7 @@ async function multipart(ctx: Context, next: Next) {
return ctx.throw(500, err);
}
- const values = getFileData(ctx);
+ const values = await getFileData(ctx);
ctx.action.mergeParams({
values,
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/actions/index.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/actions/index.ts
index 5d078ad08f..bca927d511 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/server/actions/index.ts
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/actions/index.ts
@@ -18,4 +18,22 @@ export default function ({ app }) {
});
app.resourcer.use(createMiddleware, { tag: 'createMiddleware', after: 'auth' });
app.resourcer.registerActionHandler('upload', actions.create);
+ app.resourcer.use(
+ async (ctx, next) => {
+ await next();
+ try {
+ const { resourceName, actionName } = ctx.action;
+ const collection = ctx.db.getCollection(resourceName);
+ if (collection?.options?.template !== 'file') {
+ return;
+ }
+ if (actionName === 'create' || actionName === 'upload') {
+ ctx.body = await ctx.body.reload();
+ }
+ } catch (error) {
+ console.log(error);
+ }
+ },
+ { before: 'dataWrapping' },
+ );
}
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts
index 4fd7eed295..5f097569b8 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts
@@ -8,14 +8,13 @@
*/
import { Plugin } from '@nocobase/server';
-import { Registry } from '@nocobase/utils';
+import { isURL, Registry } from '@nocobase/utils';
import { basename } from 'path';
import { Model, Transactionable } from '@nocobase/database';
import fs from 'fs';
import { STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_LOCAL, STORAGE_TYPE_S3, STORAGE_TYPE_TX_COS } from '../constants';
-import { FileModel } from './FileModel';
import initActions from './actions';
import { getFileData } from './actions/attachments';
import { AttachmentInterface } from './interfaces/attachment-interface';
@@ -158,7 +157,6 @@ export class PluginFileManagerServer extends Plugin {
for (const storage of storages) {
this.storagesCache.set(storage.get('id'), this.parseStorage(storage));
}
- this.db['_fileStorages'] = this.storagesCache;
}
async install() {
@@ -201,7 +199,7 @@ export class PluginFileManagerServer extends Plugin {
}
async beforeLoad() {
- this.db.registerModels({ FileModel });
+ this.db.registerModels({ FileModel: Model });
this.db.on('beforeDefineCollection', (options) => {
if (options.template === 'file') {
options.model = 'FileModel';
@@ -279,12 +277,38 @@ export class PluginFileManagerServer extends Plugin {
this.app.acl.addFixedParams('attachments', 'destroy', ownMerger);
this.app.db.interfaceManager.registerInterfaceType('attachment', AttachmentInterface);
+
+ this.db.on('afterFind', async (instances) => {
+ if (!instances) {
+ return;
+ }
+ const records = Array.isArray(instances) ? instances : [instances];
+ const name = records[0]?.constructor?.name;
+ if (name) {
+ const collection = this.db.getCollection(name);
+ if (collection?.name === 'attachments' || collection?.options?.template === 'file') {
+ for (const record of records) {
+ const url = await this.getFileURL(record);
+ const previewUrl = await this.getFileURL(record, true);
+ record.set('url', url);
+ record.set('preview', previewUrl);
+ record.dataValues.preview = previewUrl; // 强制添加preview,在附件字段时,通过set设置无效
+ }
+ }
+ }
+ });
}
- getFileURL(file: AttachmentModel) {
+ async getFileURL(file: AttachmentModel, preview = false) {
+ if (!file.storageId) {
+ return file.url;
+ }
const storage = this.storagesCache.get(file.storageId);
+ if (!storage) {
+ return file.url;
+ }
const storageType = this.storageTypes.get(storage.type);
- return new storageType(storage).getFileURL(file);
+ return new storageType(storage).getFileURL(file, preview ? storage.options.thumbnailRule : '');
}
}
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/index.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/index.ts
index e3ed53f4c6..07a3289ffb 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/index.ts
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/index.ts
@@ -7,8 +7,9 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import Application from '@nocobase/server';
+import { isURL } from '@nocobase/utils';
import { StorageEngine } from 'multer';
+import urlJoin from 'url-join';
export interface StorageModel {
id?: number;
@@ -41,8 +42,21 @@ export abstract class StorageType {
abstract delete(records: AttachmentModel[]): [number, AttachmentModel[]] | Promise<[number, AttachmentModel[]]>;
getFileData?(file: { [key: string]: any }): { [key: string]: any };
- getFileURL(file: AttachmentModel): string | Promise {
- return file.url;
+ getFileURL(file: AttachmentModel, preview?: boolean): string | Promise {
+ // 兼容历史数据
+ if (file.url && isURL(file.url)) {
+ if (preview) {
+ return file.url + (this.storage.options.thumbnailRule || '');
+ }
+ return file.url;
+ }
+ const keys = [
+ this.storage.baseUrl,
+ file.path && encodeURI(file.path),
+ encodeURIComponent(file.filename),
+ preview && this.storage.options.thumbnailRule,
+ ].filter(Boolean);
+ return urlJoin(keys);
}
}
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/local.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/local.ts
index 78d77e53ff..0a35462962 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/local.ts
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/local.ts
@@ -7,14 +7,18 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
+import { isURL } from '@nocobase/utils';
import fs from 'fs/promises';
import mkdirp from 'mkdirp';
import multer from 'multer';
import path from 'path';
+import urlJoin from 'url-join';
import { AttachmentModel, StorageType } from '.';
import { FILE_SIZE_LIMIT_DEFAULT, STORAGE_TYPE_LOCAL } from '../../constants';
import { getFilename } from '../utils';
+const DEFAULT_BASE_URL = '/storage/uploads';
+
function getDocumentRoot(storage): string {
const { documentRoot = process.env.LOCAL_STORAGE_DEST || 'storage/uploads' } = storage.options || {};
// TODO(feature): 后面考虑以字符串模板的方式使用,可注入 req/action 相关变量,以便于区分文件夹
@@ -27,7 +31,7 @@ export default class extends StorageType {
title: 'Local storage',
type: STORAGE_TYPE_LOCAL,
name: `local`,
- baseUrl: '/storage/uploads',
+ baseUrl: DEFAULT_BASE_URL,
options: {
documentRoot: 'storage/uploads',
},
@@ -72,7 +76,11 @@ export default class extends StorageType {
return [count, undeleted];
}
- getFileURL(file: AttachmentModel) {
- return process.env.APP_PUBLIC_PATH ? `${process.env.APP_PUBLIC_PATH.replace(/\/$/g, '')}${file.url}` : file.url;
+ async getFileURL(file: AttachmentModel, preview = false) {
+ const url = await super.getFileURL(file, preview);
+ if (isURL(url)) {
+ return url;
+ }
+ return urlJoin(process.env.APP_PUBLIC_PATH, url);
}
}
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/s3.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/s3.ts
index 0c3c686050..98113ce410 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/s3.ts
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/s3.ts
@@ -7,8 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { DeleteObjectsCommand } from '@aws-sdk/client-s3';
-import { md5 } from '@nocobase/utils';
+import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3';
+import crypto from 'crypto';
import { AttachmentModel, StorageType } from '.';
import { STORAGE_TYPE_S3 } from '../../constants';
import { cloudFilenameGetter, getFileKey } from '../utils';
@@ -32,7 +32,6 @@ export default class extends StorageType {
static filenameKey = 'key';
make() {
- const { S3Client } = require('@aws-sdk/client-s3');
const multerS3 = require('multer-s3');
const { accessKeyId, secretAccessKey, bucket, acl = 'public-read', ...options } = this.storage.options;
if (options.endpoint) {
@@ -64,27 +63,25 @@ export default class extends StorageType {
});
}
+ calculateContentMD5(body) {
+ const hash = crypto.createHash('md5').update(body).digest('base64');
+ return hash;
+ }
+
async deleteS3Objects(bucketName: string, objects: string[]) {
- const deleteBody = JSON.stringify({
- Objects: objects.map((objectKey) => ({ Key: objectKey })),
- });
-
- const contentMD5 = md5(deleteBody);
-
- const deleteCommand = new DeleteObjectsCommand({
- Bucket: bucketName,
- Delete: JSON.parse(deleteBody),
- });
-
- deleteCommand.middlewareStack.add(
- (next, context) => async (args) => {
- args.request['headers']['Content-Md5'] = contentMD5;
- return next(args);
- },
- { step: 'build' },
- );
const { s3 } = this.make();
- return await s3.send(deleteCommand);
+ const Deleted = [];
+ for (const Key of objects) {
+ const deleteCommand = new DeleteObjectCommand({
+ Bucket: bucketName,
+ Key,
+ });
+ await s3.send(deleteCommand);
+ Deleted.push({ Key });
+ }
+ return {
+ Deleted,
+ };
}
async delete(records: AttachmentModel[]): Promise<[number, AttachmentModel[]]> {
diff --git a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/collections/uiButtonSchemasRoles.ts b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/collections/uiButtonSchemasRoles.ts
index 27e6f9b8b5..18348b9d24 100644
--- a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/collections/uiButtonSchemasRoles.ts
+++ b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/collections/uiButtonSchemasRoles.ts
@@ -12,5 +12,6 @@ import { defineCollection } from '@nocobase/database';
export default defineCollection({
name: 'uiButtonSchemasRoles',
dumpRules: 'required',
+ autoGenId: false,
migrationRules: ['overwrite', 'schema-only'],
});