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:
parent
12f58effad
commit
60b966f59e
19
.env.example
19
.env.example
@ -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=
|
||||
|
11
.github/workflows/vercel.yml
vendored
11
.github/workflows/vercel.yml
vendored
@ -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
|
||||
|
17
.umirc.ts
17
.umirc.ts
@ -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'],
|
||||
|
@ -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,
|
||||
|
@ -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' },
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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',
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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: {},
|
||||
|
@ -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/',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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('');
|
||||
}
|
||||
|
@ -19,6 +19,17 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'belongsToMany',
|
||||
name: 'pubkeys',
|
||||
target: 'attachments',
|
||||
attachment: {
|
||||
storage: 'local_private',
|
||||
rules: {
|
||||
mimetype: ['text/*']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'belongsToMany',
|
||||
name: 'photos',
|
||||
|
@ -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
|
||||
|
@ -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,7 +25,11 @@ 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');
|
||||
|
||||
@ -32,13 +37,13 @@ export default {
|
||||
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,
|
||||
|
@ -3,28 +3,26 @@ 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';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// TODO(optimize): 初始化的时机不应该放在中间件里
|
||||
export function middleware(app) {
|
||||
if (process.env.NOCOBASE_ENV === 'production') {
|
||||
return;
|
||||
// use koa-mount match logic
|
||||
function match(basePath: string, pathname: string): boolean {
|
||||
if (!pathname.startsWith(basePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const storages = new Map<string, any>();
|
||||
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');
|
||||
|
||||
return app.use(async function (ctx, next) {
|
||||
const items = await StorageModel.findAll({
|
||||
where: {
|
||||
type: STORAGE_TYPE_LOCAL,
|
||||
@ -33,13 +31,43 @@ export function middleware(app) {
|
||||
|
||||
const primaryKey = StorageModel.primaryKeyAttribute;
|
||||
|
||||
storages.clear();
|
||||
for (const storage of items) {
|
||||
|
||||
// TODO:未解决 storage 更新问题
|
||||
if (storages.has(storage[primaryKey])) {
|
||||
continue;
|
||||
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));
|
||||
}
|
||||
|
||||
export async function middleware(app, options?) {
|
||||
const LOCALHOST = `http://localhost:${process.env.API_PORT}`;
|
||||
|
||||
const StorageModel = app.db.getModel('storages');
|
||||
const storages = new Map<string, any>();
|
||||
|
||||
const localServerUpdateHook = createLocalServerUpdateHook(app, storages);
|
||||
StorageModel.addHook('afterCreate', localServerUpdateHook);
|
||||
StorageModel.addHook('afterUpdate', localServerUpdateHook);
|
||||
StorageModel.addHook('afterDestroy', localServerUpdateHook);
|
||||
|
||||
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) {
|
||||
if (url.origin && url.origin !== LOCALHOST) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const basePath = url.pathname.startsWith('/') ? url.pathname : `/${url.pathname}`;
|
||||
app.use(mount(basePath, serve(getDocumentRoot(storage))));
|
||||
if (!match(basePath, ctx.path)) {
|
||||
continue;
|
||||
}
|
||||
storages.set(storage.primaryKey, storage);
|
||||
|
||||
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
|
||||
});
|
||||
|
@ -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}`;
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
9
packages/utils/src/merge.ts
Normal file
9
packages/utils/src/merge.ts
Normal 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,
|
||||
});
|
||||
}
|
36
packages/utils/src/umiConfig.ts
Normal file
36
packages/utils/src/umiConfig.ts
Normal 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(),
|
||||
},
|
||||
};
|
||||
}
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user