mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
refactor(plugin-file-manager): change storage type api and add plugin api (#6246)
* refactor(plugin-file-manager): change storage type api and add plugin api * fix(plugin-file-manager): fix test cases
This commit is contained in:
parent
2ac2b9a81c
commit
f8a5999903
@ -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'),
|
||||
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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),
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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<IStorage>();
|
||||
storageTypes = new Registry<StorageClassType>();
|
||||
storagesCache = new Map<number, StorageModel>();
|
||||
|
||||
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;
|
||||
|
@ -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)))];
|
||||
}
|
||||
|
@ -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<string> {
|
||||
return file.url;
|
||||
}
|
||||
}
|
||||
|
||||
export type StorageClassType = { new (storage: StorageModel): StorageType } & typeof StorageType;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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) })),
|
||||
},
|
||||
|
@ -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)))];
|
||||
|
Loading…
x
Reference in New Issue
Block a user