Fix: plugin-file-manager (#111)

* fix: test cases

* fix env used by file manager

* default value of process.env.LOCAL_STORAGE_BASE_URL

* use workspace dependencies

* refactor some env to adapt more scenario

* fix reviewing issues

* fix env default value

* update umi config

* bugfix

* fix shared cache

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
Junyi 2021-12-03 07:31:22 +08:00 committed by GitHub
parent 12f58effad
commit 60b966f59e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 312 additions and 284 deletions

View File

@ -4,8 +4,8 @@ ADMINER_PORT=8080
DB_MYSQL_PORT=3306
DB_POSTGRES_PORT=5432
VERDACCIO_PORT=4873
APP_PORT=13001
API_PORT=13002
########## NOCOBASE ENV ##########
@ -27,7 +27,19 @@ DB_LOG_SQL=
# API & APP
NOCOBASE_ENV=
APP_USE_STATIC_SERVER=true
API_PORT=13002
# api base path endpoint for app(web)
API_BASE_PATH=/api/
# api server access point for app(web when build)
API_BASE_URL=
# api server access point will be proxied from app(umi dev)
PROXY_TARGET_URL=
# app(web) assets
APP_DIST=packages/app/dist
# ADMIN USER (Initialization only)
@ -42,7 +54,8 @@ STORAGE_TYPE=local
# LOCAL STORAGE
LOCAL_STORAGE_USE_STATIC_SERVER=true
LOCAL_STORAGE_BASE_URL=http://localhost:13002
LOCAL_STORAGE_BASE_URL=/uploads # same origin
# LOCAL_STORAGE_BASE_URL=http://your.cdn.domain/pathname # cdn or other origin driven
# ALI OSS STORAGE
ALI_OSS_STORAGE_BASE_URL=

View File

@ -24,10 +24,13 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: yarn install
- run: yarn bootstrap
- run: echo "API_URL=${{ secrets.API_URL }}" >> .env
- run: yarn build-docs
- name: Build
env:
API_BASE_URL: ${{ secrets.API_ORIGIN }}${{ secrets.API_BASE_PATH }}
run: |
yarn install
yarn bootstrap
yarn build-docs
- name: Deploy
uses: amondnet/vercel-action@v19

View File

@ -1,23 +1,20 @@
import { defineConfig } from 'dumi';
const baseUrl = `http://localhost:${process.env.API_PORT || '13001'}/`;
console.log('baseUrl', baseUrl);
import { getUmiConfig } from '@nocobase/utils';
process.env.MFSU_AD = 'none';
const umiConfig = getUmiConfig();
export default defineConfig({
title: 'NocoBase',
hash: true,
define: {
'process.env.API_URL': process.env.API_URL || `${baseUrl}api/`,
'process.env.API_HOSTNAME': process.env.API_HOSTNAME,
...umiConfig.define,
},
// only proxy when using `umi dev`
// if the assets are built, will not proxy
proxy: {
'/api/': {
target: baseUrl,
changeOrigin: true,
pathRewrite: { '^/api/': '/api/' },
},
...umiConfig.proxy,
},
resolve: {
includes: ['docs', 'packages/client/src/schemas'],

View File

@ -15,7 +15,7 @@ import { UseRequestProvider } from 'ahooks';
import { extend } from 'umi-request';
const request = extend({
prefix: process.env.API_URL,
prefix: process.env.API_BASE_URL,
timeout: 30000,
});
@ -33,10 +33,6 @@ const client = new ClientSDK({
request,
});
console.log('process.env.API_URL', process.env.API_URL);
// console.log = () => {}
const RouteSwitch = createRouteSwitch({
components: {
AdminLayout,

View File

@ -1,25 +1,27 @@
import path from 'path';
import { defineConfig } from 'umi';
import dotenv from 'dotenv';
import path from 'path';
import { getUmiConfig } from '@nocobase/utils';
dotenv.config({
path: path.resolve(__dirname, '../../.env'),
});
process.env.MFSU_AD = 'none';
const umiConfig = getUmiConfig();
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
define: {
'process.env.API_URL': process.env.API_URL,
'process.env.API_PORT': process.env.API_PORT,
...umiConfig.define,
},
// only proxy when using `umi dev`
// if the assets are built, will not proxy
proxy: {
'/api': {
'target': `http://localhost:${process.env.API_PORT}/`,
'changeOrigin': true,
'pathRewrite': { '^/api': '/api' },
},
...umiConfig.proxy,
},
routes: [
{ path: '/', exact: false, component: '@/pages/index' },

View File

@ -42,7 +42,7 @@ const api = new Server({
},
},
resourcer: {
prefix: '/api',
prefix: process.env.API_BASE_PATH || '/api/',
},
});
@ -70,7 +70,7 @@ for (const plugin of plugins) {
api.plugin(
require(`@nocobase/plugin-client/${libDir}/server`).default, {
dist: path.resolve(process.cwd(), './dist'),
dist: process.env.APP_DIST || path.resolve(process.cwd(), './dist'),
lang: process.env.APP_LANG,
// importDemo: true,
});

View File

@ -7,7 +7,7 @@ import { ClientSDK } from '../ClientSDK';
import { extend } from 'umi-request';
const request = extend({
prefix: process.env.API_URL,
prefix: process.env.API_BASE_URL,
timeout: 30000,
});

View File

@ -17,7 +17,7 @@ import { I18nextProvider } from 'react-i18next';
import i18n from '../i18n';
const request = extend({
prefix: process.env.API_URL,
prefix: process.env.API_BASE_URL,
timeout: 30000,
});
@ -26,7 +26,7 @@ request.use(async (ctx, next) => {
const token = localStorage.getItem('NOCOBASE_TOKEN');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
headers['X-Hostname'] = process.env.API_HOSTNAME;
headers['X-Hostname'] = window.location.hostname;
}
await next();
});
@ -35,8 +35,6 @@ const client = new ClientSDK({
request,
});
console.log('process.env.API_URL', process.env.API_URL);
// console.log = () => {}
const RouteSwitch = createRouteSwitch({

View File

@ -27,7 +27,7 @@ export function useDefaultAction() {
}
export const request = extend({
prefix: process.env.API_URL,
prefix: process.env.API_BASE_URL,
timeout: 30000,
});
@ -40,8 +40,6 @@ request.use(async (ctx, next) => {
await next();
});
console.log('process.env.API_URL', process.env.API_URL);
export async function createOrUpdateCollection(data: any) {
return await request('collections:createOrUpdate', {
method: 'post',

View File

@ -407,7 +407,7 @@ Upload.Attachment = connect(
multiple={multiple}
listType={'picture-card'}
fileList={fileList}
action={`${process.env.API_URL}attachments:upload`}
action={`${process.env.API_BASE_URL}attachments:upload`}
onChange={(info) => {
setSync(false);
if (multiple) {

View File

@ -22,6 +22,18 @@ DB_STORAGE=db.sqlite
# set to 'on' to enable log
DB_LOG_SQL=
# API & APP
NOCOBASE_ENV=
# api server access point will be proxied from app(web when `umi dev`)
API_ORIGIN_DEV=http://localhost:13002
# api server access point for app(web when build)
API_ORIGIN=
# api base path endpoint for app(web)
API_BASE_PATH=/api/
# ADMIN USER (Initialization only)
ADMIN_EMAIL=admin@nocobase.com

View File

@ -1,11 +1,14 @@
import { defineConfig } from 'umi';
import dotenv from 'dotenv';
import path from 'path';
import { getUmiConfig } from '@nocobase/utils';
dotenv.config({
path: path.resolve(__dirname, './.env'),
});
const umiConfig = getUmiConfig();
process.env.MFSU_AD = 'none';
export default defineConfig({
@ -14,15 +17,12 @@ export default defineConfig({
type: 'none',
},
define: {
'process.env.API_URL': process.env.API_URL || `/api/`,
'process.env.API_PORT': process.env.API_PORT || '13001',
...umiConfig.define,
},
// only proxy when using `umi dev`
// if the assets are built, will not proxy
proxy: {
'/api': {
target: `http://localhost:${process.env.API_PORT || '13001'}/`,
changeOrigin: true,
pathRewrite: { '^/api': '/api' },
},
...umiConfig.proxy,
},
routes: [{ path: '/', exact: false, component: '@/pages/index' }],
fastRefresh: {},

View File

@ -7,7 +7,7 @@ const start = Date.now();
const api = new Application({
database: dbConfig[process.env.DB_DIALECT || 'sqlite'],
resourcer: {
prefix: '/api',
prefix: process.env.API_BASE_PATH || '/api/',
},
});

View File

@ -18,6 +18,7 @@ export default {
this.app.on('beforeStart', async () => {
await database.getModel('collections').load({
// TODO(bug): quick fix just comment out
skipExisting: true,
});
});

View File

@ -7,13 +7,11 @@
"@koa/multer": "^3.0.0",
"@nocobase/server": "^0.5.0-alpha.34",
"ali-oss": "^6.12.0",
"koa-mount": "^4.0.0",
"koa-static": "^5.0.0",
"mime-match": "^1.0.2",
"multer": "^1.4.2"
},
"devDependencies": {
"@nocobase/test": "^0.5.0-alpha.34",
"@types/multer": "^1.4.5"
},
"gitHead": "e7df1f93c4e23b9a666d99ee7372c02bdaec97c4"

View File

@ -1,39 +1,52 @@
import { promisify } from 'util';
import { promises as fs } from 'fs';
import path from 'path';
import qs from 'qs';
import { FILE_FIELD_NAME, STORAGE_TYPE_LOCAL } from '../constants';
import { getApp, getAgent, getAPI } from '.';
const DEFAULT_LOCAL_BASE_URL = process.env.LOCAL_STORAGE_BASE_URL || `http://localhost:${process.env.API_PORT}/uploads`;
import { generatePrefixByPath } from '@nocobase/test';
import { FILE_FIELD_NAME, STORAGE_TYPE_LOCAL } from '../constants';
import { getApp, requestFile } from '.';
const {
API_PORT,
LOCAL_STORAGE_BASE_URL
} = process.env;
const DEFAULT_LOCAL_BASE_URL = LOCAL_STORAGE_BASE_URL || `http://localhost:${API_PORT}/uploads`;
describe('action', () => {
let app;
let agent;
let api;
let db;
let http;
beforeEach(async () => {
app = await getApp();
agent = getAgent(app);
api = getAPI(app);
agent = app.agent();
http = app.listen(API_PORT);
db = app.db;
const Storage = app.db.getModel('storages');
const Storage = db.getModel('storages');
await Storage.create({
name: `local_${Date.now().toString(36)}`,
name: `local1_${generatePrefixByPath()}`,
type: STORAGE_TYPE_LOCAL,
baseUrl: DEFAULT_LOCAL_BASE_URL,
default: true
});
});
afterEach(() => db.close());
afterEach(async () => {
await promisify(cb => http.close(cb))();
await db.close();
});
describe('direct attachment', () => {
it('upload file should be ok', async () => {
const response = await agent
.post('/api/attachments:upload')
.attach(FILE_FIELD_NAME, path.resolve(__dirname, './files/text.txt'));
const { body } = await agent
.resource('attachments')
.upload({
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt')
});
const matcher = {
title: 'text',
@ -44,14 +57,15 @@ describe('action', () => {
meta: {},
storage_id: 1,
};
// 文件上传和解析是否正常
expect(response.body).toMatchObject(matcher);
expect(body.data).toMatchObject(matcher);
// 文件的 url 是否正常生成
expect(response.body.url).toBe(`${DEFAULT_LOCAL_BASE_URL}${response.body.path}/${response.body.filename}`);
expect(body.data.url).toBe(`${DEFAULT_LOCAL_BASE_URL}${body.data.path}/${body.data.filename}`);
const Attachment = db.getModel('attachments');
const attachment = await Attachment.findOne({
where: { id: response.body.id },
where: { id: body.data.id },
include: ['storage']
});
const storage = attachment.get('storage');
@ -73,10 +87,9 @@ describe('action', () => {
// 文件是否保存到指定路径
expect(file.toString()).toBe('Hello world!\n');
const content = await agent.get(`/uploads${attachment.path}/${attachment.filename}`);
// 通过 url 是否能正确访问
// TODO(bug)
// expect(content.text).toBe('Hello world!\n');
const content = await requestFile(attachment.url, agent);
expect(content.text).toBe('Hello world!\n');
});
});
@ -84,9 +97,9 @@ describe('action', () => {
it('upload with associatedKey, fail as 400 because file mimetype does not match', async () => {
const User = db.getModel('users');
const user = await User.create();
const response = await api.resource('users.avatar').upload({
const response = await agent.resource('users.avatar').upload({
associatedKey: user.id,
filePath: './files/text.txt'
file: path.resolve(__dirname, './files/text.txt')
});
expect(response.status).toBe(400);
});
@ -94,9 +107,9 @@ describe('action', () => {
it('upload with associatedKey', async () => {
const User = db.getModel('users');
const user = await User.create();
const response = await api.resource('users.avatar').upload({
const { body } = await agent.resource('users.avatar').upload({
associatedKey: user.id,
filePath: './files/image.png',
file: path.resolve(__dirname, './files/image.png'),
values: { width: 100, height: 100 }
});
const matcher = {
@ -112,7 +125,7 @@ describe('action', () => {
storage_id: 1,
};
// 上传正常返回
expect(response.body).toMatchObject(matcher);
expect(body.data).toMatchObject(matcher);
// 由于初始没有外键,无法获取
// await user.getAvatar()
@ -120,13 +133,44 @@ describe('action', () => {
include: ['avatar']
});
// 外键更新正常
expect(updatedUser.get('avatar').id).toBe(response.body.id);
expect(updatedUser.get('avatar').id).toBe(body.data.id);
});
it('upload to assoiciated field and storage with full base url should be ok', async () => {
const BASE_URL = `http://localhost:${process.env.API_PORT}/another-uploads`;
const storageName = 'local_private';
const urlPath = 'test/path';
const Storage = db.getModel('storages');
// 动态添加 storage
await Storage.create({
name: storageName,
type: STORAGE_TYPE_LOCAL,
path: urlPath,
baseUrl: BASE_URL,
options: {
documentRoot: 'uploads/another'
}
});
const User = db.getModel('users');
const user = await User.create();
const { body } = await agent.resource('users.pubkeys').upload({
associatedKey: user.id,
file: path.resolve(__dirname, './files/text.txt'),
values: {}
});
// 文件的 url 是否正常生成
expect(body.data.url).toBe(`${BASE_URL}/${urlPath}/${body.data.filename}`);
console.log(body.data.url);
const content = await requestFile(body.data.url, agent);
expect(content.text).toBe('Hello world!\n');
});
// TODO(bug): 没有 associatedKey 时路径解析资源名称不对,无法进入 action
it.skip('upload without associatedKey', async () => {
const response = await api.resource('users.avatar').upload({
filePath: './files/image.png',
const { body } = await agent.resource('users.avatar').upload({
file: path.resolve(__dirname, './files/image.png'),
values: { width: 100, height: 100 }
});
const matcher = {
@ -136,13 +180,13 @@ describe('action', () => {
size: 255,
mimetype: 'image/png',
// TODO(optimize): 可以考虑使用 qs 的 decoder 来进行类型解析
// see: https://github.com/ljharb/qs/issues/91
// @see: https://github.com/ljharb/qs/issues/91
// 或考虑使用 query-string 库的 parseNumbers 等配置项
meta: { width: '100', height: '100' },
storage_id: 1,
};
// 上传返回正常
expect(response.body).toMatchObject(matcher);
expect(body.data).toMatchObject(matcher);
});
});
});

View File

@ -1,72 +1,19 @@
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/src';
import { Application } from '@nocobase/server/src';
import middleware from '@nocobase/server/src/middleware'
import { MockServer, mockServer } from '@nocobase/test';
import plugin from '../server';
import { FILE_FIELD_NAME } from '../constants';
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 async function getApp(options = {}): Promise<MockServer> {
const app = mockServer({
...options,
cors: {
origin: '*'
}
},
};
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.registerPlugin({
'collections': [path.resolve(__dirname, '../../../plugin-collections')],
'file-manager': [plugin]
});
await app.loadPlugins();
app.plugin(require('@nocobase/plugin-collections/src/server').default);
app.plugin(plugin);
await app.load();
app.db.import({
directory: path.resolve(__dirname, './tables')
});
@ -75,80 +22,13 @@ export async function getApp() {
} catch (error) {
console.error(error);
}
app.use(async (ctx, next) => {
ctx.db = app.db;
await next();
});
app.use(bodyParser());
app.use(middleware({
prefix: '/api',
resourcer: app.resourcer,
database: app.db,
}));
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: string, receiver) {
return (params: ActionParams = {}) => {
const { associatedKey, resourceKey, values = {}, filePath, ...restParams } = params;
let url = `/api/${name}`;
if (associatedKey) {
url = `/api/${name.split('.').join(`/${associatedKey}/`)}`;
}
url += `:${method as string}`;
if (resourceKey) {
url += `/${resourceKey}`;
}
switch (method) {
case 'upload':
return agent.post(`${url}?${qs.stringify(restParams)}`)
.attach(FILE_FIELD_NAME, path.resolve(__dirname, filePath))
.field(values);
case 'list':
case 'get':
return agent.get(`${url}?${qs.stringify(restParams)}`);
default:
return agent.post(`${url}?${qs.stringify(restParams)}`).send(values);
}
}
}
});
}
};
// because the app in supertest is using a random port
export function requestFile(url, agent) {
return path.isAbsolute(url)
? agent.get(url)
: supertest.agent(url).get('');
}

View File

@ -19,6 +19,17 @@ export default {
}
}
},
{
type: 'belongsToMany',
name: 'pubkeys',
target: 'attachments',
attachment: {
storage: 'local_private',
rules: {
mimetype: ['text/*']
}
}
},
{
type: 'belongsToMany',
name: 'photos',

View File

@ -46,10 +46,10 @@ export async function middleware(ctx: Context, next: Next) {
// 如果没有包含关联,则直接按默认文件上传至默认存储引擎
storage = await StorageModel.findOne({ where: { default: true } });
} else {
const fieldOptions = resourceField.getOptions();
const { attachment = {} } = resourceField.getOptions();
storage = await StorageModel.findOne({
where: fieldOptions.defaultValue
? { [StorageModel.primaryKeyAttribute]: fieldOptions.defaultValue }
where: attachment.storage
? { name: attachment.storage }
: { default: true }
});
}
@ -75,8 +75,8 @@ export async function middleware(ctx: Context, next: Next) {
},
storage: makeStorage(storage),
};
const upload = multer(multerOptions);
return upload.single(FILE_FIELD_NAME)(ctx, next);
const uploader = multer(multerOptions);
return uploader.single(FILE_FIELD_NAME)(ctx, next);
};
export async function action(ctx: Context, next: Next) {
@ -86,6 +86,11 @@ export async function action(ctx: Context, next: Next) {
}
const { associatedName, associatedKey, resourceField } = ctx.action.params;
const extname = path.extname(file.filename);
const urlPath = storage.path
? (storage.path.startsWith('/')
? storage.path
: `/${storage.path}`)
: '';
const data = {
title: file.originalname.replace(extname, ''),
filename: file.filename,
@ -94,7 +99,7 @@ export async function action(ctx: Context, next: Next) {
path: storage.path,
size: file.size,
// 直接缓存起来
url: `${storage.baseUrl}${storage.path}/${file.filename}`,
url: `${storage.baseUrl}${urlPath}/${file.filename}`,
mimetype: file.mimetype,
// @ts-ignore
meta: ctx.request.body

View File

@ -8,8 +8,9 @@ import {
middleware as uploadMiddleware,
} from './actions/upload';
import {
middleware as localMiddleware,
middleware as localMiddleware
} from './storages/local';
import { STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_LOCAL } from './constants';
export default {
name: 'file-manager',
@ -24,21 +25,25 @@ export default {
// 暂时中间件只能通过 use 加进来
resourcer.use(uploadMiddleware);
resourcer.registerActionHandler('upload', uploadAction);
localMiddleware(this.app);
if (process.env.NOCOBASE_ENV !== 'production'
&& process.env.LOCAL_STORAGE_USE_STATIC_SERVER) {
await localMiddleware(this.app);
}
const Storage = database.getModel('storages');
this.app.on('db.init', async () => {
await Storage.create({
title: '本地存储',
name: `local`,
type: 'local',
baseUrl: process.env.LOCAL_STORAGE_BASE_URL,
default: process.env.STORAGE_TYPE === 'local',
type: STORAGE_TYPE_LOCAL,
baseUrl: process.env.LOCAL_STORAGE_BASE_URL || `http://localhost:${process.env.API_PORT}/uploads`,
default: process.env.STORAGE_TYPE === STORAGE_TYPE_LOCAL,
});
await Storage.create({
name: `ali-oss`,
type: 'ali-oss',
type: STORAGE_TYPE_ALI_OSS,
baseUrl: process.env.ALI_OSS_STORAGE_BASE_URL,
options: {
region: process.env.ALI_OSS_REGION,

View File

@ -3,43 +3,71 @@ import { URL } from 'url';
import mkdirp from 'mkdirp';
import multer from 'multer';
import serve from 'koa-static';
import mount from 'koa-mount';
import { STORAGE_TYPE_LOCAL } from '../constants';
import { getFilename } from '../utils';
// use koa-mount match logic
function match(basePath: string, pathname: string): boolean {
if (!pathname.startsWith(basePath)) {
return false;
}
const newPath = pathname.replace(basePath, '') || '/';
if (basePath.slice(-1) === '/') {
return true;
}
return newPath[0] === '/';
}
async function update(app, storages) {
const StorageModel = app.db.getModel('storages');
const items = await StorageModel.findAll({
where: {
type: STORAGE_TYPE_LOCAL,
}
});
const primaryKey = StorageModel.primaryKeyAttribute;
storages.clear();
for (const storage of items) {
storages.set(storage[primaryKey], storage);
}
}
function createLocalServerUpdateHook(app, storages) {
return async function (row) {
if (row.get('type') === STORAGE_TYPE_LOCAL) {
await update(app, storages);
}
}
}
export function getDocumentRoot(storage): string {
const { documentRoot = 'uploads' } = storage.options || {};
// TODO(feature): 后面考虑以字符串模板的方式使用,可注入 req/action 相关变量,以便于区分文件夹
return path.resolve(path.isAbsolute(documentRoot)
? documentRoot
: path.join(process.cwd(), documentRoot), storage.path);
: path.join(process.cwd(), documentRoot));
}
// TODO(optimize): 初始化的时机不应该放在中间件里
export function middleware(app) {
if (process.env.NOCOBASE_ENV === 'production') {
return;
}
export async function middleware(app, options?) {
const LOCALHOST = `http://localhost:${process.env.API_PORT}`;
const storages = new Map<string, any>();
const StorageModel = app.db.getModel('storages');
const storages = new Map<string, any>();
return app.use(async function (ctx, next) {
const items = await StorageModel.findAll({
where: {
type: STORAGE_TYPE_LOCAL,
}
});
const localServerUpdateHook = createLocalServerUpdateHook(app, storages);
StorageModel.addHook('afterCreate', localServerUpdateHook);
StorageModel.addHook('afterUpdate', localServerUpdateHook);
StorageModel.addHook('afterDestroy', localServerUpdateHook);
const primaryKey = StorageModel.primaryKeyAttribute;
for (const storage of items) {
// TODO未解决 storage 更新问题
if (storages.has(storage[primaryKey])) {
continue;
}
await update(app, storages);
app.use(async function (ctx, next) {
for (const storage of storages.values()) {
const baseUrl = storage.get('baseUrl');
let url;
@ -47,33 +75,40 @@ export function middleware(app) {
url = new URL(baseUrl);
} catch (e) {
url = {
protocol: 'http:',
hostname: 'localhost',
port: process.env.API_PORT,
pathname: baseUrl
};
}
// 以下情况才认为当前进程所应该提供静态服务
// 否则都忽略,交给其他 server 来提供(如 nginx/cdn 等)
// TODO(bug): https、端口 80 默认值和其他本地 ip/hostname 的情况未考虑
// TODO 实际应该用 NOCOBASE_ENV 来判断,或者抛给 env 处理
if (process.env.LOCAL_STORAGE_USE_STATIC_SERVER) {
const basePath = url.pathname.startsWith('/') ? url.pathname : `/${url.pathname}`;
app.use(mount(basePath, serve(getDocumentRoot(storage))));
if (url.origin && url.origin !== LOCALHOST) {
continue;
}
storages.set(storage.primaryKey, storage);
const basePath = url.pathname.startsWith('/') ? url.pathname : `/${url.pathname}`;
if (!match(basePath, ctx.path)) {
continue;
}
return serve(getDocumentRoot(storage), {
// for handle files after any api handlers
defer: true
})(ctx, async () => {
if (ctx.path.startsWith(basePath)) {
ctx.path = ctx.path.replace(basePath, '');
}
await next();
});
}
await next();
});
}
export default (storage) => multer.diskStorage({
destination: function (req, file, cb) {
const destPath = getDocumentRoot(storage);
mkdirp(destPath).then(() => {
cb(null, destPath);
}).catch(cb);
const destPath = path.join(getDocumentRoot(storage), storage.path);
mkdirp(destPath, (err: Error | null) => cb(err, destPath));
},
filename: getFilename
});

View File

@ -144,7 +144,7 @@ export function parseRequest(request: ParseRequest, options: ParseOptions = {}):
const params: ParsedParams = {};
let prefix = (options.prefix || '').trim();
let prefix = (options.prefix || '').trim().replace(/\/$/, '');
if (prefix && !prefix.startsWith('/')) {
prefix = `/${prefix}`;

View File

@ -49,7 +49,7 @@ export class MockServer extends Application {
agent(): SuperAgentTest & { resource: (name: string) => Resource } {
const agent = supertest.agent(this.callback());
const prefix = this.resourcer.options.prefix;
const proxy = new Proxy({}, {
const proxy = new Proxy(agent, {
get(target, method: string, receiver) {
if (method === 'resource') {
return (name: string) => {
@ -74,7 +74,7 @@ export class MockServer extends Application {
if (resourceKey) {
url += `/${resourceKey}`;
}
console.log('request url: ' + url);
switch (method) {
case 'upload':
return agent

View File

@ -1,9 +1,2 @@
import deepmerge from 'deepmerge';
const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray
export function merge(obj1: any, obj2: any) {
return deepmerge(obj1, obj2, {
arrayMerge: overwriteMerge,
});
}
export * from './merge';
export * from './umiConfig';

View File

@ -0,0 +1,9 @@
import deepmerge from 'deepmerge';
const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray
export function merge(obj1: any, obj2: any) {
return deepmerge(obj1, obj2, {
arrayMerge: overwriteMerge,
});
}

View File

@ -0,0 +1,36 @@
export function getUmiConfig() {
const { API_PORT, API_BASE_URL } = process.env;
const API_BASE_PATH = process.env.API_BASE_PATH || '/api/';
const PROXY_TARGET_URL = process.env.PROXY_TARGET_URL || `http://127.0.0.1:${API_PORT}`;
const LOCAL_STORAGE_BASE_URL = process.env.LOCAL_STORAGE_BASE_URL || '/uploads';
function getLocalStorageProxy() {
if (LOCAL_STORAGE_BASE_URL.startsWith('http')) {
return {};
}
return {
[LOCAL_STORAGE_BASE_URL]: {
target: PROXY_TARGET_URL,
changeOrigin: true,
},
};
}
return {
define: {
'process.env.API_BASE_URL': API_BASE_URL || API_BASE_PATH,
},
// only proxy when using `umi dev`
// if the assets are built, will not proxy
proxy: {
[API_BASE_PATH]: {
target: PROXY_TARGET_URL,
changeOrigin: true,
pathRewrite: { [`^${API_BASE_PATH}`]: API_BASE_PATH },
},
// for local storage
...getLocalStorageProxy(),
},
};
}

View File

@ -12580,14 +12580,6 @@ koa-is-json@^1.0.0:
resolved "https://registry.npm.taobao.org/koa-is-json/download/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14"
integrity sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=
koa-mount@^4.0.0:
version "4.0.0"
resolved "https://registry.npm.taobao.org/koa-mount/download/koa-mount-4.0.0.tgz#e0265e58198e1a14ef889514c607254ff386329c"
integrity sha1-4CZeWBmOGhTviJUUxgclT/OGMpw=
dependencies:
debug "^4.0.1"
koa-compose "^4.1.0"
koa-send@^5.0.0:
version "5.0.1"
resolved "https://registry.npm.taobao.org/koa-send/download/koa-send-5.0.1.tgz#39dceebfafb395d0d60beaffba3a70b4f543fe79"