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..e937ff8da5 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 @@ -52,88 +52,6 @@ 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'), 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 new file mode 100644 index 0000000000..bfc30a4477 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/server.test.ts @@ -0,0 +1,156 @@ +/** + * 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 path from 'path'; + +import { getApp } from '.'; +import PluginFileManagerServer from '../server'; + +import { STORAGE_TYPE_LOCAL, FILE_FIELD_NAME } 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`; + +describe('file manager > server', () => { + let app; + let agent; + let db; + let plugin: PluginFileManagerServer; + let StorageRepo; + let AttachmentRepo; + let local; + + beforeEach(async () => { + app = await getApp(); + agent = app.agent(); + db = app.db; + plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer; + + AttachmentRepo = db.getCollection('attachments').repository; + StorageRepo = db.getCollection('storages').repository; + local = await StorageRepo.findOne({ + filter: { + type: STORAGE_TYPE_LOCAL, + }, + }); + }); + + afterEach(async () => { + await app.destroy(); + }); + + describe('api', () => { + describe('createFileRecord', () => { + it('should be create file record', async () => { + 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 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 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); + }); + }); + + describe('uploadFile', () => { + it('should be upload file', async () => { + 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); + }); + }); + + describe('getFileURL', () => { + it('local attachment without env', async () => { + 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(`${process.env.APP_PUBLIC_PATH?.replace(/\/$/g, '') || ''}${body.data.url}`); + }); + + it('local attachment with env', async () => { + const originalPath = process.env.APP_PUBLIC_PATH; + process.env.APP_PUBLIC_PATH = 'http://localhost'; + + 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}`); + + process.env.APP_PUBLIC_PATH = originalPath; + }); + }); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/storages/ali-oss.test.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/storages/ali-oss.test.ts index 15ad32bdfd..f4518a3a19 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/storages/ali-oss.test.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/storages/ali-oss.test.ts @@ -13,6 +13,7 @@ import AliOSSStorage from '../../storages/ali-oss'; import { FILE_FIELD_NAME } from '../../../constants'; import { getApp, requestFile } from '..'; import { Database } from '@nocobase/database'; +import PluginFileManagerServer from '../../server'; const itif = process.env.ALI_OSS_ACCESS_KEY_SECRET ? it : it.skip; @@ -23,19 +24,20 @@ describe('storage:ali-oss', () => { let AttachmentRepo; let StorageRepo; let storage; - const aliossStorage = new AliOSSStorage(); + let plugin: PluginFileManagerServer; beforeEach(async () => { app = await getApp(); agent = app.agent(); db = app.db; + plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer; AttachmentRepo = db.getCollection('attachments').repository; StorageRepo = db.getCollection('storages').repository; storage = await StorageRepo.create({ values: { - ...aliossStorage.defaults(), + ...AliOSSStorage.defaults(), name: 'ali-oss', default: true, path: 'test/path', @@ -107,7 +109,7 @@ describe('storage:ali-oss', () => { itif('destroy record should not delete file when paranoid', async () => { const paranoidStorage = await StorageRepo.create({ values: { - ...aliossStorage.defaults(), + ...AliOSSStorage.defaults(), name: 'ali-oss-2', path: 'test/nocobase', paranoid: true, @@ -134,4 +136,29 @@ describe('storage:ali-oss', () => { expect(content2.status).toBe(200); }); }); + + describe('plugin api', () => { + itif('getFileURL', async () => { + const options = AliOSSStorage.defaults(); + await StorageRepo.create({ + values: { + ...options, + name: 'ali-oss-2', + path: 'test/nocobase', + default: true, + }, + }); + + const { body } = await agent.resource('attachments').create({ + [FILE_FIELD_NAME]: path.resolve(__dirname, '../files/text.txt'), + }); + + const url = plugin.getFileURL(body.data); + expect(url).toBe(`${options.baseUrl}/${body.data.path}/${body.data.filename}`); + + // 通过 url 是否能正确访问 + const content1 = await requestFile(url, agent); + expect(content1.text).toBe('Hello world!\n'); + }); + }); }); diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/storages/s3.test.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/storages/s3.test.ts index a9f7804e5b..6b8e07394f 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/storages/s3.test.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/storages/s3.test.ts @@ -23,7 +23,6 @@ describe('storage:s3', () => { let AttachmentRepo; let StorageRepo; let storage; - const s3Storage = new S3Storage(); beforeEach(async () => { app = await getApp(); @@ -35,7 +34,7 @@ describe('storage:s3', () => { storage = await StorageRepo.create({ values: { - ...s3Storage.defaults(), + ...S3Storage.defaults(), name: 's3', default: true, path: 'test/path', @@ -106,7 +105,7 @@ describe('storage:s3', () => { itif('destroy record should not delete file when paranoid', async () => { const paranoidStorage = await StorageRepo.create({ values: { - ...s3Storage.defaults(), + ...S3Storage.defaults(), name: 's3-2', path: 'test/nocobase', paranoid: true, diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/storages/tx-cos.test.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/storages/tx-cos.test.ts index 53f7ab6280..b98d0737af 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/storages/tx-cos.test.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/storages/tx-cos.test.ts @@ -21,7 +21,6 @@ describe('storage:tx-cos', () => { let agent; let db: Database; let storage; - const txStorage = new TXCOSStorage(); beforeEach(async () => { app = await getApp(); @@ -30,7 +29,7 @@ describe('storage:tx-cos', () => { const Storage = db.getCollection('storages').model; storage = await Storage.create({ - ...txStorage.defaults(), + ...TXCOSStorage.defaults(), name: 'tx-cos', default: true, path: 'test/path', 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 f233d36123..e8066aad02 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 @@ -20,6 +20,7 @@ import { LIMIT_FILES, } from '../../constants'; import * as Rules from '../rules'; +import { StorageClassType } from '../storages'; // TODO(optimize): 需要优化错误处理,计算失败后需要抛出对应错误,以便程序处理 function getFileFilter(storage) { @@ -39,8 +40,8 @@ export function getFileData(ctx: Context) { return ctx.throw(400, 'file validation failed'); } - const storageConfig = ctx.app.pm.get(Plugin).storageTypes.get(storage.type); - const { [storageConfig.filenameKey || 'filename']: name } = file; + const StorageType = ctx.app.pm.get(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); @@ -48,6 +49,8 @@ export function getFileData(ctx: Context) { const baseUrl = storage.baseUrl.replace(/\/+$/, ''); const pathname = [path, filename].filter(Boolean).join('/'); + const storageInstance = new StorageType(storage); + return { title: Buffer.from(file.originalname, 'latin1').toString('utf8').replace(extname, ''), filename, @@ -61,7 +64,7 @@ export function getFileData(ctx: Context) { // @ts-ignore meta: ctx.request.body, storageId: storage.id, - ...(storageConfig.getFileData ? storageConfig.getFileData(file) : {}), + ...(storageInstance.getFileData ? storageInstance.getFileData(file) : {}), }; } @@ -72,11 +75,12 @@ async function multipart(ctx: Context, next: Next) { return ctx.throw(500); } - const storageConfig = ctx.app.pm.get(Plugin).storageTypes.get(storage.type); - if (!storageConfig) { + const StorageType = ctx.app.pm.get(Plugin).storageTypes.get(storage.type) as StorageClassType; + if (!StorageType) { ctx.logger.error(`[file-manager] storage type "${storage.type}" is not defined`); return ctx.throw(500); } + const storageInstance = new StorageType(storage); const multerOptions = { fileFilter: getFileFilter(storage), @@ -84,7 +88,7 @@ async function multipart(ctx: Context, next: Next) { // 每次只允许提交一个文件 files: LIMIT_FILES, }, - storage: storageConfig.make(storage), + storage: storageInstance.make(), }; multerOptions.limits['fileSize'] = Math.min( Math.max(FILE_SIZE_LIMIT_MIN, storage.rules.size ?? FILE_SIZE_LIMIT_DEFAULT), diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/index.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/index.ts index 37b4265659..10daee88fe 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/index.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/index.ts @@ -10,7 +10,7 @@ import { StorageEngine } from 'multer'; export * from '../constants'; -export { AttachmentModel, default, IStorage, PluginFileManagerServer, StorageModel } from './server'; +export { AttachmentModel, default, PluginFileManagerServer, StorageModel } from './server'; export { StorageType } from './storages'; 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 a353170d28..4fd7eed295 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts @@ -19,7 +19,7 @@ import { FileModel } from './FileModel'; import initActions from './actions'; import { getFileData } from './actions/attachments'; import { AttachmentInterface } from './interfaces/attachment-interface'; -import { AttachmentModel, IStorage, StorageModel } from './storages'; +import { AttachmentModel, StorageClassType, StorageModel, StorageType } from './storages'; import StorageTypeAliOss from './storages/ali-oss'; import StorageTypeLocal from './storages/local'; import StorageTypeS3 from './storages/s3'; @@ -53,7 +53,7 @@ export type UploadFileOptions = { }; export class PluginFileManagerServer extends Plugin { - storageTypes = new Registry(); + storageTypes = new Registry(); storagesCache = new Map(); afterDestroy = async (record: Model, options) => { @@ -66,15 +66,16 @@ export class PluginFileManagerServer extends Plugin { if (storage?.paranoid) { return; } - const storageConfig = this.storageTypes.get(storage.type); - const result = await storageConfig.delete(storage, [record as unknown as AttachmentModel]); + const Type = this.storageTypes.get(storage.type); + const storageConfig = new Type(storage); + const result = await storageConfig.delete([record as unknown as AttachmentModel]); if (!result[0]) { throw new FileDeleteError('Failed to delete file', record); } }; - registerStorageType(type: string, options: IStorage) { - this.storageTypes.register(type, options); + registerStorageType(type: string, Type: StorageClassType) { + this.storageTypes.register(type, Type); } async createFileRecord(options: FileRecordOptions) { @@ -98,21 +99,15 @@ export class PluginFileManagerServer extends Plugin { const storageRepository = this.db.getRepository('storages'); let storageInstance; - if (storageName) { - storageInstance = await storageRepository.findOne({ - filter: { - name: storageName, - }, - }); - } - - if (!storageInstance) { - storageInstance = await storageRepository.findOne({ - filter: { - default: true, - }, - }); - } + storageInstance = await storageRepository.findOne({ + filter: storageName + ? { + name: storageName, + } + : { + default: true, + }, + }); const fileStream = fs.createReadStream(filePath); @@ -126,13 +121,14 @@ export class PluginFileManagerServer extends Plugin { storageInstance.options['documentRoot'] = documentRoot; } - const storageConfig = this.storageTypes.get(storageInstance.type); + const storageType = this.storageTypes.get(storageInstance.type); + const storage = new storageType(storageInstance); - if (!storageConfig) { + if (!storage) { throw new Error(`[file-manager] storage type "${storageInstance.type}" is not defined`); } - const engine = storageConfig.make(storageInstance); + const engine = storage.make(); const file = { originalname: basename(filePath), @@ -166,14 +162,14 @@ export class PluginFileManagerServer extends Plugin { } async install() { - const defaultStorageConfig = this.storageTypes.get(DEFAULT_STORAGE_TYPE); + const defaultStorageType = this.storageTypes.get(DEFAULT_STORAGE_TYPE); - if (defaultStorageConfig) { + if (defaultStorageType) { const Storage = this.db.getCollection('storages'); if ( await Storage.repository.findOne({ filter: { - name: defaultStorageConfig.defaults().name, + name: defaultStorageType.defaults().name, }, }) ) { @@ -181,7 +177,7 @@ export class PluginFileManagerServer extends Plugin { } await Storage.repository.create({ values: { - ...defaultStorageConfig.defaults(), + ...defaultStorageType.defaults(), type: DEFAULT_STORAGE_TYPE, default: true, }, @@ -219,10 +215,10 @@ export class PluginFileManagerServer extends Plugin { async load() { this.db.on('afterDestroy', this.afterDestroy); - this.storageTypes.register(STORAGE_TYPE_LOCAL, new StorageTypeLocal()); - this.storageTypes.register(STORAGE_TYPE_ALI_OSS, new StorageTypeAliOss()); - this.storageTypes.register(STORAGE_TYPE_S3, new StorageTypeS3()); - this.storageTypes.register(STORAGE_TYPE_TX_COS, new StorageTypeTxCos()); + this.storageTypes.register(STORAGE_TYPE_LOCAL, StorageTypeLocal); + this.storageTypes.register(STORAGE_TYPE_ALI_OSS, StorageTypeAliOss); + this.storageTypes.register(STORAGE_TYPE_S3, StorageTypeS3); + this.storageTypes.register(STORAGE_TYPE_TX_COS, StorageTypeTxCos); const Storage = this.db.getModel('storages'); Storage.afterSave((m, { transaction }) => { @@ -284,6 +280,12 @@ export class PluginFileManagerServer extends Plugin { this.app.db.interfaceManager.registerInterfaceType('attachment', AttachmentInterface); } + + getFileURL(file: AttachmentModel) { + const storage = this.storagesCache.get(file.storageId); + const storageType = this.storageTypes.get(storage.type); + return new storageType(storage).getFileURL(file); + } } export default PluginFileManagerServer; diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/ali-oss.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/ali-oss.ts index 37efd83c20..d57db86cbd 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/ali-oss.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/ali-oss.ts @@ -12,14 +12,7 @@ import { STORAGE_TYPE_ALI_OSS } from '../../constants'; import { cloudFilenameGetter, getFileKey } from '../utils'; export default class extends StorageType { - make(storage) { - const createAliOssStorage = require('multer-aliyun-oss'); - return new createAliOssStorage({ - config: storage.options, - filename: cloudFilenameGetter(storage), - }); - } - defaults() { + static defaults() { return { title: '阿里云对象存储', type: STORAGE_TYPE_ALI_OSS, @@ -33,8 +26,16 @@ export default class extends StorageType { }, }; } - async delete(storage, records: AttachmentModel[]): Promise<[number, AttachmentModel[]]> { - const { client } = this.make(storage); + + make() { + const createAliOssStorage = require('multer-aliyun-oss'); + return new createAliOssStorage({ + config: this.storage.options, + filename: cloudFilenameGetter(this.storage), + }); + } + async delete(records: AttachmentModel[]): Promise<[number, AttachmentModel[]]> { + const { client } = this.make(); const { deleted } = await client.deleteMulti(records.map(getFileKey)); return [deleted.length, records.filter((record) => !deleted.find((item) => item.Key === getFileKey(record)))]; } 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 5d053b4f7b..e3ed53f4c6 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 @@ -27,21 +27,23 @@ export interface AttachmentModel { title: string; filename: string; path: string; + url: string; + storageId: number; } -export interface IStorage { - filenameKey?: string; - middleware?(app: Application): void; +export abstract class StorageType { + static defaults(): StorageModel { + return {} as StorageModel; + } + static filenameKey?: string; + constructor(public storage: StorageModel) {} + abstract make(): StorageEngine; + abstract delete(records: AttachmentModel[]): [number, AttachmentModel[]] | Promise<[number, AttachmentModel[]]>; + getFileData?(file: { [key: string]: any }): { [key: string]: any }; - make(storage: StorageModel): StorageEngine; - defaults(): StorageModel; - delete(storage: StorageModel, records: AttachmentModel[]): Promise<[number, AttachmentModel[]]>; -} - -export abstract class StorageType implements IStorage { - abstract make(storage: StorageModel): StorageEngine; - abstract delete(storage: StorageModel, records: AttachmentModel[]): Promise<[number, AttachmentModel[]]>; - defaults(): StorageModel { - return {} as any; + getFileURL(file: AttachmentModel): string | Promise { + return file.url; } } + +export type StorageClassType = { new (storage: StorageModel): StorageType } & typeof StorageType; 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..2079b012ca 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 @@ -22,16 +22,7 @@ function getDocumentRoot(storage): string { } export default class extends StorageType { - make(storage) { - return multer.diskStorage({ - destination: function (req, file, cb) { - const destPath = path.join(getDocumentRoot(storage), storage.path); - mkdirp(destPath, (err: Error | null) => cb(err, destPath)); - }, - filename: getFilename, - }); - } - defaults() { + static defaults() { return { title: 'Local storage', type: STORAGE_TYPE_LOCAL, @@ -45,8 +36,18 @@ export default class extends StorageType { }, }; } - async delete(storage, records: AttachmentModel[]): Promise<[number, AttachmentModel[]]> { - const documentRoot = getDocumentRoot(storage); + + make() { + return multer.diskStorage({ + destination: (req, file, cb) => { + const destPath = path.join(getDocumentRoot(this.storage), this.storage.path); + mkdirp(destPath, (err: Error | null) => cb(err, destPath)); + }, + filename: getFilename, + }); + } + async delete(records: AttachmentModel[]): Promise<[number, AttachmentModel[]]> { + const documentRoot = getDocumentRoot(this.storage); let count = 0; const undeleted = []; await records.reduce( @@ -70,4 +71,7 @@ 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; + } } 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 f8820d5e51..19670740a2 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 @@ -12,11 +12,27 @@ import { STORAGE_TYPE_S3 } from '../../constants'; import { cloudFilenameGetter, getFileKey } from '../utils'; export default class extends StorageType { - filenameKey = 'key'; - make(storage) { + static defaults() { + return { + title: 'AWS S3', + name: 'aws-s3', + type: STORAGE_TYPE_S3, + baseUrl: process.env.AWS_S3_STORAGE_BASE_URL, + options: { + region: process.env.AWS_S3_REGION, + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + bucket: process.env.AWS_S3_BUCKET, + }, + }; + } + + static filenameKey = 'key'; + + make() { const { S3Client } = require('@aws-sdk/client-s3'); const multerS3 = require('multer-s3'); - const { accessKeyId, secretAccessKey, bucket, acl = 'public-read', ...options } = storage.options; + const { accessKeyId, secretAccessKey, bucket, acl = 'public-read', ...options } = this.storage.options; if (options.endpoint) { options.forcePathStyle = true; } else { @@ -42,29 +58,16 @@ export default class extends StorageType { multerS3.AUTO_CONTENT_TYPE(req, file, cb); }, - key: cloudFilenameGetter(storage), + key: cloudFilenameGetter(this.storage), }); } - defaults() { - return { - title: 'AWS S3', - name: 'aws-s3', - type: STORAGE_TYPE_S3, - baseUrl: process.env.AWS_S3_STORAGE_BASE_URL, - options: { - region: process.env.AWS_S3_REGION, - accessKeyId: process.env.AWS_ACCESS_KEY_ID, - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, - bucket: process.env.AWS_S3_BUCKET, - }, - }; - } - async delete(storage, records: AttachmentModel[]): Promise<[number, AttachmentModel[]]> { + + async delete(records: AttachmentModel[]): Promise<[number, AttachmentModel[]]> { const { DeleteObjectsCommand } = require('@aws-sdk/client-s3'); - const { s3 } = this.make(storage); + const { s3 } = this.make(); const { Deleted } = await s3.send( new DeleteObjectsCommand({ - Bucket: storage.options.bucket, + Bucket: this.storage.options.bucket, Delete: { Objects: records.map((record) => ({ Key: getFileKey(record) })), }, diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/tx-cos.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/tx-cos.ts index 260117a545..58b01e915f 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/tx-cos.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/tx-cos.ts @@ -14,18 +14,7 @@ import { STORAGE_TYPE_TX_COS } from '../../constants'; import { getFilename, getFileKey } from '../utils'; export default class extends StorageType { - filenameKey = 'url'; - make(storage) { - const createTxCosStorage = require('multer-cos'); - return new createTxCosStorage({ - cos: { - ...storage.options, - dir: (storage.path ?? '').replace(/\/+$/, ''), - }, - filename: getFilename, - }); - } - defaults() { + static defaults() { return { title: '腾讯云对象存储', type: STORAGE_TYPE_TX_COS, @@ -39,11 +28,24 @@ export default class extends StorageType { }, }; } - async delete(storage, records: AttachmentModel[]): Promise<[number, AttachmentModel[]]> { - const { cos } = this.make(storage); + + static filenameKey = 'url'; + + make() { + const createTxCosStorage = require('multer-cos'); + return new createTxCosStorage({ + cos: { + ...this.storage.options, + dir: (this.storage.path ?? '').replace(/\/+$/, ''), + }, + filename: getFilename, + }); + } + async delete(records: AttachmentModel[]): Promise<[number, AttachmentModel[]]> { + const { cos } = this.make(); const { Deleted } = await promisify(cos.deleteMultipleObject).call(cos, { - Region: storage.options.Region, - Bucket: storage.options.Bucket, + Region: this.storage.options.Region, + Bucket: this.storage.options.Bucket, Objects: records.map((record) => ({ Key: getFileKey(record) })), }); return [Deleted.length, records.filter((record) => !Deleted.find((item) => item.Key === getFileKey(record)))];