feature: add file manager base architecture (#44)
* feature: add file manager base architecture * 修改 action 注册方式 * put upload action and middleware together * bugfix Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
6ffa3b53e8
commit
8bdbd804f0
@ -65,7 +65,6 @@ export class Column extends Field {
|
||||
if (DataTypes[type]) {
|
||||
return DataTypes[type];
|
||||
}
|
||||
|
||||
return DataTypes[(<typeof Column>this.constructor).name.toUpperCase()];
|
||||
}
|
||||
|
||||
@ -332,6 +331,9 @@ export class JSON extends Column {
|
||||
export class JSONB extends Column {
|
||||
}
|
||||
|
||||
export class UUID extends Column {
|
||||
}
|
||||
|
||||
export interface HasOneAccessors {
|
||||
get: string;
|
||||
set: string;
|
||||
|
@ -4,7 +4,13 @@
|
||||
"main": "lib/index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@koa/multer": "^3.0.0",
|
||||
"@nocobase/database": "^0.3.0-alpha.0",
|
||||
"@nocobase/resourcer": "^0.3.0-alpha.0"
|
||||
"@nocobase/resourcer": "^0.3.0-alpha.0",
|
||||
"multer": "^1.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nocobase/actions": "^0.3.0-alpha.0",
|
||||
"@nocobase/server": "^0.3.0-alpha.0"
|
||||
}
|
||||
}
|
||||
|
29
packages/plugin-file-manager/src/__tests__/action.test.ts
Normal file
29
packages/plugin-file-manager/src/__tests__/action.test.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import path from 'path';
|
||||
import { FILE_FIELD_NAME } from '../constants';
|
||||
import { getApp, getAgent } from '.';
|
||||
|
||||
describe('user fields', () => {
|
||||
let app;
|
||||
let agent;
|
||||
let db;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await getApp();
|
||||
agent = getAgent(app);
|
||||
db = app.database;
|
||||
await db.sync({ force: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
describe('', () => {
|
||||
it('', async () => {
|
||||
const response = await agent
|
||||
.post('/api/attachments:upload')
|
||||
.attach(FILE_FIELD_NAME, path.resolve(__dirname, './files/text.txt'));
|
||||
console.log(response.body);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1 @@
|
||||
Hello world!
|
138
packages/plugin-file-manager/src/__tests__/index.ts
Normal file
138
packages/plugin-file-manager/src/__tests__/index.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import path from 'path';
|
||||
import qs from 'qs';
|
||||
import supertest from 'supertest';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import { Dialect } from 'sequelize';
|
||||
import Database from '@nocobase/database';
|
||||
import { actions, middlewares } from '@nocobase/actions';
|
||||
import { Application, middleware } from '@nocobase/server';
|
||||
import plugin from '../server';
|
||||
|
||||
function getTestKey() {
|
||||
const { id } = require.main;
|
||||
const key = id
|
||||
.replace(`${process.env.PWD}/packages`, '')
|
||||
.replace(/src\/__tests__/g, '')
|
||||
.replace('.test.ts', '')
|
||||
.replace(/[^\w]/g, '_')
|
||||
.replace(/_+/g, '_');
|
||||
return key
|
||||
}
|
||||
|
||||
const config = {
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_DATABASE,
|
||||
host: process.env.DB_HOST,
|
||||
port: Number.parseInt(process.env.DB_PORT, 10),
|
||||
dialect: process.env.DB_DIALECT as Dialect,
|
||||
define: {
|
||||
hooks: {
|
||||
beforeCreate(model, options) {
|
||||
|
||||
},
|
||||
},
|
||||
},
|
||||
logging: process.env.DB_LOG_SQL === 'on',
|
||||
sync: {
|
||||
force: true,
|
||||
alter: {
|
||||
drop: true,
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
beforeDefine(columns, model) {
|
||||
model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export function getDatabase() {
|
||||
return new Database(config);
|
||||
};
|
||||
|
||||
export async function getApp() {
|
||||
const app = new Application({
|
||||
database: config,
|
||||
resourcer: {
|
||||
prefix: '/api',
|
||||
},
|
||||
});
|
||||
app.resourcer.use(middlewares.associated);
|
||||
app.resourcer.registerActionHandlers({...actions.associate, ...actions.common});
|
||||
app.registerPlugins({
|
||||
'collections': [path.resolve(__dirname, '../../../plugin-collections')],
|
||||
'file-manager': [plugin]
|
||||
});
|
||||
await app.loadPlugins();
|
||||
await app.database.sync({
|
||||
force: true,
|
||||
});
|
||||
app.use(async (ctx, next) => {
|
||||
ctx.db = app.database;
|
||||
await next();
|
||||
});
|
||||
app.use(bodyParser());
|
||||
app.use(middleware({
|
||||
prefix: '/api',
|
||||
resourcer: app.resourcer,
|
||||
database: app.database,
|
||||
}));
|
||||
return app;
|
||||
}
|
||||
|
||||
interface ActionParams {
|
||||
resourceKey?: string | number;
|
||||
// resourceName?: string;
|
||||
// associatedName?: string;
|
||||
associatedKey?: string | number;
|
||||
fields?: any;
|
||||
filter?: any;
|
||||
values?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface Handler {
|
||||
get: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
list: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
create: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
update: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
destroy: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
[name: string]: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
resource: (name: string) => Handler;
|
||||
}
|
||||
|
||||
export function getAgent(app: Application) {
|
||||
return supertest.agent(app.callback());
|
||||
}
|
||||
|
||||
export function getAPI(app: Application) {
|
||||
const agent = getAgent(app);
|
||||
return {
|
||||
resource(name: string): any {
|
||||
return new Proxy({}, {
|
||||
get(target, method, receiver) {
|
||||
return (params: ActionParams = {}) => {
|
||||
const { associatedKey, resourceKey, values = {}, ...restParams } = params;
|
||||
let url = `/api/${name}`;
|
||||
if (associatedKey) {
|
||||
url = `/api/${name.split('.').join(`/${associatedKey}/`)}`;
|
||||
}
|
||||
url += `:${method as string}`;
|
||||
if (resourceKey) {
|
||||
url += `/${resourceKey}`;
|
||||
}
|
||||
if (['list', 'get'].indexOf(method as string) !== -1) {
|
||||
return agent.get(`${url}?${qs.stringify(restParams)}`);
|
||||
} else {
|
||||
return agent.post(`${url}?${qs.stringify(restParams)}`).send(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
23
packages/plugin-file-manager/src/actions/upload.ts
Normal file
23
packages/plugin-file-manager/src/actions/upload.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import multer from '@koa/multer';
|
||||
import actions from '@nocobase/actions';
|
||||
|
||||
import { FILE_FIELD_NAME } from '../constants';
|
||||
|
||||
export async function middleware(ctx: actions.Context, next: actions.Next) {
|
||||
const { actionName } = ctx.action.params;
|
||||
|
||||
if (actionName !== 'upload') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const options = {};
|
||||
const upload = multer(options);
|
||||
return upload.single(FILE_FIELD_NAME)(ctx, next);
|
||||
};
|
||||
|
||||
export async function action(ctx: actions.Context, next: actions.Next) {
|
||||
const { [FILE_FIELD_NAME]: file } = ctx;
|
||||
console.log(file);
|
||||
ctx.body = file;
|
||||
await next();
|
||||
};
|
@ -6,9 +6,55 @@ export default {
|
||||
internal: true,
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
comment: '唯一 ID,系统文件名',
|
||||
type: 'uuid',
|
||||
name: 'id',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
comment: '用户文件名',
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
comment: '扩展名(含“.”)',
|
||||
type: 'string',
|
||||
name: 'extname',
|
||||
},
|
||||
{
|
||||
comment: '文件体积(字节)',
|
||||
type: 'integer',
|
||||
name: 'size',
|
||||
},
|
||||
{
|
||||
comment: '文件类型(mimetype 前半段,通常用于预览)',
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'mimetype',
|
||||
},
|
||||
{
|
||||
comment: '存储引擎',
|
||||
type: 'belongsTo',
|
||||
name: 'storage',
|
||||
},
|
||||
{
|
||||
comment: '相对路径',
|
||||
type: 'string',
|
||||
name: 'path',
|
||||
},
|
||||
{
|
||||
comment: '其他文件信息(如图片的宽高)',
|
||||
type: 'jsonb',
|
||||
name: 'meta',
|
||||
},
|
||||
{
|
||||
comment: '网络访问地址',
|
||||
type: 'url',
|
||||
name: 'url'
|
||||
}
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
|
38
packages/plugin-file-manager/src/collections/storages.ts
Normal file
38
packages/plugin-file-manager/src/collections/storages.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { TableOptions } from '@nocobase/database';
|
||||
|
||||
export default {
|
||||
name: 'storages',
|
||||
title: '存储引擎',
|
||||
internal: true,
|
||||
fields: [
|
||||
{
|
||||
comment: '标识名称,用于用户记忆',
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
comment: '类型标识,如 local/ali-oss 等',
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
defaultValue: 'local'
|
||||
},
|
||||
{
|
||||
comment: '配置项',
|
||||
type: 'jsonb',
|
||||
name: 'options',
|
||||
defaultValue: {}
|
||||
},
|
||||
{
|
||||
comment: '存储相对路径模板',
|
||||
type: 'string',
|
||||
name: 'path',
|
||||
defaultValue: ''
|
||||
},
|
||||
{
|
||||
comment: '访问地址前缀',
|
||||
type: 'string',
|
||||
name: 'baseUrl',
|
||||
defaultValue: ''
|
||||
},
|
||||
]
|
||||
} as TableOptions;
|
1
packages/plugin-file-manager/src/constants.ts
Normal file
1
packages/plugin-file-manager/src/constants.ts
Normal file
@ -0,0 +1 @@
|
||||
export const FILE_FIELD_NAME = 'file';
|
20
packages/plugin-file-manager/src/fields/URL.ts
Normal file
20
packages/plugin-file-manager/src/fields/URL.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { VIRTUAL, VirtualOptions, FieldContext } from '@nocobase/database';
|
||||
|
||||
export interface URLOptions extends Omit<VirtualOptions, 'type'> {
|
||||
type: 'url';
|
||||
}
|
||||
|
||||
export default class URL extends VIRTUAL {
|
||||
|
||||
constructor({ type, ...options }, context: FieldContext) {
|
||||
super({
|
||||
...options,
|
||||
type: 'virtual',
|
||||
get() {
|
||||
const storage = this.getDataValue('storage') || {};
|
||||
return `${storage.baseUrl}${this.getDataValue('path')}/${this.getDataValue('id')}${this.getDataValue('extname')}`;
|
||||
}
|
||||
} as VirtualOptions, context);
|
||||
}
|
||||
}
|
1
packages/plugin-file-manager/src/fields/index.ts
Normal file
1
packages/plugin-file-manager/src/fields/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as URL } from './URL';
|
@ -1,5 +0,0 @@
|
||||
import { ResourceOptions } from '@nocobase/resourcer';
|
||||
|
||||
export default {
|
||||
name: 'attachments',
|
||||
} as ResourceOptions;
|
@ -1,12 +1,29 @@
|
||||
import path from 'path';
|
||||
import Database from '@nocobase/database';
|
||||
import Database, { registerFields } from '@nocobase/database';
|
||||
import Resourcer from '@nocobase/resourcer';
|
||||
|
||||
export default async function (options = {}) {
|
||||
import * as fields from './fields';
|
||||
import { IStorage } from './storages';
|
||||
import {
|
||||
action as uploadAction,
|
||||
middleware as uploadMiddleware,
|
||||
} from './actions/upload';
|
||||
|
||||
export interface FileManagerOptions {
|
||||
storages: IStorage[]
|
||||
}
|
||||
|
||||
export default async function (options: FileManagerOptions) {
|
||||
const database: Database = this.database;
|
||||
const resourcer: Resourcer = this.resourcer;
|
||||
|
||||
registerFields(fields);
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
// 暂时中间件只能通过 use 加进来
|
||||
resourcer.use(uploadMiddleware);
|
||||
resourcer.registerActionHandler('upload', uploadAction);
|
||||
}
|
||||
|
5
packages/plugin-file-manager/src/storages/IStorage.ts
Normal file
5
packages/plugin-file-manager/src/storages/IStorage.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default interface IStorage {
|
||||
options: any;
|
||||
|
||||
put: (file, data) => Promise<any>
|
||||
}
|
13
packages/plugin-file-manager/src/storages/Local.ts
Normal file
13
packages/plugin-file-manager/src/storages/Local.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import IStorage from './IStorage';
|
||||
|
||||
export default class Local implements IStorage {
|
||||
options: any;
|
||||
|
||||
constructor(options: any) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
async put(file, data) {
|
||||
|
||||
}
|
||||
}
|
2
packages/plugin-file-manager/src/storages/index.ts
Normal file
2
packages/plugin-file-manager/src/storages/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as IStorage } from './IStorage';
|
||||
export { default as Local } from './Local';
|
Loading…
Reference in New Issue
Block a user