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_MYSQL_PORT=3306
|
||||||
DB_POSTGRES_PORT=5432
|
DB_POSTGRES_PORT=5432
|
||||||
VERDACCIO_PORT=4873
|
VERDACCIO_PORT=4873
|
||||||
|
|
||||||
APP_PORT=13001
|
APP_PORT=13001
|
||||||
API_PORT=13002
|
|
||||||
|
|
||||||
########## NOCOBASE ENV ##########
|
########## NOCOBASE ENV ##########
|
||||||
|
|
||||||
@ -27,7 +27,19 @@ DB_LOG_SQL=
|
|||||||
# API & APP
|
# API & APP
|
||||||
|
|
||||||
NOCOBASE_ENV=
|
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
|
APP_DIST=packages/app/dist
|
||||||
|
|
||||||
# ADMIN USER (Initialization only)
|
# ADMIN USER (Initialization only)
|
||||||
@ -42,7 +54,8 @@ STORAGE_TYPE=local
|
|||||||
|
|
||||||
# LOCAL STORAGE
|
# LOCAL STORAGE
|
||||||
LOCAL_STORAGE_USE_STATIC_SERVER=true
|
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
|
||||||
ALI_OSS_STORAGE_BASE_URL=
|
ALI_OSS_STORAGE_BASE_URL=
|
||||||
|
11
.github/workflows/vercel.yml
vendored
11
.github/workflows/vercel.yml
vendored
@ -24,10 +24,13 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-node-
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
- run: yarn install
|
- name: Build
|
||||||
- run: yarn bootstrap
|
env:
|
||||||
- run: echo "API_URL=${{ secrets.API_URL }}" >> .env
|
API_BASE_URL: ${{ secrets.API_ORIGIN }}${{ secrets.API_BASE_PATH }}
|
||||||
- run: yarn build-docs
|
run: |
|
||||||
|
yarn install
|
||||||
|
yarn bootstrap
|
||||||
|
yarn build-docs
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
uses: amondnet/vercel-action@v19
|
uses: amondnet/vercel-action@v19
|
||||||
|
17
.umirc.ts
17
.umirc.ts
@ -1,23 +1,20 @@
|
|||||||
import { defineConfig } from 'dumi';
|
import { defineConfig } from 'dumi';
|
||||||
|
import { getUmiConfig } from '@nocobase/utils';
|
||||||
const baseUrl = `http://localhost:${process.env.API_PORT || '13001'}/`;
|
|
||||||
console.log('baseUrl', baseUrl);
|
|
||||||
|
|
||||||
process.env.MFSU_AD = 'none';
|
process.env.MFSU_AD = 'none';
|
||||||
|
|
||||||
|
const umiConfig = getUmiConfig();
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
title: 'NocoBase',
|
title: 'NocoBase',
|
||||||
hash: true,
|
hash: true,
|
||||||
define: {
|
define: {
|
||||||
'process.env.API_URL': process.env.API_URL || `${baseUrl}api/`,
|
...umiConfig.define,
|
||||||
'process.env.API_HOSTNAME': process.env.API_HOSTNAME,
|
|
||||||
},
|
},
|
||||||
|
// only proxy when using `umi dev`
|
||||||
|
// if the assets are built, will not proxy
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api/': {
|
...umiConfig.proxy,
|
||||||
target: baseUrl,
|
|
||||||
changeOrigin: true,
|
|
||||||
pathRewrite: { '^/api/': '/api/' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
includes: ['docs', 'packages/client/src/schemas'],
|
includes: ['docs', 'packages/client/src/schemas'],
|
||||||
|
@ -15,7 +15,7 @@ import { UseRequestProvider } from 'ahooks';
|
|||||||
import { extend } from 'umi-request';
|
import { extend } from 'umi-request';
|
||||||
|
|
||||||
const request = extend({
|
const request = extend({
|
||||||
prefix: process.env.API_URL,
|
prefix: process.env.API_BASE_URL,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -33,10 +33,6 @@ const client = new ClientSDK({
|
|||||||
request,
|
request,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('process.env.API_URL', process.env.API_URL);
|
|
||||||
|
|
||||||
// console.log = () => {}
|
|
||||||
|
|
||||||
const RouteSwitch = createRouteSwitch({
|
const RouteSwitch = createRouteSwitch({
|
||||||
components: {
|
components: {
|
||||||
AdminLayout,
|
AdminLayout,
|
||||||
|
@ -1,25 +1,27 @@
|
|||||||
|
import path from 'path';
|
||||||
import { defineConfig } from 'umi';
|
import { defineConfig } from 'umi';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import path from 'path';
|
import { getUmiConfig } from '@nocobase/utils';
|
||||||
|
|
||||||
dotenv.config({
|
dotenv.config({
|
||||||
path: path.resolve(__dirname, '../../.env'),
|
path: path.resolve(__dirname, '../../.env'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
process.env.MFSU_AD = 'none';
|
||||||
|
|
||||||
|
const umiConfig = getUmiConfig();
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
nodeModulesTransform: {
|
nodeModulesTransform: {
|
||||||
type: 'none',
|
type: 'none',
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
'process.env.API_URL': process.env.API_URL,
|
...umiConfig.define,
|
||||||
'process.env.API_PORT': process.env.API_PORT,
|
|
||||||
},
|
},
|
||||||
|
// only proxy when using `umi dev`
|
||||||
|
// if the assets are built, will not proxy
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
...umiConfig.proxy,
|
||||||
'target': `http://localhost:${process.env.API_PORT}/`,
|
|
||||||
'changeOrigin': true,
|
|
||||||
'pathRewrite': { '^/api': '/api' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', exact: false, component: '@/pages/index' },
|
{ path: '/', exact: false, component: '@/pages/index' },
|
||||||
|
@ -42,7 +42,7 @@ const api = new Server({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
resourcer: {
|
resourcer: {
|
||||||
prefix: '/api',
|
prefix: process.env.API_BASE_PATH || '/api/',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ for (const plugin of plugins) {
|
|||||||
|
|
||||||
api.plugin(
|
api.plugin(
|
||||||
require(`@nocobase/plugin-client/${libDir}/server`).default, {
|
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,
|
lang: process.env.APP_LANG,
|
||||||
// importDemo: true,
|
// importDemo: true,
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,7 @@ import { ClientSDK } from '../ClientSDK';
|
|||||||
import { extend } from 'umi-request';
|
import { extend } from 'umi-request';
|
||||||
|
|
||||||
const request = extend({
|
const request = extend({
|
||||||
prefix: process.env.API_URL,
|
prefix: process.env.API_BASE_URL,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ import { I18nextProvider } from 'react-i18next';
|
|||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
|
|
||||||
const request = extend({
|
const request = extend({
|
||||||
prefix: process.env.API_URL,
|
prefix: process.env.API_BASE_URL,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ request.use(async (ctx, next) => {
|
|||||||
const token = localStorage.getItem('NOCOBASE_TOKEN');
|
const token = localStorage.getItem('NOCOBASE_TOKEN');
|
||||||
if (token) {
|
if (token) {
|
||||||
headers['Authorization'] = `Bearer ${token}`;
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
headers['X-Hostname'] = process.env.API_HOSTNAME;
|
headers['X-Hostname'] = window.location.hostname;
|
||||||
}
|
}
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
@ -35,8 +35,6 @@ const client = new ClientSDK({
|
|||||||
request,
|
request,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('process.env.API_URL', process.env.API_URL);
|
|
||||||
|
|
||||||
// console.log = () => {}
|
// console.log = () => {}
|
||||||
|
|
||||||
const RouteSwitch = createRouteSwitch({
|
const RouteSwitch = createRouteSwitch({
|
||||||
|
@ -27,7 +27,7 @@ export function useDefaultAction() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const request = extend({
|
export const request = extend({
|
||||||
prefix: process.env.API_URL,
|
prefix: process.env.API_BASE_URL,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -40,8 +40,6 @@ request.use(async (ctx, next) => {
|
|||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('process.env.API_URL', process.env.API_URL);
|
|
||||||
|
|
||||||
export async function createOrUpdateCollection(data: any) {
|
export async function createOrUpdateCollection(data: any) {
|
||||||
return await request('collections:createOrUpdate', {
|
return await request('collections:createOrUpdate', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
@ -407,7 +407,7 @@ Upload.Attachment = connect(
|
|||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
listType={'picture-card'}
|
listType={'picture-card'}
|
||||||
fileList={fileList}
|
fileList={fileList}
|
||||||
action={`${process.env.API_URL}attachments:upload`}
|
action={`${process.env.API_BASE_URL}attachments:upload`}
|
||||||
onChange={(info) => {
|
onChange={(info) => {
|
||||||
setSync(false);
|
setSync(false);
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
|
@ -22,6 +22,18 @@ DB_STORAGE=db.sqlite
|
|||||||
# set to 'on' to enable log
|
# set to 'on' to enable log
|
||||||
DB_LOG_SQL=
|
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 USER (Initialization only)
|
||||||
|
|
||||||
ADMIN_EMAIL=admin@nocobase.com
|
ADMIN_EMAIL=admin@nocobase.com
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { defineConfig } from 'umi';
|
import { defineConfig } from 'umi';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { getUmiConfig } from '@nocobase/utils';
|
||||||
|
|
||||||
dotenv.config({
|
dotenv.config({
|
||||||
path: path.resolve(__dirname, './.env'),
|
path: path.resolve(__dirname, './.env'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const umiConfig = getUmiConfig();
|
||||||
|
|
||||||
process.env.MFSU_AD = 'none';
|
process.env.MFSU_AD = 'none';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@ -14,15 +17,12 @@ export default defineConfig({
|
|||||||
type: 'none',
|
type: 'none',
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
'process.env.API_URL': process.env.API_URL || `/api/`,
|
...umiConfig.define,
|
||||||
'process.env.API_PORT': process.env.API_PORT || '13001',
|
|
||||||
},
|
},
|
||||||
|
// only proxy when using `umi dev`
|
||||||
|
// if the assets are built, will not proxy
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
...umiConfig.proxy,
|
||||||
target: `http://localhost:${process.env.API_PORT || '13001'}/`,
|
|
||||||
changeOrigin: true,
|
|
||||||
pathRewrite: { '^/api': '/api' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
routes: [{ path: '/', exact: false, component: '@/pages/index' }],
|
routes: [{ path: '/', exact: false, component: '@/pages/index' }],
|
||||||
fastRefresh: {},
|
fastRefresh: {},
|
||||||
|
@ -7,7 +7,7 @@ const start = Date.now();
|
|||||||
const api = new Application({
|
const api = new Application({
|
||||||
database: dbConfig[process.env.DB_DIALECT || 'sqlite'],
|
database: dbConfig[process.env.DB_DIALECT || 'sqlite'],
|
||||||
resourcer: {
|
resourcer: {
|
||||||
prefix: '/api',
|
prefix: process.env.API_BASE_PATH || '/api/',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ export default {
|
|||||||
|
|
||||||
this.app.on('beforeStart', async () => {
|
this.app.on('beforeStart', async () => {
|
||||||
await database.getModel('collections').load({
|
await database.getModel('collections').load({
|
||||||
|
// TODO(bug): quick fix just comment out
|
||||||
skipExisting: true,
|
skipExisting: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,13 +7,11 @@
|
|||||||
"@koa/multer": "^3.0.0",
|
"@koa/multer": "^3.0.0",
|
||||||
"@nocobase/server": "^0.5.0-alpha.34",
|
"@nocobase/server": "^0.5.0-alpha.34",
|
||||||
"ali-oss": "^6.12.0",
|
"ali-oss": "^6.12.0",
|
||||||
"koa-mount": "^4.0.0",
|
|
||||||
"koa-static": "^5.0.0",
|
"koa-static": "^5.0.0",
|
||||||
"mime-match": "^1.0.2",
|
"mime-match": "^1.0.2",
|
||||||
"multer": "^1.4.2"
|
"multer": "^1.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nocobase/test": "^0.5.0-alpha.34",
|
|
||||||
"@types/multer": "^1.4.5"
|
"@types/multer": "^1.4.5"
|
||||||
},
|
},
|
||||||
"gitHead": "e7df1f93c4e23b9a666d99ee7372c02bdaec97c4"
|
"gitHead": "e7df1f93c4e23b9a666d99ee7372c02bdaec97c4"
|
||||||
|
@ -1,39 +1,52 @@
|
|||||||
|
import { promisify } from 'util';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import path from 'path';
|
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', () => {
|
describe('action', () => {
|
||||||
let app;
|
let app;
|
||||||
let agent;
|
let agent;
|
||||||
let api;
|
|
||||||
let db;
|
let db;
|
||||||
|
let http;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
app = await getApp();
|
app = await getApp();
|
||||||
agent = getAgent(app);
|
agent = app.agent();
|
||||||
api = getAPI(app);
|
http = app.listen(API_PORT);
|
||||||
db = app.db;
|
db = app.db;
|
||||||
|
|
||||||
const Storage = app.db.getModel('storages');
|
const Storage = db.getModel('storages');
|
||||||
await Storage.create({
|
await Storage.create({
|
||||||
name: `local_${Date.now().toString(36)}`,
|
name: `local1_${generatePrefixByPath()}`,
|
||||||
type: STORAGE_TYPE_LOCAL,
|
type: STORAGE_TYPE_LOCAL,
|
||||||
baseUrl: DEFAULT_LOCAL_BASE_URL,
|
baseUrl: DEFAULT_LOCAL_BASE_URL,
|
||||||
default: true
|
default: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => db.close());
|
afterEach(async () => {
|
||||||
|
await promisify(cb => http.close(cb))();
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
describe('direct attachment', () => {
|
describe('direct attachment', () => {
|
||||||
it('upload file should be ok', async () => {
|
it('upload file should be ok', async () => {
|
||||||
const response = await agent
|
const { body } = await agent
|
||||||
.post('/api/attachments:upload')
|
.resource('attachments')
|
||||||
.attach(FILE_FIELD_NAME, path.resolve(__dirname, './files/text.txt'));
|
.upload({
|
||||||
|
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt')
|
||||||
|
});
|
||||||
|
|
||||||
const matcher = {
|
const matcher = {
|
||||||
title: 'text',
|
title: 'text',
|
||||||
@ -44,14 +57,15 @@ describe('action', () => {
|
|||||||
meta: {},
|
meta: {},
|
||||||
storage_id: 1,
|
storage_id: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 文件上传和解析是否正常
|
// 文件上传和解析是否正常
|
||||||
expect(response.body).toMatchObject(matcher);
|
expect(body.data).toMatchObject(matcher);
|
||||||
// 文件的 url 是否正常生成
|
// 文件的 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 = db.getModel('attachments');
|
||||||
const attachment = await Attachment.findOne({
|
const attachment = await Attachment.findOne({
|
||||||
where: { id: response.body.id },
|
where: { id: body.data.id },
|
||||||
include: ['storage']
|
include: ['storage']
|
||||||
});
|
});
|
||||||
const storage = attachment.get('storage');
|
const storage = attachment.get('storage');
|
||||||
@ -73,10 +87,9 @@ describe('action', () => {
|
|||||||
// 文件是否保存到指定路径
|
// 文件是否保存到指定路径
|
||||||
expect(file.toString()).toBe('Hello world!\n');
|
expect(file.toString()).toBe('Hello world!\n');
|
||||||
|
|
||||||
const content = await agent.get(`/uploads${attachment.path}/${attachment.filename}`);
|
|
||||||
// 通过 url 是否能正确访问
|
// 通过 url 是否能正确访问
|
||||||
// TODO(bug)
|
const content = await requestFile(attachment.url, agent);
|
||||||
// expect(content.text).toBe('Hello world!\n');
|
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 () => {
|
it('upload with associatedKey, fail as 400 because file mimetype does not match', async () => {
|
||||||
const User = db.getModel('users');
|
const User = db.getModel('users');
|
||||||
const user = await User.create();
|
const user = await User.create();
|
||||||
const response = await api.resource('users.avatar').upload({
|
const response = await agent.resource('users.avatar').upload({
|
||||||
associatedKey: user.id,
|
associatedKey: user.id,
|
||||||
filePath: './files/text.txt'
|
file: path.resolve(__dirname, './files/text.txt')
|
||||||
});
|
});
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
@ -94,9 +107,9 @@ describe('action', () => {
|
|||||||
it('upload with associatedKey', async () => {
|
it('upload with associatedKey', async () => {
|
||||||
const User = db.getModel('users');
|
const User = db.getModel('users');
|
||||||
const user = await User.create();
|
const user = await User.create();
|
||||||
const response = await api.resource('users.avatar').upload({
|
const { body } = await agent.resource('users.avatar').upload({
|
||||||
associatedKey: user.id,
|
associatedKey: user.id,
|
||||||
filePath: './files/image.png',
|
file: path.resolve(__dirname, './files/image.png'),
|
||||||
values: { width: 100, height: 100 }
|
values: { width: 100, height: 100 }
|
||||||
});
|
});
|
||||||
const matcher = {
|
const matcher = {
|
||||||
@ -112,7 +125,7 @@ describe('action', () => {
|
|||||||
storage_id: 1,
|
storage_id: 1,
|
||||||
};
|
};
|
||||||
// 上传正常返回
|
// 上传正常返回
|
||||||
expect(response.body).toMatchObject(matcher);
|
expect(body.data).toMatchObject(matcher);
|
||||||
|
|
||||||
// 由于初始没有外键,无法获取
|
// 由于初始没有外键,无法获取
|
||||||
// await user.getAvatar()
|
// await user.getAvatar()
|
||||||
@ -120,13 +133,44 @@ describe('action', () => {
|
|||||||
include: ['avatar']
|
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
|
// TODO(bug): 没有 associatedKey 时路径解析资源名称不对,无法进入 action
|
||||||
it.skip('upload without associatedKey', async () => {
|
it.skip('upload without associatedKey', async () => {
|
||||||
const response = await api.resource('users.avatar').upload({
|
const { body } = await agent.resource('users.avatar').upload({
|
||||||
filePath: './files/image.png',
|
file: path.resolve(__dirname, './files/image.png'),
|
||||||
values: { width: 100, height: 100 }
|
values: { width: 100, height: 100 }
|
||||||
});
|
});
|
||||||
const matcher = {
|
const matcher = {
|
||||||
@ -136,13 +180,13 @@ describe('action', () => {
|
|||||||
size: 255,
|
size: 255,
|
||||||
mimetype: 'image/png',
|
mimetype: 'image/png',
|
||||||
// TODO(optimize): 可以考虑使用 qs 的 decoder 来进行类型解析
|
// TODO(optimize): 可以考虑使用 qs 的 decoder 来进行类型解析
|
||||||
// see: https://github.com/ljharb/qs/issues/91
|
// @see: https://github.com/ljharb/qs/issues/91
|
||||||
// 或考虑使用 query-string 库的 parseNumbers 等配置项
|
// 或考虑使用 query-string 库的 parseNumbers 等配置项
|
||||||
meta: { width: '100', height: '100' },
|
meta: { width: '100', height: '100' },
|
||||||
storage_id: 1,
|
storage_id: 1,
|
||||||
};
|
};
|
||||||
// 上传返回正常
|
// 上传返回正常
|
||||||
expect(response.body).toMatchObject(matcher);
|
expect(body.data).toMatchObject(matcher);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,72 +1,19 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import qs from 'qs';
|
|
||||||
import supertest from 'supertest';
|
import supertest from 'supertest';
|
||||||
import bodyParser from 'koa-bodyparser';
|
import { MockServer, mockServer } from '@nocobase/test';
|
||||||
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 plugin from '../server';
|
import plugin from '../server';
|
||||||
import { FILE_FIELD_NAME } from '../constants';
|
|
||||||
|
|
||||||
function getTestKey() {
|
export async function getApp(options = {}): Promise<MockServer> {
|
||||||
const { id } = require.main;
|
const app = mockServer({
|
||||||
const key = id
|
...options,
|
||||||
.replace(`${process.env.PWD}/packages`, '')
|
cors: {
|
||||||
.replace(/src\/__tests__/g, '')
|
origin: '*'
|
||||||
.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.plugin(require('@nocobase/plugin-collections/src/server').default);
|
||||||
app.resourcer.registerActionHandlers({ ...actions.associate, ...actions.common });
|
app.plugin(plugin);
|
||||||
app.registerPlugin({
|
await app.load();
|
||||||
'collections': [path.resolve(__dirname, '../../../plugin-collections')],
|
|
||||||
'file-manager': [plugin]
|
|
||||||
});
|
|
||||||
await app.loadPlugins();
|
|
||||||
app.db.import({
|
app.db.import({
|
||||||
directory: path.resolve(__dirname, './tables')
|
directory: path.resolve(__dirname, './tables')
|
||||||
});
|
});
|
||||||
@ -75,80 +22,13 @@ export async function getApp() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(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;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActionParams {
|
// because the app in supertest is using a random port
|
||||||
resourceKey?: string | number;
|
export function requestFile(url, agent) {
|
||||||
// resourceName?: string;
|
return path.isAbsolute(url)
|
||||||
// associatedName?: string;
|
? agent.get(url)
|
||||||
associatedKey?: string | number;
|
: supertest.agent(url).get('');
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,17 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsToMany',
|
||||||
|
name: 'pubkeys',
|
||||||
|
target: 'attachments',
|
||||||
|
attachment: {
|
||||||
|
storage: 'local_private',
|
||||||
|
rules: {
|
||||||
|
mimetype: ['text/*']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'belongsToMany',
|
type: 'belongsToMany',
|
||||||
name: 'photos',
|
name: 'photos',
|
||||||
|
@ -46,10 +46,10 @@ export async function middleware(ctx: Context, next: Next) {
|
|||||||
// 如果没有包含关联,则直接按默认文件上传至默认存储引擎
|
// 如果没有包含关联,则直接按默认文件上传至默认存储引擎
|
||||||
storage = await StorageModel.findOne({ where: { default: true } });
|
storage = await StorageModel.findOne({ where: { default: true } });
|
||||||
} else {
|
} else {
|
||||||
const fieldOptions = resourceField.getOptions();
|
const { attachment = {} } = resourceField.getOptions();
|
||||||
storage = await StorageModel.findOne({
|
storage = await StorageModel.findOne({
|
||||||
where: fieldOptions.defaultValue
|
where: attachment.storage
|
||||||
? { [StorageModel.primaryKeyAttribute]: fieldOptions.defaultValue }
|
? { name: attachment.storage }
|
||||||
: { default: true }
|
: { default: true }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -75,8 +75,8 @@ export async function middleware(ctx: Context, next: Next) {
|
|||||||
},
|
},
|
||||||
storage: makeStorage(storage),
|
storage: makeStorage(storage),
|
||||||
};
|
};
|
||||||
const upload = multer(multerOptions);
|
const uploader = multer(multerOptions);
|
||||||
return upload.single(FILE_FIELD_NAME)(ctx, next);
|
return uploader.single(FILE_FIELD_NAME)(ctx, next);
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function action(ctx: Context, next: 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 { associatedName, associatedKey, resourceField } = ctx.action.params;
|
||||||
const extname = path.extname(file.filename);
|
const extname = path.extname(file.filename);
|
||||||
|
const urlPath = storage.path
|
||||||
|
? (storage.path.startsWith('/')
|
||||||
|
? storage.path
|
||||||
|
: `/${storage.path}`)
|
||||||
|
: '';
|
||||||
const data = {
|
const data = {
|
||||||
title: file.originalname.replace(extname, ''),
|
title: file.originalname.replace(extname, ''),
|
||||||
filename: file.filename,
|
filename: file.filename,
|
||||||
@ -94,7 +99,7 @@ export async function action(ctx: Context, next: Next) {
|
|||||||
path: storage.path,
|
path: storage.path,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
// 直接缓存起来
|
// 直接缓存起来
|
||||||
url: `${storage.baseUrl}${storage.path}/${file.filename}`,
|
url: `${storage.baseUrl}${urlPath}/${file.filename}`,
|
||||||
mimetype: file.mimetype,
|
mimetype: file.mimetype,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
meta: ctx.request.body
|
meta: ctx.request.body
|
||||||
|
@ -8,8 +8,9 @@ import {
|
|||||||
middleware as uploadMiddleware,
|
middleware as uploadMiddleware,
|
||||||
} from './actions/upload';
|
} from './actions/upload';
|
||||||
import {
|
import {
|
||||||
middleware as localMiddleware,
|
middleware as localMiddleware
|
||||||
} from './storages/local';
|
} from './storages/local';
|
||||||
|
import { STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_LOCAL } from './constants';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'file-manager',
|
name: 'file-manager',
|
||||||
@ -24,21 +25,25 @@ export default {
|
|||||||
// 暂时中间件只能通过 use 加进来
|
// 暂时中间件只能通过 use 加进来
|
||||||
resourcer.use(uploadMiddleware);
|
resourcer.use(uploadMiddleware);
|
||||||
resourcer.registerActionHandler('upload', uploadAction);
|
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');
|
const Storage = database.getModel('storages');
|
||||||
|
|
||||||
this.app.on('db.init', async () => {
|
this.app.on('db.init', async () => {
|
||||||
await Storage.create({
|
await Storage.create({
|
||||||
title: '本地存储',
|
title: '本地存储',
|
||||||
name: `local`,
|
name: `local`,
|
||||||
type: 'local',
|
type: STORAGE_TYPE_LOCAL,
|
||||||
baseUrl: process.env.LOCAL_STORAGE_BASE_URL,
|
baseUrl: process.env.LOCAL_STORAGE_BASE_URL || `http://localhost:${process.env.API_PORT}/uploads`,
|
||||||
default: process.env.STORAGE_TYPE === 'local',
|
default: process.env.STORAGE_TYPE === STORAGE_TYPE_LOCAL,
|
||||||
});
|
});
|
||||||
await Storage.create({
|
await Storage.create({
|
||||||
name: `ali-oss`,
|
name: `ali-oss`,
|
||||||
type: 'ali-oss',
|
type: STORAGE_TYPE_ALI_OSS,
|
||||||
baseUrl: process.env.ALI_OSS_STORAGE_BASE_URL,
|
baseUrl: process.env.ALI_OSS_STORAGE_BASE_URL,
|
||||||
options: {
|
options: {
|
||||||
region: process.env.ALI_OSS_REGION,
|
region: process.env.ALI_OSS_REGION,
|
||||||
|
@ -3,43 +3,71 @@ import { URL } from 'url';
|
|||||||
import mkdirp from 'mkdirp';
|
import mkdirp from 'mkdirp';
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import serve from 'koa-static';
|
import serve from 'koa-static';
|
||||||
import mount from 'koa-mount';
|
|
||||||
import { STORAGE_TYPE_LOCAL } from '../constants';
|
import { STORAGE_TYPE_LOCAL } from '../constants';
|
||||||
import { getFilename } from '../utils';
|
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 {
|
export function getDocumentRoot(storage): string {
|
||||||
const { documentRoot = 'uploads' } = storage.options || {};
|
const { documentRoot = 'uploads' } = storage.options || {};
|
||||||
// TODO(feature): 后面考虑以字符串模板的方式使用,可注入 req/action 相关变量,以便于区分文件夹
|
// TODO(feature): 后面考虑以字符串模板的方式使用,可注入 req/action 相关变量,以便于区分文件夹
|
||||||
return path.resolve(path.isAbsolute(documentRoot)
|
return path.resolve(path.isAbsolute(documentRoot)
|
||||||
? documentRoot
|
? documentRoot
|
||||||
: path.join(process.cwd(), documentRoot), storage.path);
|
: path.join(process.cwd(), documentRoot));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(optimize): 初始化的时机不应该放在中间件里
|
export async function middleware(app, options?) {
|
||||||
export function middleware(app) {
|
const LOCALHOST = `http://localhost:${process.env.API_PORT}`;
|
||||||
if (process.env.NOCOBASE_ENV === 'production') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const storages = new Map<string, any>();
|
|
||||||
const StorageModel = app.db.getModel('storages');
|
const StorageModel = app.db.getModel('storages');
|
||||||
|
const storages = new Map<string, any>();
|
||||||
|
|
||||||
return app.use(async function (ctx, next) {
|
const localServerUpdateHook = createLocalServerUpdateHook(app, storages);
|
||||||
const items = await StorageModel.findAll({
|
StorageModel.addHook('afterCreate', localServerUpdateHook);
|
||||||
where: {
|
StorageModel.addHook('afterUpdate', localServerUpdateHook);
|
||||||
type: STORAGE_TYPE_LOCAL,
|
StorageModel.addHook('afterDestroy', localServerUpdateHook);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const primaryKey = StorageModel.primaryKeyAttribute;
|
await update(app, storages);
|
||||||
|
|
||||||
for (const storage of items) {
|
|
||||||
|
|
||||||
// TODO:未解决 storage 更新问题
|
|
||||||
if (storages.has(storage[primaryKey])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
app.use(async function (ctx, next) {
|
||||||
|
for (const storage of storages.values()) {
|
||||||
const baseUrl = storage.get('baseUrl');
|
const baseUrl = storage.get('baseUrl');
|
||||||
|
|
||||||
let url;
|
let url;
|
||||||
@ -47,33 +75,40 @@ export function middleware(app) {
|
|||||||
url = new URL(baseUrl);
|
url = new URL(baseUrl);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
url = {
|
url = {
|
||||||
protocol: 'http:',
|
|
||||||
hostname: 'localhost',
|
|
||||||
port: process.env.API_PORT,
|
|
||||||
pathname: baseUrl
|
pathname: baseUrl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 以下情况才认为当前进程所应该提供静态服务
|
// 以下情况才认为当前进程所应该提供静态服务
|
||||||
// 否则都忽略,交给其他 server 来提供(如 nginx/cdn 等)
|
// 否则都忽略,交给其他 server 来提供(如 nginx/cdn 等)
|
||||||
// TODO(bug): https、端口 80 默认值和其他本地 ip/hostname 的情况未考虑
|
if (url.origin && url.origin !== LOCALHOST) {
|
||||||
// TODO 实际应该用 NOCOBASE_ENV 来判断,或者抛给 env 处理
|
continue;
|
||||||
if (process.env.LOCAL_STORAGE_USE_STATIC_SERVER) {
|
|
||||||
const basePath = url.pathname.startsWith('/') ? url.pathname : `/${url.pathname}`;
|
|
||||||
app.use(mount(basePath, serve(getDocumentRoot(storage))));
|
|
||||||
}
|
}
|
||||||
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();
|
await next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (storage) => multer.diskStorage({
|
export default (storage) => multer.diskStorage({
|
||||||
destination: function (req, file, cb) {
|
destination: function (req, file, cb) {
|
||||||
const destPath = getDocumentRoot(storage);
|
const destPath = path.join(getDocumentRoot(storage), storage.path);
|
||||||
mkdirp(destPath).then(() => {
|
mkdirp(destPath, (err: Error | null) => cb(err, destPath));
|
||||||
cb(null, destPath);
|
|
||||||
}).catch(cb);
|
|
||||||
},
|
},
|
||||||
filename: getFilename
|
filename: getFilename
|
||||||
});
|
});
|
||||||
|
@ -144,7 +144,7 @@ export function parseRequest(request: ParseRequest, options: ParseOptions = {}):
|
|||||||
|
|
||||||
const params: ParsedParams = {};
|
const params: ParsedParams = {};
|
||||||
|
|
||||||
let prefix = (options.prefix || '').trim();
|
let prefix = (options.prefix || '').trim().replace(/\/$/, '');
|
||||||
|
|
||||||
if (prefix && !prefix.startsWith('/')) {
|
if (prefix && !prefix.startsWith('/')) {
|
||||||
prefix = `/${prefix}`;
|
prefix = `/${prefix}`;
|
||||||
|
@ -49,7 +49,7 @@ export class MockServer extends Application {
|
|||||||
agent(): SuperAgentTest & { resource: (name: string) => Resource } {
|
agent(): SuperAgentTest & { resource: (name: string) => Resource } {
|
||||||
const agent = supertest.agent(this.callback());
|
const agent = supertest.agent(this.callback());
|
||||||
const prefix = this.resourcer.options.prefix;
|
const prefix = this.resourcer.options.prefix;
|
||||||
const proxy = new Proxy({}, {
|
const proxy = new Proxy(agent, {
|
||||||
get(target, method: string, receiver) {
|
get(target, method: string, receiver) {
|
||||||
if (method === 'resource') {
|
if (method === 'resource') {
|
||||||
return (name: string) => {
|
return (name: string) => {
|
||||||
@ -74,7 +74,7 @@ export class MockServer extends Application {
|
|||||||
if (resourceKey) {
|
if (resourceKey) {
|
||||||
url += `/${resourceKey}`;
|
url += `/${resourceKey}`;
|
||||||
}
|
}
|
||||||
console.log('request url: ' + url);
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'upload':
|
case 'upload':
|
||||||
return agent
|
return agent
|
||||||
|
@ -1,9 +1,2 @@
|
|||||||
import deepmerge from 'deepmerge';
|
export * from './merge';
|
||||||
|
export * from './umiConfig';
|
||||||
const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray
|
|
||||||
|
|
||||||
export function merge(obj1: any, obj2: any) {
|
|
||||||
return deepmerge(obj1, obj2, {
|
|
||||||
arrayMerge: overwriteMerge,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
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"
|
resolved "https://registry.npm.taobao.org/koa-is-json/download/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14"
|
||||||
integrity sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=
|
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:
|
koa-send@^5.0.0:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.npm.taobao.org/koa-send/download/koa-send-5.0.1.tgz#39dceebfafb395d0d60beaffba3a70b4f543fe79"
|
resolved "https://registry.npm.taobao.org/koa-send/download/koa-send-5.0.1.tgz#39dceebfafb395d0d60beaffba3a70b4f543fe79"
|
||||||
|
Loading…
Reference in New Issue
Block a user