mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-12-02 04:07:50 +08:00
Add S3 storage and refactors (#124)
* add s3 storage and refactors * fix env and dependencies
This commit is contained in:
parent
c177ebb8e3
commit
5dfa57581a
@ -50,7 +50,7 @@ ADMIN_PASSWORD=admin123
|
||||
# STORAGE (Initialization only)
|
||||
|
||||
# local or ali-oss
|
||||
STORAGE_TYPE=local
|
||||
DEFAULT_STORAGE_TYPE=local
|
||||
|
||||
# LOCAL STORAGE
|
||||
LOCAL_STORAGE_USE_STATIC_SERVER=true
|
||||
@ -63,3 +63,10 @@ ALI_OSS_REGION=oss-cn-beijing
|
||||
ALI_OSS_ACCESS_KEY_ID=
|
||||
ALI_OSS_ACCESS_KEY_SECRET=
|
||||
ALI_OSS_BUCKET=
|
||||
|
||||
# AWS
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_S3_REGION=
|
||||
AWS_S3_BUCKET=
|
||||
AWS_S3_STORAGE_BASE_URL=
|
||||
|
@ -32,8 +32,8 @@ ADMIN_PASSWORD=admin
|
||||
|
||||
# STORAGE (Initialization only)
|
||||
|
||||
# local or ali-oss
|
||||
STORAGE_TYPE=local
|
||||
# local or ali-oss or s3
|
||||
DEFAULT_STORAGE_TYPE=local
|
||||
|
||||
# LOCAL STORAGE
|
||||
LOCAL_STORAGE_USE_STATIC_SERVER=true
|
||||
@ -45,3 +45,10 @@ ALI_OSS_REGION=oss-cn-beijing
|
||||
ALI_OSS_ACCESS_KEY_ID=
|
||||
ALI_OSS_ACCESS_KEY_SECRET=
|
||||
ALI_OSS_BUCKET=
|
||||
|
||||
# AWS
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_S3_REGION=
|
||||
AWS_S3_BUCKET=
|
||||
AWS_S3_STORAGE_BASE_URL=
|
||||
|
@ -30,7 +30,7 @@ ADMIN_PASSWORD=admin
|
||||
# STORAGE (Initialization only)
|
||||
|
||||
# local or ali-oss
|
||||
STORAGE_TYPE=local
|
||||
DEFAULT_STORAGE_TYPE=local
|
||||
|
||||
# LOCAL STORAGE
|
||||
LOCAL_STORAGE_USE_STATIC_SERVER=true
|
||||
@ -42,3 +42,10 @@ ALI_OSS_REGION=oss-cn-beijing
|
||||
ALI_OSS_ACCESS_KEY_ID=
|
||||
ALI_OSS_ACCESS_KEY_SECRET=
|
||||
ALI_OSS_BUCKET=
|
||||
|
||||
# AWS
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_S3_REGION=
|
||||
AWS_S3_BUCKET=
|
||||
AWS_S3_STORAGE_BASE_URL=
|
||||
|
@ -42,7 +42,7 @@ ADMIN_PASSWORD=admin123
|
||||
# STORAGE (Initialization only)
|
||||
|
||||
# local or ali-oss
|
||||
STORAGE_TYPE=local
|
||||
DEFAULT_STORAGE_TYPE=local
|
||||
|
||||
# LOCAL STORAGE
|
||||
LOCAL_STORAGE_USE_STATIC_SERVER=true
|
||||
@ -54,3 +54,10 @@ ALI_OSS_REGION=oss-cn-beijing
|
||||
ALI_OSS_ACCESS_KEY_ID=
|
||||
ALI_OSS_ACCESS_KEY_SECRET=
|
||||
ALI_OSS_BUCKET=
|
||||
|
||||
# AWS
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_S3_REGION=
|
||||
AWS_S3_BUCKET=
|
||||
AWS_S3_STORAGE_BASE_URL=
|
||||
|
@ -5,11 +5,12 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@koa/multer": "^3.0.0",
|
||||
"@nocobase/server": "^0.5.0-alpha.34",
|
||||
"ali-oss": "^6.12.0",
|
||||
"aws-sdk": "^2.2.32",
|
||||
"koa-static": "^5.0.0",
|
||||
"mime-match": "^1.0.2",
|
||||
"multer": "^1.4.2"
|
||||
"multer": "^1.4.2",
|
||||
"multer-aliyun-oss": "1.1.1",
|
||||
"multer-s3": "^2.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/multer": "^1.4.5"
|
||||
|
@ -11,9 +11,11 @@ export async function getApp(options = {}): Promise<MockServer> {
|
||||
origin: '*'
|
||||
}
|
||||
});
|
||||
app.plugin(require('@nocobase/plugin-collections/src/server').default);
|
||||
|
||||
app.plugin(plugin);
|
||||
|
||||
await app.load();
|
||||
|
||||
app.db.import({
|
||||
directory: path.resolve(__dirname, './tables')
|
||||
});
|
||||
@ -26,9 +28,11 @@ export async function getApp(options = {}): Promise<MockServer> {
|
||||
return app;
|
||||
}
|
||||
|
||||
// because the app in supertest is using a random port
|
||||
// because the app in supertest will use a random port
|
||||
export function requestFile(url, agent) {
|
||||
return path.isAbsolute(url)
|
||||
// url starts with double slash "//" will be considered as http or https
|
||||
// url starts with single slash "/" will be considered from local server
|
||||
return (url[0] === '/' && url[1] !== '/'
|
||||
? agent.get(url)
|
||||
: supertest.agent(url).get('');
|
||||
: supertest.agent(url).get(''));
|
||||
}
|
||||
|
@ -0,0 +1,72 @@
|
||||
import path from 'path';
|
||||
|
||||
import { generatePrefixByPath } from '@nocobase/test';
|
||||
|
||||
import aliossStorage from '../../storages/ali-oss';
|
||||
import { FILE_FIELD_NAME } from '../../constants';
|
||||
import { getApp, requestFile } from '..';
|
||||
|
||||
const itif = process.env.ALI_OSS_ACCESS_KEY_SECRET ? it : it.skip;
|
||||
|
||||
describe('storage:ali-oss', () => {
|
||||
let app;
|
||||
let agent;
|
||||
let db;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await getApp();
|
||||
agent = app.agent();
|
||||
db = app.db;
|
||||
|
||||
const Storage = db.getModel('storages');
|
||||
await Storage.create({
|
||||
...aliossStorage.defaults(),
|
||||
name: `ali-oss_${generatePrefixByPath()}`,
|
||||
default: true,
|
||||
path: 'test/path'
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
describe('direct attachment', () => {
|
||||
itif('upload file should be ok', async () => {
|
||||
const { body } = await agent
|
||||
.resource('attachments')
|
||||
.upload({
|
||||
[FILE_FIELD_NAME]: path.resolve(__dirname, '../files/text.txt')
|
||||
});
|
||||
|
||||
const Attachment = db.getModel('attachments');
|
||||
const attachment = await Attachment.findOne({
|
||||
where: { id: body.data.id },
|
||||
include: ['storage']
|
||||
});
|
||||
|
||||
const matcher = {
|
||||
title: 'text',
|
||||
extname: '.txt',
|
||||
path: 'test/path',
|
||||
// TODO(bug): alioss will not return the size of file
|
||||
// size: 13,
|
||||
mimetype: 'text/plain',
|
||||
meta: {},
|
||||
storage_id: 1,
|
||||
};
|
||||
|
||||
// 文件上传和解析是否正常
|
||||
expect(body.data).toMatchObject(matcher);
|
||||
// 文件的 url 是否正常生成
|
||||
expect(body.data.url).toBe(`${attachment.storage.baseUrl}/${body.data.path}/${body.data.filename}`);
|
||||
// 文件的数据是否正常保存
|
||||
expect(attachment).toMatchObject(matcher);
|
||||
|
||||
// 通过 url 是否能正确访问
|
||||
const content = await requestFile(attachment.url, agent);
|
||||
|
||||
expect(content.text).toBe('Hello world!\n');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,70 @@
|
||||
import path from 'path';
|
||||
|
||||
import { generatePrefixByPath } from '@nocobase/test';
|
||||
|
||||
import s3Storage from '../../storages/s3';
|
||||
import { FILE_FIELD_NAME } from '../../constants';
|
||||
import { getApp, requestFile } from '..';
|
||||
|
||||
const itif = process.env.AWS_SECRET_ACCESS_KEY ? it : it.skip;
|
||||
|
||||
describe('storage:s3', () => {
|
||||
let app;
|
||||
let agent;
|
||||
let db;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await getApp();
|
||||
agent = app.agent();
|
||||
db = app.db;
|
||||
|
||||
const Storage = db.getModel('storages');
|
||||
await Storage.create({
|
||||
...s3Storage.defaults(),
|
||||
name: `s3_${generatePrefixByPath()}`,
|
||||
default: true,
|
||||
path: 'test/path'
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
describe('direct attachment', () => {
|
||||
itif('upload file should be ok', async () => {
|
||||
const { body } = await agent
|
||||
.resource('attachments')
|
||||
.upload({
|
||||
[FILE_FIELD_NAME]: path.resolve(__dirname, '../files/text.txt')
|
||||
});
|
||||
|
||||
const Attachment = db.getModel('attachments');
|
||||
const attachment = await Attachment.findOne({
|
||||
where: { id: body.data.id },
|
||||
include: ['storage']
|
||||
});
|
||||
|
||||
const matcher = {
|
||||
title: 'text',
|
||||
extname: '.txt',
|
||||
path: 'test/path',
|
||||
size: 13,
|
||||
mimetype: 'text/plain',
|
||||
meta: {},
|
||||
storage_id: 1,
|
||||
};
|
||||
|
||||
// 文件上传和解析是否正常
|
||||
expect(body.data).toMatchObject(matcher);
|
||||
// 文件的 url 是否正常生成
|
||||
expect(body.data.url).toBe(`${attachment.storage.baseUrl}/${body.data.path}/${body.data.filename}`);
|
||||
// 文件的数据是否正常保存
|
||||
expect(attachment).toMatchObject(matcher);
|
||||
|
||||
// 通过 url 是否能正确访问
|
||||
const content = await requestFile(attachment.url, agent);
|
||||
expect(content.text).toBe('Hello world!\n');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
import path from 'path';
|
||||
import multer from '@koa/multer';
|
||||
import { Context, Next } from '@nocobase/actions';
|
||||
import storageMakers from '../storages';
|
||||
import { getStorageConfig } from '../storages';
|
||||
import * as Rules from '../rules';
|
||||
import { FILE_FIELD_NAME, LIMIT_FILES, LIMIT_MAX_FILE_SIZE } from '../constants';
|
||||
|
||||
@ -61,8 +61,8 @@ export async function middleware(ctx: Context, next: Next) {
|
||||
// 传递已取得的存储引擎,避免重查
|
||||
ctx.storage = storage;
|
||||
|
||||
const makeStorage = storageMakers.get(storage.type);
|
||||
if (!makeStorage) {
|
||||
const storageConfig = getStorageConfig(storage.type);
|
||||
if (!storageConfig) {
|
||||
console.error(`[file-manager] storage type "${storage.type}" is not defined`);
|
||||
return ctx.throw(500);
|
||||
}
|
||||
@ -73,10 +73,10 @@ export async function middleware(ctx: Context, next: Next) {
|
||||
// 每次只允许提交一个文件
|
||||
files: LIMIT_FILES
|
||||
},
|
||||
storage: makeStorage(storage),
|
||||
storage: storageConfig.make(storage),
|
||||
};
|
||||
const uploader = multer(multerOptions);
|
||||
return uploader.single(FILE_FIELD_NAME)(ctx, next);
|
||||
const upload = multer(multerOptions).single(FILE_FIELD_NAME);
|
||||
return upload(ctx, next);
|
||||
};
|
||||
|
||||
export async function action(ctx: Context, next: Next) {
|
||||
@ -84,31 +84,36 @@ export async function action(ctx: Context, next: Next) {
|
||||
if (!file) {
|
||||
return ctx.throw(400, 'file validation failed');
|
||||
}
|
||||
const { associatedName, associatedKey, resourceField } = ctx.action.params;
|
||||
const extname = path.extname(file.filename);
|
||||
|
||||
const storageConfig = getStorageConfig(storage.type);
|
||||
const { [storageConfig.filenameKey || 'filename']: name } = file;
|
||||
// make compatible filename across cloud service (with path)
|
||||
const filename = path.basename(name);
|
||||
const extname = path.extname(filename);
|
||||
const urlPath = storage.path
|
||||
? (storage.path.startsWith('/')
|
||||
? storage.path
|
||||
: `/${storage.path}`)
|
||||
? storage.path.replace(/^([^\/])/, '/$1')
|
||||
: '';
|
||||
|
||||
const data = {
|
||||
title: file.originalname.replace(extname, ''),
|
||||
filename: file.filename,
|
||||
filename,
|
||||
extname,
|
||||
// TODO(feature): 暂时两者相同,后面 storage.path 模版化以后,这里只是 file 实际的 path
|
||||
path: storage.path,
|
||||
size: file.size,
|
||||
// 直接缓存起来
|
||||
url: `${storage.baseUrl}${urlPath}/${file.filename}`,
|
||||
url: `${storage.baseUrl}${urlPath}/${filename}`,
|
||||
mimetype: file.mimetype,
|
||||
// @ts-ignore
|
||||
meta: ctx.request.body
|
||||
}
|
||||
|
||||
meta: ctx.request.body,
|
||||
...(storageConfig.getFileData ? storageConfig.getFileData(file) : {})
|
||||
};
|
||||
|
||||
const attachment = await ctx.db.sequelize.transaction(async transaction => {
|
||||
// TODO(optimize): 应使用关联 accessors 获取
|
||||
const result = await storage.createAttachment(data, { transaction });
|
||||
|
||||
|
||||
const { associatedName, associatedKey, resourceField } = ctx.action.params;
|
||||
if (associatedKey && resourceField) {
|
||||
const Attachment = ctx.db.getModel('attachments');
|
||||
const SourceModel = ctx.db.getModel(associatedName);
|
||||
|
@ -4,3 +4,4 @@ export const LIMIT_MAX_FILE_SIZE = 1024 * 1024 * 1024;
|
||||
|
||||
export const STORAGE_TYPE_LOCAL = 'local';
|
||||
export const STORAGE_TYPE_ALI_OSS = 'ali-oss';
|
||||
export const STORAGE_TYPE_S3 = 's3';
|
||||
|
@ -0,0 +1 @@
|
||||
export * from './constants';
|
@ -1,16 +1,14 @@
|
||||
import path from 'path';
|
||||
import Database from '@nocobase/database';
|
||||
import Resourcer from '@nocobase/resourcer';
|
||||
import { PluginOptions, Plugin } from '@nocobase/server';
|
||||
import { PluginOptions } from '@nocobase/server';
|
||||
|
||||
import {
|
||||
action as uploadAction,
|
||||
middleware as uploadMiddleware,
|
||||
} from './actions/upload';
|
||||
import {
|
||||
middleware as localMiddleware
|
||||
} from './storages/local';
|
||||
import { STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_LOCAL } from './constants';
|
||||
import { getStorageConfig } from './storages';
|
||||
import { STORAGE_TYPE_LOCAL } from './constants';
|
||||
|
||||
export default {
|
||||
name: 'file-manager',
|
||||
@ -26,33 +24,25 @@ export default {
|
||||
resourcer.use(uploadMiddleware);
|
||||
resourcer.registerActionHandler('upload', uploadAction);
|
||||
|
||||
if (process.env.NOCOBASE_ENV !== 'production'
|
||||
&& process.env.LOCAL_STORAGE_USE_STATIC_SERVER) {
|
||||
await localMiddleware(this.app);
|
||||
}
|
||||
|
||||
const Storage = database.getModel('storages');
|
||||
const { DEFAULT_STORAGE_TYPE } = process.env;
|
||||
|
||||
if (process.env.NOCOBASE_ENV !== 'production'
|
||||
&& DEFAULT_STORAGE_TYPE === STORAGE_TYPE_LOCAL
|
||||
&& process.env.LOCAL_STORAGE_USE_STATIC_SERVER
|
||||
) {
|
||||
await getStorageConfig(STORAGE_TYPE_LOCAL).middleware(this.app);
|
||||
}
|
||||
|
||||
this.app.on('db.init', async () => {
|
||||
await Storage.create({
|
||||
title: '本地存储',
|
||||
name: `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: STORAGE_TYPE_ALI_OSS,
|
||||
baseUrl: process.env.ALI_OSS_STORAGE_BASE_URL,
|
||||
options: {
|
||||
region: process.env.ALI_OSS_REGION,
|
||||
accessKeyId: process.env.ALI_OSS_ACCESS_KEY_ID,
|
||||
accessKeySecret: process.env.ALI_OSS_ACCESS_KEY_SECRET,
|
||||
bucket: process.env.ALI_OSS_BUCKET,
|
||||
},
|
||||
default: process.env.STORAGE_TYPE === 'ali-oss',
|
||||
});
|
||||
const defaultStorageConfig = getStorageConfig(DEFAULT_STORAGE_TYPE);
|
||||
if (defaultStorageConfig) {
|
||||
const StorageModel = database.getModel('storages');
|
||||
await StorageModel.create({
|
||||
...defaultStorageConfig.defaults(),
|
||||
type: DEFAULT_STORAGE_TYPE,
|
||||
default: true
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
} as PluginOptions;
|
||||
|
@ -1,42 +1,26 @@
|
||||
import AliOss from 'ali-oss';
|
||||
import { getFilename } from '../utils';
|
||||
import { STORAGE_TYPE_ALI_OSS } from '../constants';
|
||||
import { cloudFilenameGetter } from '../utils';
|
||||
|
||||
export class AliOssStorage {
|
||||
|
||||
private client: AliOss;
|
||||
|
||||
private getFilename: Function;
|
||||
|
||||
constructor(opts) {
|
||||
this.client = new AliOss(opts.config);
|
||||
this.getFilename = opts.filename || getFilename;
|
||||
}
|
||||
|
||||
_handleFile(req, file, cb) {
|
||||
if (!this.client) {
|
||||
console.error('oss client undefined');
|
||||
return cb({ message: 'oss client undefined' });
|
||||
}
|
||||
this.getFilename(req, file, (err, filename) => {
|
||||
if (err) return cb(err)
|
||||
this.client.putStream(filename, file.stream).then(
|
||||
result => cb(null, {
|
||||
filename: result.name,
|
||||
url: result.url
|
||||
})
|
||||
).catch(cb);
|
||||
export default {
|
||||
make(storage) {
|
||||
const createAliOssStorage = require('multer-aliyun-oss');
|
||||
return new createAliOssStorage({
|
||||
config: storage.options,
|
||||
filename: cloudFilenameGetter(storage)
|
||||
});
|
||||
}
|
||||
|
||||
_removeFile(req, file, cb) {
|
||||
if (!this.client) {
|
||||
console.error('oss client undefined');
|
||||
return cb({ message: 'oss client undefined' });
|
||||
},
|
||||
defaults() {
|
||||
return {
|
||||
title: '阿里云对象存储',
|
||||
type: STORAGE_TYPE_ALI_OSS,
|
||||
name: 'ali-oss-1',
|
||||
baseUrl: process.env.ALI_OSS_STORAGE_BASE_URL,
|
||||
options: {
|
||||
region: process.env.ALI_OSS_REGION,
|
||||
accessKeyId: process.env.ALI_OSS_ACCESS_KEY_ID,
|
||||
accessKeySecret: process.env.ALI_OSS_ACCESS_KEY_SECRET,
|
||||
bucket: process.env.ALI_OSS_BUCKET,
|
||||
}
|
||||
}
|
||||
this.client.delete(file.filename).then(
|
||||
result => cb(null, result)
|
||||
).catch(cb);
|
||||
}
|
||||
}
|
||||
|
||||
export default (storage) => new AliOssStorage({ config: storage.options });
|
||||
|
@ -1,11 +1,27 @@
|
||||
import local from './local';
|
||||
import oss from './ali-oss';
|
||||
import { STORAGE_TYPE_LOCAL, STORAGE_TYPE_ALI_OSS } from '../constants';
|
||||
import s3 from './s3';
|
||||
|
||||
import {
|
||||
STORAGE_TYPE_LOCAL,
|
||||
STORAGE_TYPE_ALI_OSS,
|
||||
STORAGE_TYPE_S3
|
||||
} from '../constants';
|
||||
|
||||
export interface IStorage {
|
||||
filenameKey?: string;
|
||||
middleware?: Function;
|
||||
getFileData?: Function;
|
||||
make: Function;
|
||||
defaults: Function;
|
||||
}
|
||||
|
||||
const map = new Map<string, Function>();
|
||||
const map = new Map<string, IStorage>();
|
||||
map.set(STORAGE_TYPE_LOCAL, local);
|
||||
map.set(STORAGE_TYPE_ALI_OSS, oss);
|
||||
map.set(STORAGE_TYPE_S3, s3);
|
||||
|
||||
export default map;
|
||||
|
||||
export function getStorageConfig(key: string): IStorage {
|
||||
return map.get(key);
|
||||
};
|
||||
|
@ -45,7 +45,7 @@ function createLocalServerUpdateHook(app, storages) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getDocumentRoot(storage): string {
|
||||
function getDocumentRoot(storage): string {
|
||||
const { documentRoot = 'uploads' } = storage.options || {};
|
||||
// TODO(feature): 后面考虑以字符串模板的方式使用,可注入 req/action 相关变量,以便于区分文件夹
|
||||
return path.resolve(path.isAbsolute(documentRoot)
|
||||
@ -53,7 +53,7 @@ export function getDocumentRoot(storage): string {
|
||||
: path.join(process.cwd(), documentRoot));
|
||||
}
|
||||
|
||||
export async function middleware(app, options?) {
|
||||
async function middleware(app, options?) {
|
||||
const LOCALHOST = `http://localhost:${process.env.API_PORT}`;
|
||||
|
||||
const StorageModel = app.db.getModel('storages');
|
||||
@ -105,10 +105,23 @@ export async function middleware(app, options?) {
|
||||
});
|
||||
}
|
||||
|
||||
export default (storage) => multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
const destPath = path.join(getDocumentRoot(storage), storage.path);
|
||||
mkdirp(destPath, (err: Error | null) => cb(err, destPath));
|
||||
export default {
|
||||
middleware,
|
||||
make(storage) {
|
||||
return multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
const destPath = path.join(getDocumentRoot(storage), storage.path);
|
||||
mkdirp(destPath, (err: Error | null) => cb(err, destPath));
|
||||
},
|
||||
filename: getFilename
|
||||
});
|
||||
},
|
||||
filename: getFilename
|
||||
});
|
||||
defaults() {
|
||||
return {
|
||||
title: '本地存储',
|
||||
type: STORAGE_TYPE_LOCAL,
|
||||
name: `local`,
|
||||
baseUrl: process.env.LOCAL_STORAGE_BASE_URL || `http://localhost:${process.env.API_PORT}/uploads`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
53
packages/plugin-file-manager/src/storages/s3.ts
Normal file
53
packages/plugin-file-manager/src/storages/s3.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { STORAGE_TYPE_S3 } from '../constants';
|
||||
import { cloudFilenameGetter } from '../utils';
|
||||
|
||||
export default {
|
||||
filenameKey: 'key',
|
||||
make(storage) {
|
||||
const S3Client = require('aws-sdk/clients/s3');
|
||||
const multerS3 = require('multer-s3');
|
||||
const {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
bucket,
|
||||
acl = 'public-read',
|
||||
...options
|
||||
} = storage.options;
|
||||
const s3 = new S3Client({
|
||||
...options,
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey
|
||||
}
|
||||
});
|
||||
|
||||
return multerS3({
|
||||
s3,
|
||||
bucket,
|
||||
acl,
|
||||
contentType(req, file, cb) {
|
||||
if (file.mimetype) {
|
||||
cb(null, file.mimetype);
|
||||
return;
|
||||
}
|
||||
|
||||
multerS3.AUTO_CONTENT_TYPE(req, file, cb);
|
||||
},
|
||||
key: cloudFilenameGetter(storage)
|
||||
});
|
||||
},
|
||||
defaults() {
|
||||
return {
|
||||
title: 'AWS S3',
|
||||
name: 'aws-s3',
|
||||
type: STORAGE_TYPE_S3,
|
||||
baseUrl: process.env.AWS_S3_STORAGE_BASE_URL,
|
||||
options: {
|
||||
region: process.env.AWS_S3_REGION,
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||
bucket: process.env.AWS_S3_BUCKET,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,5 +4,14 @@ import path from 'path';
|
||||
export function getFilename(req, file, cb) {
|
||||
crypto.pseudoRandomBytes(16, function (err, raw) {
|
||||
cb(err, err ? undefined : `${raw.toString('hex')}${path.extname(file.originalname)}`)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export const cloudFilenameGetter = storage => (req, file, cb) => {
|
||||
getFilename(req, file, (err, filename) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
cb(null, `${storage.path ? `${storage.path}/` : ''}${filename}`);
|
||||
});
|
||||
}
|
||||
|
119
yarn.lock
119
yarn.lock
@ -4884,10 +4884,10 @@ ajv@^8.0.1:
|
||||
require-from-string "^2.0.2"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ali-oss@^6.12.0:
|
||||
ali-oss@^6.8.0:
|
||||
version "6.16.0"
|
||||
resolved "https://registry.nlark.com/ali-oss/download/ali-oss-6.16.0.tgz?cache=0&sync_timestamp=1626077110646&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fali-oss%2Fdownload%2Fali-oss-6.16.0.tgz#3b7fbe10f13fbd535478fc31c7d05aaf4280269b"
|
||||
integrity sha1-O3++EPE/vVNUePwxx9Bar0KAJps=
|
||||
resolved "https://registry.yarnpkg.com/ali-oss/-/ali-oss-6.16.0.tgz#3b7fbe10f13fbd535478fc31c7d05aaf4280269b"
|
||||
integrity sha512-tK/+yEKtBBD+kMoHABxg6lCgC+Ad9HNjCln7qdL6LRYbUm+FFTKJubC4hT2FIooMBDb9tnI7My4MVreKnbJQRg==
|
||||
dependencies:
|
||||
address "^1.0.0"
|
||||
agentkeepalive "^3.4.1"
|
||||
@ -5481,6 +5481,21 @@ autoprefixer@^9.6.1:
|
||||
postcss "^7.0.32"
|
||||
postcss-value-parser "^4.1.0"
|
||||
|
||||
aws-sdk@^2.2.32:
|
||||
version "2.1042.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1042.0.tgz#c3385bf6cbb8f97c2cde427c0ab3d9720fa4b82a"
|
||||
integrity sha512-JWjs6+Zhuo990WYH1iQR1njGOvoCFzaf2azX/zh3JdL7QNwzdqczoODMj0wb22831/7EoPDGaXHqp7aQwDsxwA==
|
||||
dependencies:
|
||||
buffer "4.9.2"
|
||||
events "1.1.1"
|
||||
ieee754 "1.1.13"
|
||||
jmespath "0.15.0"
|
||||
querystring "0.2.0"
|
||||
sax "1.2.1"
|
||||
url "0.10.3"
|
||||
uuid "3.3.2"
|
||||
xml2js "0.4.19"
|
||||
|
||||
aws-sign2@~0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.nlark.com/aws-sign2/download/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
||||
@ -6025,7 +6040,7 @@ buffer-xor@^1.0.3:
|
||||
resolved "https://registry.npm.taobao.org/buffer-xor/download/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
|
||||
integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
|
||||
|
||||
buffer@^4.3.0:
|
||||
buffer@4.9.2, buffer@^4.3.0:
|
||||
version "4.9.2"
|
||||
resolved "https://registry.npm.taobao.org/buffer/download/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
|
||||
integrity sha1-Iw6tNEACmIZEhBqwJEr4xEu+Pvg=
|
||||
@ -8821,6 +8836,11 @@ eventemitter3@^4.0.4:
|
||||
resolved "https://registry.nlark.com/eventemitter3/download/eventemitter3-4.0.7.tgz?cache=0&sync_timestamp=1622604485818&other_urls=https%3A%2F%2Fregistry.nlark.com%2Feventemitter3%2Fdownload%2Feventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||
integrity sha1-Lem2j2Uo1WRO9cWVJqG0oHMGFp8=
|
||||
|
||||
events@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
|
||||
integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
|
||||
|
||||
events@^3.0.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.npmmirror.com/events/download/events-3.3.0.tgz?cache=0&sync_timestamp=1636449286836&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fevents%2Fdownload%2Fevents-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
||||
@ -9160,6 +9180,11 @@ file-saver@^2.0.5:
|
||||
resolved "https://registry.npm.taobao.org/file-saver/download/file-saver-2.0.5.tgz?cache=0&sync_timestamp=1605790980036&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffile-saver%2Fdownload%2Ffile-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
|
||||
integrity sha1-1hz+LOBZ9BTYmendbUEH7iVnDDg=
|
||||
|
||||
file-type@^3.3.0:
|
||||
version "3.9.0"
|
||||
resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9"
|
||||
integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek=
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npm.taobao.org/file-uri-to-path/download/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||
@ -10275,6 +10300,11 @@ hsla-regex@^1.0.0:
|
||||
resolved "https://registry.npm.taobao.org/hsla-regex/download/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38"
|
||||
integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg=
|
||||
|
||||
html-comment-regex@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7"
|
||||
integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
|
||||
|
||||
html-dom-parser@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.nlark.com/html-dom-parser/download/html-dom-parser-1.0.2.tgz#bb5ff844f214657d899aa4fb7b0a9e7d15607e96"
|
||||
@ -10494,6 +10524,11 @@ identity-obj-proxy@3.0.0:
|
||||
dependencies:
|
||||
harmony-reflect "^1.4.6"
|
||||
|
||||
ieee754@1.1.13:
|
||||
version "1.1.13"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
|
||||
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
|
||||
|
||||
ieee754@^1.1.13, ieee754@^1.1.4:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npm.taobao.org/ieee754/download/ieee754-1.2.1.tgz?cache=0&sync_timestamp=1603838623318&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fieee754%2Fdownload%2Fieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
@ -12210,10 +12245,15 @@ jest@^26.6.3:
|
||||
import-local "^3.0.2"
|
||||
jest-cli "^26.6.3"
|
||||
|
||||
jmespath@0.15.0:
|
||||
version "0.15.0"
|
||||
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
|
||||
integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=
|
||||
|
||||
js-base64@^2.5.2:
|
||||
version "2.6.4"
|
||||
resolved "https://registry.npmmirror.com/js-base64/download/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
|
||||
integrity sha1-9OaGxd4eofhn28rT1G2WlCjfmMQ=
|
||||
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
|
||||
integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
|
||||
|
||||
js-cookie@^2.2.1:
|
||||
version "2.2.1"
|
||||
@ -12538,8 +12578,8 @@ kleur@^3.0.3:
|
||||
|
||||
ko-sleep@^1.0.3:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.npmmirror.com/ko-sleep/download/ko-sleep-1.1.4.tgz?cache=0&sync_timestamp=1633002399948&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fko-sleep%2Fdownload%2Fko-sleep-1.1.4.tgz#56462fba835e07bb8c26cfa083f9893a3fde5469"
|
||||
integrity sha1-VkYvuoNeB7uMJs+gg/mJOj/eVGk=
|
||||
resolved "https://registry.yarnpkg.com/ko-sleep/-/ko-sleep-1.1.4.tgz#56462fba835e07bb8c26cfa083f9893a3fde5469"
|
||||
integrity sha512-s05WGpvvzyTuRlRE8fM7ru2Z3O+InbJuBcckTWKg2W+2c1k6SnFa3IfiSSt0/peFrlYAXgNoxuJWWVNmWh+K/A==
|
||||
dependencies:
|
||||
ms "*"
|
||||
|
||||
@ -13778,6 +13818,22 @@ ms@2.1.2:
|
||||
resolved "https://registry.npmmirror.com/ms/download/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk=
|
||||
|
||||
multer-aliyun-oss@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/multer-aliyun-oss/-/multer-aliyun-oss-1.1.1.tgz#7d6c22989b8755205edbc4bcc233e08e5d13e2e1"
|
||||
integrity sha512-BcXibwKeHs2amI10ciIAGYWUQUZAX3sFJ3LIZJJsD0AN1xpF+ko2ruZQe01i47czL5sJaIxtRvRpYsVRTQmBaQ==
|
||||
dependencies:
|
||||
ali-oss "^6.8.0"
|
||||
|
||||
multer-s3@^2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/multer-s3/-/multer-s3-2.10.0.tgz#95c5a51ad0d165bcabdfd54572ded76a25b54754"
|
||||
integrity sha512-RZsiqG19C9gE82lB7v8duJ+TMIf70fWYHlIwuNcsanOH1ePBoPXZvboEQxEow9jUkk7WQsuyVA2TgriOuDrVrw==
|
||||
dependencies:
|
||||
file-type "^3.3.0"
|
||||
html-comment-regex "^1.1.2"
|
||||
run-parallel "^1.1.6"
|
||||
|
||||
multer@^1.4.2:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.nlark.com/multer/download/multer-1.4.3.tgz?cache=0&sync_timestamp=1628499005677&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fmulter%2Fdownload%2Fmulter-1.4.3.tgz#4db352d6992e028ac0eacf7be45c6efd0264297b"
|
||||
@ -13828,8 +13884,8 @@ mysql2@^2.1.0:
|
||||
|
||||
mz-modules@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npm.taobao.org/mz-modules/download/mz-modules-2.1.0.tgz#7f529877afd0d42f409a7463b96986d61cfbcf96"
|
||||
integrity sha1-f1KYd6/Q1C9AmnRjuWmG1hz7z5Y=
|
||||
resolved "https://registry.yarnpkg.com/mz-modules/-/mz-modules-2.1.0.tgz#7f529877afd0d42f409a7463b96986d61cfbcf96"
|
||||
integrity sha512-sjk8lcRW3vrVYnZ+W+67L/2rL+jbO5K/N6PFGIcLWTiYytNr22Ah9FDXFs+AQntTM1boZcoHi5qS+CV1seuPog==
|
||||
dependencies:
|
||||
glob "^7.1.2"
|
||||
ko-sleep "^1.0.3"
|
||||
@ -18174,7 +18230,7 @@ run-async@^2.2.0:
|
||||
resolved "https://registry.npm.taobao.org/run-async/download/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
|
||||
integrity sha1-hEDsz5nqPnC9QJ1JqriOEMGJpFU=
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
run-parallel@^1.1.6, run-parallel@^1.1.9:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npm.taobao.org/run-parallel/download/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
||||
integrity sha1-ZtE2jae9+SHrnZW9GpIp5/IaQ+4=
|
||||
@ -18247,6 +18303,11 @@ sane@^4.0.3:
|
||||
minimist "^1.1.1"
|
||||
walker "~1.0.5"
|
||||
|
||||
sax@1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
|
||||
integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
|
||||
|
||||
sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.npm.taobao.org/sax/download/sax-1.2.4.tgz?cache=0&sync_timestamp=1608181219722&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsax%2Fdownload%2Fsax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
@ -18934,8 +18995,8 @@ stream-each@^1.1.0:
|
||||
|
||||
stream-http@2.8.2:
|
||||
version "2.8.2"
|
||||
resolved "https://registry.npm.taobao.org/stream-http/download/stream-http-2.8.2.tgz#4126e8c6b107004465918aa2fc35549e77402c87"
|
||||
integrity sha1-QSboxrEHAERlkYqi/DVUnndALIc=
|
||||
resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.2.tgz#4126e8c6b107004465918aa2fc35549e77402c87"
|
||||
integrity sha512-QllfrBhqF1DPcz46WxKTs6Mz1Bpc+8Qm6vbqOpVav5odAXwbyzwnEczoWqtxrsmlO+cJqtPrp/8gWKWjaKLLlA==
|
||||
dependencies:
|
||||
builtin-status-codes "^3.0.0"
|
||||
inherits "^2.0.1"
|
||||
@ -20431,6 +20492,14 @@ url-parse-lax@^3.0.0:
|
||||
dependencies:
|
||||
prepend-http "^2.0.0"
|
||||
|
||||
url@0.10.3:
|
||||
version "0.10.3"
|
||||
resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"
|
||||
integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=
|
||||
dependencies:
|
||||
punycode "1.3.2"
|
||||
querystring "0.2.0"
|
||||
|
||||
url@^0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.npm.taobao.org/url/download/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
|
||||
@ -20440,9 +20509,9 @@ url@^0.11.0:
|
||||
querystring "0.2.0"
|
||||
|
||||
urllib@^2.33.1:
|
||||
version "2.37.4"
|
||||
resolved "https://registry.nlark.com/urllib/download/urllib-2.37.4.tgz#004d4d0c2567e3e5448fe7a580801510ec449362"
|
||||
integrity sha1-AE1NDCVn4+VEj+elgIAVEOxEk2I=
|
||||
version "2.38.0"
|
||||
resolved "https://registry.yarnpkg.com/urllib/-/urllib-2.38.0.tgz#5c0088f42091ef1cef07bb2547677487170414f5"
|
||||
integrity sha512-8nim/hlS5GXtWe2BJ6usPimKx5VE3nenXgcG26ip5Ru+MKPddINH8uLpZ948n6ADhlus6A0AYj8xTYNmGQi8yA==
|
||||
dependencies:
|
||||
any-promise "^1.3.0"
|
||||
content-type "^1.0.2"
|
||||
@ -20545,6 +20614,11 @@ utility@^1.16.1, utility@^1.8.0:
|
||||
mz "^2.7.0"
|
||||
unescape "^1.0.1"
|
||||
|
||||
uuid@3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
|
||||
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
|
||||
|
||||
uuid@^3.0.1, uuid@^3.2.1, uuid@^3.3.2:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.npmmirror.com/uuid/download/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
@ -21121,6 +21195,14 @@ xml-name-validator@^3.0.0:
|
||||
resolved "https://registry.nlark.com/xml-name-validator/download/xml-name-validator-3.0.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fxml-name-validator%2Fdownload%2Fxml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
|
||||
integrity sha1-auc+Bt5NjG5H+fsYH3jWSK1FfGo=
|
||||
|
||||
xml2js@0.4.19:
|
||||
version "0.4.19"
|
||||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
|
||||
integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==
|
||||
dependencies:
|
||||
sax ">=0.6.0"
|
||||
xmlbuilder "~9.0.1"
|
||||
|
||||
xml2js@^0.4.16:
|
||||
version "0.4.23"
|
||||
resolved "https://registry.npm.taobao.org/xml2js/download/xml2js-0.4.23.tgz?cache=0&sync_timestamp=1576776179444&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fxml2js%2Fdownload%2Fxml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
|
||||
@ -21134,6 +21216,11 @@ xmlbuilder@~11.0.0:
|
||||
resolved "https://registry.npm.taobao.org/xmlbuilder/download/xmlbuilder-11.0.1.tgz?cache=0&sync_timestamp=1586386503877&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fxmlbuilder%2Fdownload%2Fxmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
|
||||
integrity sha1-vpuuHIoEbnazESdyY0fQrXACvrM=
|
||||
|
||||
xmlbuilder@~9.0.1:
|
||||
version "9.0.7"
|
||||
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
|
||||
integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
|
||||
|
||||
xmlchars@^2.1.1, xmlchars@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npm.taobao.org/xmlchars/download/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
||||
|
Loading…
Reference in New Issue
Block a user