diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/client/templates/file.ts b/packages/plugins/@nocobase/plugin-file-manager/src/client/templates/file.ts index f943aefe38..9bf39430d3 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/client/templates/file.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/client/templates/file.ts @@ -101,7 +101,7 @@ export class FileCollectionTemplate extends CollectionTemplate { // 文件的可访问地址 { interface: 'url', - type: 'string', + type: 'text', name: 'url', deletable: false, length: 1024, 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 235aedc30c..f5d6eab353 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 @@ -164,10 +164,10 @@ describe('action', () => { // 关联的存储引擎是否正确 const storage = await attachment.getStorage(); - expect(storage).toMatchObject({ - type: 'local', + expect(storage.get()).toMatchObject({ + type: STORAGE_TYPE_LOCAL, options: { documentRoot: LOCAL_STORAGE_DEST }, - rules: {}, + rules: { size: FILE_SIZE_LIMIT_DEFAULT }, path: '', baseUrl: DEFAULT_LOCAL_BASE_URL, default: true, @@ -176,7 +176,7 @@ describe('action', () => { const { documentRoot = 'storage/uploads' } = storage.options || {}; const destPath = path.resolve( path.isAbsolute(documentRoot) ? documentRoot : path.join(process.cwd(), documentRoot), - storage.path, + storage.path || '', ); const file = await fs.readFile(`${destPath}/${attachment.filename}`); // 文件是否保存到指定路径 @@ -287,6 +287,51 @@ describe('action', () => { const content = await agent.get(url); expect(content.text.includes('Hello world!')).toBe(true); }); + + it('path longer than 255', async () => { + const BASE_URL = `http://localhost:${APP_PORT}/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'; + + // 动态添加 storage + const storage = await StorageRepo.create({ + values: { + name: 'local_private', + type: STORAGE_TYPE_LOCAL, + rules: { + mimetype: ['text/*'], + }, + path: urlPath, + baseUrl: BASE_URL, + options: { + documentRoot: 'storage/uploads/another', + }, + }, + }); + + db.collection({ + name: 'customers', + fields: [ + { + name: 'file', + type: 'belongsTo', + target: 'attachments', + storage: storage.name, + }, + ], + }); + + const { body } = await agent.resource('attachments').create({ + attachmentField: 'customers.file', + file: path.resolve(__dirname, './files/text.txt'), + }); + + // 文件的 url 是否正常生成 + expect(body.data.url).toBe(`${BASE_URL}/${urlPath}/${body.data.filename}`); + const url = body.data.url.replace(`http://localhost:${APP_PORT}`, ''); + const content = await agent.get(url); + expect(content.text.includes('Hello world!')).toBe(true); + }); }); }); @@ -356,7 +401,7 @@ describe('action', () => { const { documentRoot = 'storage/uploads' } = storage.options || {}; const destPath = path.resolve( path.isAbsolute(documentRoot) ? documentRoot : path.join(process.cwd(), documentRoot), - storage.path, + storage.path || '', ); const file = await fs.stat(path.join(destPath, attachment.filename)); expect(file).toBeTruthy(); @@ -382,7 +427,7 @@ describe('action', () => { const { documentRoot = path.join('storage', 'uploads') } = storage.options || {}; const destPath = path.resolve( path.isAbsolute(documentRoot) ? documentRoot : path.join(process.cwd(), documentRoot), - storage.path, + storage.path || '', ); const file = await fs.stat(path.join(destPath, attachment.filename)); expect(file).toBeTruthy(); @@ -414,7 +459,7 @@ describe('action', () => { const { documentRoot = path.join('storage', 'uploads') } = storage.options || {}; const destPath = path.resolve( path.isAbsolute(documentRoot) ? documentRoot : path.join(process.cwd(), documentRoot), - storage.path, + storage.path || '', ); const file1 = await fs.stat(path.join(destPath, f1.data.filename)); expect(file1).toBeTruthy(); @@ -443,7 +488,7 @@ describe('action', () => { const { documentRoot = path.join('storage', 'uploads') } = storage.options || {}; const destPath = path.resolve( path.isAbsolute(documentRoot) ? documentRoot : path.join(process.cwd(), documentRoot), - storage.path, + storage.path || '', ); const filePath = path.join(destPath, attachment.filename); const file = await fs.stat(filePath); diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/migrations/20250225112112-increase-url-length.test.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/migrations/20250225112112-increase-url-length.test.ts new file mode 100644 index 0000000000..99ec267160 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/migrations/20250225112112-increase-url-length.test.ts @@ -0,0 +1,194 @@ +/** + * 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 { MockServer, createMockServer } from '@nocobase/test'; +import Migration from '../../migrations/20250225112112-increase-url-length'; +import PluginCollectionManagerServer from '../../server'; + +describe('file-manager > migrations', () => { + let app: MockServer; + + beforeEach(async () => { + app = await createMockServer({ + plugins: ['nocobase'], + }); + await app.version.update('1.5.13'); + }); + + afterEach(async () => { + await app.destroy(); + }); + + test('migration', async () => { + await app.db.getRepository('collections').create({ + values: { + name: 'foo', + template: 'file', + createdBy: true, + updatedBy: true, + fields: [ + { + interface: 'input', + type: 'string', + name: 'title', + deletable: false, + uiSchema: { + type: 'string', + title: `{{t("Title")}}`, + 'x-component': 'Input', + }, + }, + // '系统文件名(含扩展名)', + { + interface: 'input', + type: 'string', + name: 'filename', + deletable: false, + uiSchema: { + type: 'string', + title: `{{t("File name")}}`, + 'x-component': 'Input', + 'x-read-pretty': true, + }, + }, + // '扩展名(含“.”)', + { + interface: 'input', + type: 'string', + name: 'extname', + deletable: false, + uiSchema: { + type: 'string', + title: `{{t("Extension name")}}`, + 'x-component': 'Input', + 'x-read-pretty': true, + }, + }, + // '文件体积(字节)', + { + interface: 'integer', + type: 'integer', + name: 'size', + deletable: false, + uiSchema: { + type: 'number', + title: `{{t("Size")}}`, + 'x-component': 'InputNumber', + 'x-read-pretty': true, + 'x-component-props': { + stringMode: true, + step: '0', + }, + }, + }, + { + interface: 'input', + type: 'string', + name: 'mimetype', + deletable: false, + uiSchema: { + type: 'string', + title: `{{t("MIME type")}}`, + 'x-component': 'Input', + 'x-read-pretty': true, + }, + }, + // '相对路径(含“/”前缀)', + { + interface: 'input', + type: 'string', + name: 'path', + deletable: false, + uiSchema: { + type: 'string', + title: `{{t("Path")}}`, + 'x-component': 'Input', + 'x-read-pretty': true, + }, + }, + // 文件的可访问地址 + { + interface: 'url', + type: 'string', + name: 'url', + deletable: false, + uiSchema: { + type: 'string', + title: `{{t("URL")}}`, + 'x-component': 'Input.URL', + 'x-read-pretty': true, + }, + }, + // 用于预览 + { + interface: 'url', + type: 'string', + name: 'preview', + field: 'url', // 直接引用 url 字段 + deletable: false, + uiSchema: { + type: 'string', + title: `{{t("Preview")}}`, + 'x-component': 'Preview', + 'x-read-pretty': true, + }, + }, + { + comment: '存储引擎', + type: 'belongsTo', + name: 'storage', + target: 'storages', + foreignKey: 'storageId', + deletable: false, + uiSchema: { + type: 'string', + title: `{{t("Storage")}}`, + 'x-component': 'Input', + 'x-read-pretty': true, + }, + }, + // '其他文件信息(如图片的宽高)', + { + type: 'jsonb', + name: 'meta', + deletable: false, + defaultValue: {}, + }, + ], + }, + context: {}, + }); + + const migration = new Migration({ + db: app.db, + // @ts-ignore + app: app, + plugin: app.pm.get(PluginCollectionManagerServer), + }); + await migration.up(); + const fileURL = await app.db.getRepository('fields').findOne({ + filter: { + collectionName: 'foo', + name: 'url', + }, + }); + expect(fileURL.type).toBe('text'); + const filePath = await app.db.getRepository('fields').findOne({ + filter: { + collectionName: 'foo', + name: 'path', + }, + }); + expect(filePath.type).toBe('text'); + + const storageCollection = app.db.getCollection('storages'); + const pathField = storageCollection.getField('path'); + expect(pathField.type).toBe('text'); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/collections/attachments.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/collections/attachments.ts index 48c88761ab..387f8736b2 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/collections/attachments.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/collections/attachments.ts @@ -57,7 +57,7 @@ export default defineCollection({ }, { comment: '相对路径(含“/”前缀)', - type: 'string', + type: 'text', name: 'path', }, { @@ -68,9 +68,8 @@ export default defineCollection({ }, { comment: '网络访问地址', - type: 'string', + type: 'text', name: 'url', - length: 1024, // formula: '{{ storage.baseUrl }}{{ path }}/{{ filename }}' }, ], diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/collections/storages.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/collections/storages.ts index 602d515223..b3566ef84a 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/collections/storages.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/collections/storages.ts @@ -47,7 +47,7 @@ export default defineCollection({ }, { comment: '存储相对路径模板', - type: 'string', + type: 'text', name: 'path', defaultValue: '', }, diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/migrations/20250224112112-increase-url-length.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/migrations/20250224112112-increase-url-length.ts deleted file mode 100644 index d21e843f4d..0000000000 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/migrations/20250224112112-increase-url-length.ts +++ /dev/null @@ -1,55 +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 { DataTypes } from 'sequelize'; -import { Migration } from '@nocobase/server'; - -export default class extends Migration { - on = 'afterLoad'; // 'beforeLoad' or 'afterLoad' - appVersion = '<1.5.14'; - - async up() { - const queryInterface = this.db.sequelize.getQueryInterface(); - const CollectionRepo = this.db.getRepository('collections'); - const FieldRepo = this.db.getRepository('fields'); - await this.db.sequelize.transaction(async (transaction) => { - const collections = await CollectionRepo.find({ - filter: { - 'options.template': 'file', - }, - transaction, - }); - collections.push({ - name: 'attachments', - }); - for (const item of collections) { - const collection = this.db.getCollection(item.name) || this.db.collection(item); - const tableName = collection.getTableNameWithSchema(); - await queryInterface.changeColumn( - tableName, - 'url', - { - type: DataTypes.STRING(1024), - }, - { transaction }, - ); - await FieldRepo.update({ - filter: { - collectionName: item.name, - name: 'url', - }, - values: { - length: 1024, - }, - transaction, - }); - } - }); - } -} diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/migrations/20250225112112-increase-url-length.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/migrations/20250225112112-increase-url-length.ts new file mode 100644 index 0000000000..01edbf70e9 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/migrations/20250225112112-increase-url-length.ts @@ -0,0 +1,85 @@ +/** + * 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 { DataTypes } from 'sequelize'; +import { Migration } from '@nocobase/server'; +import { CollectionRepository } from '@nocobase/plugin-data-source-main'; + +export default class extends Migration { + on = 'afterLoad'; // 'beforeLoad' or 'afterLoad' + appVersion = '<1.6.0'; + + async up() { + const queryInterface = this.db.sequelize.getQueryInterface(); + const CollectionRepo = this.db.getRepository('collections') as CollectionRepository; + const FieldRepo = this.db.getRepository('fields'); + const StorageRepo = this.db.getRepository('storages'); + await CollectionRepo.load({ + filter: { + 'options.template': 'file', + }, + }); + const collections = Array.from(this.db.collections.values()).filter( + (item) => item.name === 'attachments' || item.options.template === 'file', + ); + + await this.db.sequelize.transaction(async (transaction) => { + for (const collection of collections) { + const tableName = collection.getTableNameWithSchema(); + await queryInterface.changeColumn( + tableName, + 'url', + { + type: DataTypes.TEXT, + }, + { transaction }, + ); + await queryInterface.changeColumn( + tableName, + 'path', + { + type: DataTypes.TEXT, + }, + { transaction }, + ); + + await FieldRepo.update({ + filter: { + collectionName: collection.name, + name: ['url', 'path'], + }, + values: { + type: 'text', + length: null, + }, + transaction, + }); + } + + await queryInterface.changeColumn( + this.db.getCollection('storages').getTableNameWithSchema(), + 'path', + { + type: DataTypes.TEXT, + }, + { transaction }, + ); + await FieldRepo.update({ + filter: { + collectionName: 'storages', + name: 'path', + }, + values: { + type: 'text', + }, + transaction, + }); + }); + } +} 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 46fab821d7..215d2a62a4 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 @@ -25,7 +25,7 @@ export default class extends StorageType { make(storage) { return multer.diskStorage({ destination: function (req, file, cb) { - const destPath = path.join(getDocumentRoot(storage), storage.path); + const destPath = path.join(getDocumentRoot(storage), storage.path || ''); mkdirp(destPath, (err: Error | null) => cb(err, destPath)); }, filename: getFilename, @@ -40,6 +40,7 @@ export default class extends StorageType { options: { documentRoot: 'storage/uploads', }, + path: '', rules: { size: FILE_SIZE_LIMIT_DEFAULT, },