fix(plugin-file-manager): fix migration and add test cases (#6288)

* fix(plugin-file-manager): fix migration and add test cases

* fix(plugin-file-manager): fix test case fails for mysql

* fix(plugin-file-manager): fix test cases

* fix(plugin-file-manager): change migration name
This commit is contained in:
Junyi 2025-02-25 20:53:13 +08:00 committed by GitHub
parent e293b4b7d6
commit aa8b98d615
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 338 additions and 69 deletions

View File

@ -101,7 +101,7 @@ export class FileCollectionTemplate extends CollectionTemplate {
// 文件的可访问地址
{
interface: 'url',
type: 'string',
type: 'text',
name: 'url',
deletable: false,
length: 1024,

View File

@ -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);

View File

@ -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');
});
});

View File

@ -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 }}'
},
],

View File

@ -47,7 +47,7 @@ export default defineCollection({
},
{
comment: '存储相对路径模板',
type: 'string',
type: 'text',
name: 'path',
defaultValue: '',
},

View File

@ -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,
});
}
});
}
}

View File

@ -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,
});
});
}
}

View File

@ -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,
},