mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 21:49:25 +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('create / upload', () => {
|
||||||
describe('default storage', () => {
|
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 () => {
|
it('upload file should be ok', async () => {
|
||||||
const { body } = await agent.resource('attachments').create({
|
const { body } = await agent.resource('attachments').create({
|
||||||
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
|
[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 { FILE_FIELD_NAME } from '../../../constants';
|
||||||
import { getApp, requestFile } from '..';
|
import { getApp, requestFile } from '..';
|
||||||
import { Database } from '@nocobase/database';
|
import { Database } from '@nocobase/database';
|
||||||
|
import PluginFileManagerServer from '../../server';
|
||||||
|
|
||||||
const itif = process.env.ALI_OSS_ACCESS_KEY_SECRET ? it : it.skip;
|
const itif = process.env.ALI_OSS_ACCESS_KEY_SECRET ? it : it.skip;
|
||||||
|
|
||||||
@ -23,19 +24,20 @@ describe('storage:ali-oss', () => {
|
|||||||
let AttachmentRepo;
|
let AttachmentRepo;
|
||||||
let StorageRepo;
|
let StorageRepo;
|
||||||
let storage;
|
let storage;
|
||||||
const aliossStorage = new AliOSSStorage();
|
let plugin: PluginFileManagerServer;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
app = await getApp();
|
app = await getApp();
|
||||||
agent = app.agent();
|
agent = app.agent();
|
||||||
db = app.db;
|
db = app.db;
|
||||||
|
plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
|
||||||
|
|
||||||
AttachmentRepo = db.getCollection('attachments').repository;
|
AttachmentRepo = db.getCollection('attachments').repository;
|
||||||
StorageRepo = db.getCollection('storages').repository;
|
StorageRepo = db.getCollection('storages').repository;
|
||||||
|
|
||||||
storage = await StorageRepo.create({
|
storage = await StorageRepo.create({
|
||||||
values: {
|
values: {
|
||||||
...aliossStorage.defaults(),
|
...AliOSSStorage.defaults(),
|
||||||
name: 'ali-oss',
|
name: 'ali-oss',
|
||||||
default: true,
|
default: true,
|
||||||
path: 'test/path',
|
path: 'test/path',
|
||||||
@ -107,7 +109,7 @@ describe('storage:ali-oss', () => {
|
|||||||
itif('destroy record should not delete file when paranoid', async () => {
|
itif('destroy record should not delete file when paranoid', async () => {
|
||||||
const paranoidStorage = await StorageRepo.create({
|
const paranoidStorage = await StorageRepo.create({
|
||||||
values: {
|
values: {
|
||||||
...aliossStorage.defaults(),
|
...AliOSSStorage.defaults(),
|
||||||
name: 'ali-oss-2',
|
name: 'ali-oss-2',
|
||||||
path: 'test/nocobase',
|
path: 'test/nocobase',
|
||||||
paranoid: true,
|
paranoid: true,
|
||||||
@ -134,4 +136,29 @@ describe('storage:ali-oss', () => {
|
|||||||
expect(content2.status).toBe(200);
|
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 AttachmentRepo;
|
||||||
let StorageRepo;
|
let StorageRepo;
|
||||||
let storage;
|
let storage;
|
||||||
const s3Storage = new S3Storage();
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
app = await getApp();
|
app = await getApp();
|
||||||
@ -35,7 +34,7 @@ describe('storage:s3', () => {
|
|||||||
|
|
||||||
storage = await StorageRepo.create({
|
storage = await StorageRepo.create({
|
||||||
values: {
|
values: {
|
||||||
...s3Storage.defaults(),
|
...S3Storage.defaults(),
|
||||||
name: 's3',
|
name: 's3',
|
||||||
default: true,
|
default: true,
|
||||||
path: 'test/path',
|
path: 'test/path',
|
||||||
@ -106,7 +105,7 @@ describe('storage:s3', () => {
|
|||||||
itif('destroy record should not delete file when paranoid', async () => {
|
itif('destroy record should not delete file when paranoid', async () => {
|
||||||
const paranoidStorage = await StorageRepo.create({
|
const paranoidStorage = await StorageRepo.create({
|
||||||
values: {
|
values: {
|
||||||
...s3Storage.defaults(),
|
...S3Storage.defaults(),
|
||||||
name: 's3-2',
|
name: 's3-2',
|
||||||
path: 'test/nocobase',
|
path: 'test/nocobase',
|
||||||
paranoid: true,
|
paranoid: true,
|
||||||
|
@ -21,7 +21,6 @@ describe('storage:tx-cos', () => {
|
|||||||
let agent;
|
let agent;
|
||||||
let db: Database;
|
let db: Database;
|
||||||
let storage;
|
let storage;
|
||||||
const txStorage = new TXCOSStorage();
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
app = await getApp();
|
app = await getApp();
|
||||||
@ -30,7 +29,7 @@ describe('storage:tx-cos', () => {
|
|||||||
|
|
||||||
const Storage = db.getCollection('storages').model;
|
const Storage = db.getCollection('storages').model;
|
||||||
storage = await Storage.create({
|
storage = await Storage.create({
|
||||||
...txStorage.defaults(),
|
...TXCOSStorage.defaults(),
|
||||||
name: 'tx-cos',
|
name: 'tx-cos',
|
||||||
default: true,
|
default: true,
|
||||||
path: 'test/path',
|
path: 'test/path',
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
LIMIT_FILES,
|
LIMIT_FILES,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import * as Rules from '../rules';
|
import * as Rules from '../rules';
|
||||||
|
import { StorageClassType } from '../storages';
|
||||||
|
|
||||||
// TODO(optimize): 需要优化错误处理,计算失败后需要抛出对应错误,以便程序处理
|
// TODO(optimize): 需要优化错误处理,计算失败后需要抛出对应错误,以便程序处理
|
||||||
function getFileFilter(storage) {
|
function getFileFilter(storage) {
|
||||||
@ -39,8 +40,8 @@ export function getFileData(ctx: Context) {
|
|||||||
return ctx.throw(400, 'file validation failed');
|
return ctx.throw(400, 'file validation failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const storageConfig = ctx.app.pm.get(Plugin).storageTypes.get(storage.type);
|
const StorageType = ctx.app.pm.get(Plugin).storageTypes.get(storage.type) as StorageClassType;
|
||||||
const { [storageConfig.filenameKey || 'filename']: name } = file;
|
const { [StorageType.filenameKey || 'filename']: name } = file;
|
||||||
// make compatible filename across cloud service (with path)
|
// make compatible filename across cloud service (with path)
|
||||||
const filename = Path.basename(name);
|
const filename = Path.basename(name);
|
||||||
const extname = Path.extname(filename);
|
const extname = Path.extname(filename);
|
||||||
@ -48,6 +49,8 @@ export function getFileData(ctx: Context) {
|
|||||||
const baseUrl = storage.baseUrl.replace(/\/+$/, '');
|
const baseUrl = storage.baseUrl.replace(/\/+$/, '');
|
||||||
const pathname = [path, filename].filter(Boolean).join('/');
|
const pathname = [path, filename].filter(Boolean).join('/');
|
||||||
|
|
||||||
|
const storageInstance = new StorageType(storage);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: Buffer.from(file.originalname, 'latin1').toString('utf8').replace(extname, ''),
|
title: Buffer.from(file.originalname, 'latin1').toString('utf8').replace(extname, ''),
|
||||||
filename,
|
filename,
|
||||||
@ -61,7 +64,7 @@ export function getFileData(ctx: Context) {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
meta: ctx.request.body,
|
meta: ctx.request.body,
|
||||||
storageId: storage.id,
|
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);
|
return ctx.throw(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
const storageConfig = ctx.app.pm.get(Plugin).storageTypes.get(storage.type);
|
const StorageType = ctx.app.pm.get(Plugin).storageTypes.get(storage.type) as StorageClassType;
|
||||||
if (!storageConfig) {
|
if (!StorageType) {
|
||||||
ctx.logger.error(`[file-manager] storage type "${storage.type}" is not defined`);
|
ctx.logger.error(`[file-manager] storage type "${storage.type}" is not defined`);
|
||||||
return ctx.throw(500);
|
return ctx.throw(500);
|
||||||
}
|
}
|
||||||
|
const storageInstance = new StorageType(storage);
|
||||||
|
|
||||||
const multerOptions = {
|
const multerOptions = {
|
||||||
fileFilter: getFileFilter(storage),
|
fileFilter: getFileFilter(storage),
|
||||||
@ -84,7 +88,7 @@ async function multipart(ctx: Context, next: Next) {
|
|||||||
// 每次只允许提交一个文件
|
// 每次只允许提交一个文件
|
||||||
files: LIMIT_FILES,
|
files: LIMIT_FILES,
|
||||||
},
|
},
|
||||||
storage: storageConfig.make(storage),
|
storage: storageInstance.make(),
|
||||||
};
|
};
|
||||||
multerOptions.limits['fileSize'] = Math.min(
|
multerOptions.limits['fileSize'] = Math.min(
|
||||||
Math.max(FILE_SIZE_LIMIT_MIN, storage.rules.size ?? FILE_SIZE_LIMIT_DEFAULT),
|
Math.max(FILE_SIZE_LIMIT_MIN, storage.rules.size ?? FILE_SIZE_LIMIT_DEFAULT),
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
import { StorageEngine } from 'multer';
|
import { StorageEngine } from 'multer';
|
||||||
|
|
||||||
export * from '../constants';
|
export * from '../constants';
|
||||||
export { AttachmentModel, default, IStorage, PluginFileManagerServer, StorageModel } from './server';
|
export { AttachmentModel, default, PluginFileManagerServer, StorageModel } from './server';
|
||||||
|
|
||||||
export { StorageType } from './storages';
|
export { StorageType } from './storages';
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ import { FileModel } from './FileModel';
|
|||||||
import initActions from './actions';
|
import initActions from './actions';
|
||||||
import { getFileData } from './actions/attachments';
|
import { getFileData } from './actions/attachments';
|
||||||
import { AttachmentInterface } from './interfaces/attachment-interface';
|
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 StorageTypeAliOss from './storages/ali-oss';
|
||||||
import StorageTypeLocal from './storages/local';
|
import StorageTypeLocal from './storages/local';
|
||||||
import StorageTypeS3 from './storages/s3';
|
import StorageTypeS3 from './storages/s3';
|
||||||
@ -53,7 +53,7 @@ export type UploadFileOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class PluginFileManagerServer extends Plugin {
|
export class PluginFileManagerServer extends Plugin {
|
||||||
storageTypes = new Registry<IStorage>();
|
storageTypes = new Registry<StorageClassType>();
|
||||||
storagesCache = new Map<number, StorageModel>();
|
storagesCache = new Map<number, StorageModel>();
|
||||||
|
|
||||||
afterDestroy = async (record: Model, options) => {
|
afterDestroy = async (record: Model, options) => {
|
||||||
@ -66,15 +66,16 @@ export class PluginFileManagerServer extends Plugin {
|
|||||||
if (storage?.paranoid) {
|
if (storage?.paranoid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const storageConfig = this.storageTypes.get(storage.type);
|
const Type = this.storageTypes.get(storage.type);
|
||||||
const result = await storageConfig.delete(storage, [record as unknown as AttachmentModel]);
|
const storageConfig = new Type(storage);
|
||||||
|
const result = await storageConfig.delete([record as unknown as AttachmentModel]);
|
||||||
if (!result[0]) {
|
if (!result[0]) {
|
||||||
throw new FileDeleteError('Failed to delete file', record);
|
throw new FileDeleteError('Failed to delete file', record);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
registerStorageType(type: string, options: IStorage) {
|
registerStorageType(type: string, Type: StorageClassType) {
|
||||||
this.storageTypes.register(type, options);
|
this.storageTypes.register(type, Type);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createFileRecord(options: FileRecordOptions) {
|
async createFileRecord(options: FileRecordOptions) {
|
||||||
@ -98,21 +99,15 @@ export class PluginFileManagerServer extends Plugin {
|
|||||||
const storageRepository = this.db.getRepository('storages');
|
const storageRepository = this.db.getRepository('storages');
|
||||||
let storageInstance;
|
let storageInstance;
|
||||||
|
|
||||||
if (storageName) {
|
storageInstance = await storageRepository.findOne({
|
||||||
storageInstance = await storageRepository.findOne({
|
filter: storageName
|
||||||
filter: {
|
? {
|
||||||
name: storageName,
|
name: storageName,
|
||||||
},
|
}
|
||||||
});
|
: {
|
||||||
}
|
default: true,
|
||||||
|
},
|
||||||
if (!storageInstance) {
|
});
|
||||||
storageInstance = await storageRepository.findOne({
|
|
||||||
filter: {
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileStream = fs.createReadStream(filePath);
|
const fileStream = fs.createReadStream(filePath);
|
||||||
|
|
||||||
@ -126,13 +121,14 @@ export class PluginFileManagerServer extends Plugin {
|
|||||||
storageInstance.options['documentRoot'] = documentRoot;
|
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`);
|
throw new Error(`[file-manager] storage type "${storageInstance.type}" is not defined`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const engine = storageConfig.make(storageInstance);
|
const engine = storage.make();
|
||||||
|
|
||||||
const file = {
|
const file = {
|
||||||
originalname: basename(filePath),
|
originalname: basename(filePath),
|
||||||
@ -166,14 +162,14 @@ export class PluginFileManagerServer extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async install() {
|
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');
|
const Storage = this.db.getCollection('storages');
|
||||||
if (
|
if (
|
||||||
await Storage.repository.findOne({
|
await Storage.repository.findOne({
|
||||||
filter: {
|
filter: {
|
||||||
name: defaultStorageConfig.defaults().name,
|
name: defaultStorageType.defaults().name,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
@ -181,7 +177,7 @@ export class PluginFileManagerServer extends Plugin {
|
|||||||
}
|
}
|
||||||
await Storage.repository.create({
|
await Storage.repository.create({
|
||||||
values: {
|
values: {
|
||||||
...defaultStorageConfig.defaults(),
|
...defaultStorageType.defaults(),
|
||||||
type: DEFAULT_STORAGE_TYPE,
|
type: DEFAULT_STORAGE_TYPE,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
@ -219,10 +215,10 @@ export class PluginFileManagerServer extends Plugin {
|
|||||||
async load() {
|
async load() {
|
||||||
this.db.on('afterDestroy', this.afterDestroy);
|
this.db.on('afterDestroy', this.afterDestroy);
|
||||||
|
|
||||||
this.storageTypes.register(STORAGE_TYPE_LOCAL, new StorageTypeLocal());
|
this.storageTypes.register(STORAGE_TYPE_LOCAL, StorageTypeLocal);
|
||||||
this.storageTypes.register(STORAGE_TYPE_ALI_OSS, new StorageTypeAliOss());
|
this.storageTypes.register(STORAGE_TYPE_ALI_OSS, StorageTypeAliOss);
|
||||||
this.storageTypes.register(STORAGE_TYPE_S3, new StorageTypeS3());
|
this.storageTypes.register(STORAGE_TYPE_S3, StorageTypeS3);
|
||||||
this.storageTypes.register(STORAGE_TYPE_TX_COS, new StorageTypeTxCos());
|
this.storageTypes.register(STORAGE_TYPE_TX_COS, StorageTypeTxCos);
|
||||||
|
|
||||||
const Storage = this.db.getModel('storages');
|
const Storage = this.db.getModel('storages');
|
||||||
Storage.afterSave((m, { transaction }) => {
|
Storage.afterSave((m, { transaction }) => {
|
||||||
@ -284,6 +280,12 @@ export class PluginFileManagerServer extends Plugin {
|
|||||||
|
|
||||||
this.app.db.interfaceManager.registerInterfaceType('attachment', AttachmentInterface);
|
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;
|
export default PluginFileManagerServer;
|
||||||
|
@ -12,14 +12,7 @@ import { STORAGE_TYPE_ALI_OSS } from '../../constants';
|
|||||||
import { cloudFilenameGetter, getFileKey } from '../utils';
|
import { cloudFilenameGetter, getFileKey } from '../utils';
|
||||||
|
|
||||||
export default class extends StorageType {
|
export default class extends StorageType {
|
||||||
make(storage) {
|
static defaults() {
|
||||||
const createAliOssStorage = require('multer-aliyun-oss');
|
|
||||||
return new createAliOssStorage({
|
|
||||||
config: storage.options,
|
|
||||||
filename: cloudFilenameGetter(storage),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
defaults() {
|
|
||||||
return {
|
return {
|
||||||
title: '阿里云对象存储',
|
title: '阿里云对象存储',
|
||||||
type: STORAGE_TYPE_ALI_OSS,
|
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));
|
const { deleted } = await client.deleteMulti(records.map(getFileKey));
|
||||||
return [deleted.length, records.filter((record) => !deleted.find((item) => item.Key === getFileKey(record)))];
|
return [deleted.length, records.filter((record) => !deleted.find((item) => item.Key === getFileKey(record)))];
|
||||||
}
|
}
|
||||||
|
@ -27,21 +27,23 @@ export interface AttachmentModel {
|
|||||||
title: string;
|
title: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
url: string;
|
||||||
|
storageId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IStorage {
|
export abstract class StorageType {
|
||||||
filenameKey?: string;
|
static defaults(): StorageModel {
|
||||||
middleware?(app: Application): void;
|
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 };
|
getFileData?(file: { [key: string]: any }): { [key: string]: any };
|
||||||
make(storage: StorageModel): StorageEngine;
|
getFileURL(file: AttachmentModel): string | Promise<string> {
|
||||||
defaults(): StorageModel;
|
return file.url;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StorageClassType = { new (storage: StorageModel): StorageType } & typeof StorageType;
|
||||||
|
@ -22,16 +22,7 @@ function getDocumentRoot(storage): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class extends StorageType {
|
export default class extends StorageType {
|
||||||
make(storage) {
|
static defaults() {
|
||||||
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() {
|
|
||||||
return {
|
return {
|
||||||
title: 'Local storage',
|
title: 'Local storage',
|
||||||
type: STORAGE_TYPE_LOCAL,
|
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;
|
let count = 0;
|
||||||
const undeleted = [];
|
const undeleted = [];
|
||||||
await records.reduce(
|
await records.reduce(
|
||||||
@ -70,4 +71,7 @@ export default class extends StorageType {
|
|||||||
|
|
||||||
return [count, undeleted];
|
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';
|
import { cloudFilenameGetter, getFileKey } from '../utils';
|
||||||
|
|
||||||
export default class extends StorageType {
|
export default class extends StorageType {
|
||||||
filenameKey = 'key';
|
static defaults() {
|
||||||
make(storage) {
|
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 { S3Client } = require('@aws-sdk/client-s3');
|
||||||
const multerS3 = require('multer-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) {
|
if (options.endpoint) {
|
||||||
options.forcePathStyle = true;
|
options.forcePathStyle = true;
|
||||||
} else {
|
} else {
|
||||||
@ -42,29 +58,16 @@ export default class extends StorageType {
|
|||||||
|
|
||||||
multerS3.AUTO_CONTENT_TYPE(req, file, cb);
|
multerS3.AUTO_CONTENT_TYPE(req, file, cb);
|
||||||
},
|
},
|
||||||
key: cloudFilenameGetter(storage),
|
key: cloudFilenameGetter(this.storage),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
defaults() {
|
|
||||||
return {
|
async delete(records: AttachmentModel[]): Promise<[number, AttachmentModel[]]> {
|
||||||
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[]]> {
|
|
||||||
const { DeleteObjectsCommand } = require('@aws-sdk/client-s3');
|
const { DeleteObjectsCommand } = require('@aws-sdk/client-s3');
|
||||||
const { s3 } = this.make(storage);
|
const { s3 } = this.make();
|
||||||
const { Deleted } = await s3.send(
|
const { Deleted } = await s3.send(
|
||||||
new DeleteObjectsCommand({
|
new DeleteObjectsCommand({
|
||||||
Bucket: storage.options.bucket,
|
Bucket: this.storage.options.bucket,
|
||||||
Delete: {
|
Delete: {
|
||||||
Objects: records.map((record) => ({ Key: getFileKey(record) })),
|
Objects: records.map((record) => ({ Key: getFileKey(record) })),
|
||||||
},
|
},
|
||||||
|
@ -14,18 +14,7 @@ import { STORAGE_TYPE_TX_COS } from '../../constants';
|
|||||||
import { getFilename, getFileKey } from '../utils';
|
import { getFilename, getFileKey } from '../utils';
|
||||||
|
|
||||||
export default class extends StorageType {
|
export default class extends StorageType {
|
||||||
filenameKey = 'url';
|
static defaults() {
|
||||||
make(storage) {
|
|
||||||
const createTxCosStorage = require('multer-cos');
|
|
||||||
return new createTxCosStorage({
|
|
||||||
cos: {
|
|
||||||
...storage.options,
|
|
||||||
dir: (storage.path ?? '').replace(/\/+$/, ''),
|
|
||||||
},
|
|
||||||
filename: getFilename,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
defaults() {
|
|
||||||
return {
|
return {
|
||||||
title: '腾讯云对象存储',
|
title: '腾讯云对象存储',
|
||||||
type: STORAGE_TYPE_TX_COS,
|
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, {
|
const { Deleted } = await promisify(cos.deleteMultipleObject).call(cos, {
|
||||||
Region: storage.options.Region,
|
Region: this.storage.options.Region,
|
||||||
Bucket: storage.options.Bucket,
|
Bucket: this.storage.options.Bucket,
|
||||||
Objects: records.map((record) => ({ Key: getFileKey(record) })),
|
Objects: records.map((record) => ({ Key: getFileKey(record) })),
|
||||||
});
|
});
|
||||||
return [Deleted.length, records.filter((record) => !Deleted.find((item) => item.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