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:
Junyi 2020-12-19 08:45:19 +08:00 committed by GitHub
parent 6ffa3b53e8
commit 8bdbd804f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 348 additions and 11 deletions

View File

@ -65,7 +65,6 @@ export class Column extends Field {
if (DataTypes[type]) { if (DataTypes[type]) {
return DataTypes[type]; return DataTypes[type];
} }
return DataTypes[(<typeof Column>this.constructor).name.toUpperCase()]; return DataTypes[(<typeof Column>this.constructor).name.toUpperCase()];
} }
@ -332,6 +331,9 @@ export class JSON extends Column {
export class JSONB extends Column { export class JSONB extends Column {
} }
export class UUID extends Column {
}
export interface HasOneAccessors { export interface HasOneAccessors {
get: string; get: string;
set: string; set: string;

View File

@ -4,7 +4,13 @@
"main": "lib/index.js", "main": "lib/index.js",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@koa/multer": "^3.0.0",
"@nocobase/database": "^0.3.0-alpha.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"
} }
} }

View 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);
});
});
});

View File

@ -0,0 +1 @@
Hello world!

View 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);
}
}
}
});
}
};
}

View 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();
};

View File

@ -6,9 +6,55 @@ export default {
internal: true, internal: true,
fields: [ fields: [
{ {
type: 'string', comment: '唯一 ID系统文件名',
name: 'title', 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: [ actions: [
{ {

View 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;

View File

@ -0,0 +1 @@
export const FILE_FIELD_NAME = 'file';

View 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);
}
}

View File

@ -0,0 +1 @@
export { default as URL } from './URL';

View File

@ -1,5 +0,0 @@
import { ResourceOptions } from '@nocobase/resourcer';
export default {
name: 'attachments',
} as ResourceOptions;

View File

@ -1,12 +1,29 @@
import path from 'path'; import path from 'path';
import Database from '@nocobase/database'; import Database, { registerFields } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer'; 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 database: Database = this.database;
const resourcer: Resourcer = this.resourcer; const resourcer: Resourcer = this.resourcer;
registerFields(fields);
database.import({ database.import({
directory: path.resolve(__dirname, 'collections'), directory: path.resolve(__dirname, 'collections'),
}); });
// 暂时中间件只能通过 use 加进来
resourcer.use(uploadMiddleware);
resourcer.registerActionHandler('upload', uploadAction);
} }

View File

@ -0,0 +1,5 @@
export default interface IStorage {
options: any;
put: (file, data) => Promise<any>
}

View 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) {
}
}

View File

@ -0,0 +1,2 @@
export { default as IStorage } from './IStorage';
export { default as Local } from './Local';