diff --git a/.env.example b/.env.example index c2b214327..665d5c64e 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/packages/api/.env.example b/packages/api/.env.example index 1efc2b2ab..b7d66cfb2 100644 --- a/packages/api/.env.example +++ b/packages/api/.env.example @@ -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= diff --git a/packages/create-nocobase-app/templates/AppGenerator/.env.example b/packages/create-nocobase-app/templates/AppGenerator/.env.example index a39aab179..74f704877 100644 --- a/packages/create-nocobase-app/templates/AppGenerator/.env.example +++ b/packages/create-nocobase-app/templates/AppGenerator/.env.example @@ -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= diff --git a/packages/create-nocobase-app/templates/AppGenerator/.env.tpl b/packages/create-nocobase-app/templates/AppGenerator/.env.tpl index cca017953..c24353434 100644 --- a/packages/create-nocobase-app/templates/AppGenerator/.env.tpl +++ b/packages/create-nocobase-app/templates/AppGenerator/.env.tpl @@ -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= diff --git a/packages/plugin-file-manager/package.json b/packages/plugin-file-manager/package.json index a565e2b2c..9b7757733 100644 --- a/packages/plugin-file-manager/package.json +++ b/packages/plugin-file-manager/package.json @@ -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" diff --git a/packages/plugin-file-manager/src/__tests__/index.ts b/packages/plugin-file-manager/src/__tests__/index.ts index eba9b05e5..069ce4c10 100644 --- a/packages/plugin-file-manager/src/__tests__/index.ts +++ b/packages/plugin-file-manager/src/__tests__/index.ts @@ -11,9 +11,11 @@ export async function getApp(options = {}): Promise { 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 { 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('')); } diff --git a/packages/plugin-file-manager/src/__tests__/storages/ali-oss.test.ts b/packages/plugin-file-manager/src/__tests__/storages/ali-oss.test.ts new file mode 100644 index 000000000..aa3cd6dba --- /dev/null +++ b/packages/plugin-file-manager/src/__tests__/storages/ali-oss.test.ts @@ -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'); + }); + }); +}); diff --git a/packages/plugin-file-manager/src/__tests__/storages/s3.test.ts b/packages/plugin-file-manager/src/__tests__/storages/s3.test.ts new file mode 100644 index 000000000..598e9a5c6 --- /dev/null +++ b/packages/plugin-file-manager/src/__tests__/storages/s3.test.ts @@ -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'); + }); + }); +}); diff --git a/packages/plugin-file-manager/src/actions/upload.ts b/packages/plugin-file-manager/src/actions/upload.ts index 24e7341d8..e3cf92ef3 100644 --- a/packages/plugin-file-manager/src/actions/upload.ts +++ b/packages/plugin-file-manager/src/actions/upload.ts @@ -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); diff --git a/packages/plugin-file-manager/src/constants.ts b/packages/plugin-file-manager/src/constants.ts index bced89736..15958088b 100644 --- a/packages/plugin-file-manager/src/constants.ts +++ b/packages/plugin-file-manager/src/constants.ts @@ -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'; diff --git a/packages/plugin-file-manager/src/index.ts b/packages/plugin-file-manager/src/index.ts index e69de29bb..c94f80f84 100644 --- a/packages/plugin-file-manager/src/index.ts +++ b/packages/plugin-file-manager/src/index.ts @@ -0,0 +1 @@ +export * from './constants'; diff --git a/packages/plugin-file-manager/src/server.ts b/packages/plugin-file-manager/src/server.ts index 4691a0af7..503b504c6 100644 --- a/packages/plugin-file-manager/src/server.ts +++ b/packages/plugin-file-manager/src/server.ts @@ -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; diff --git a/packages/plugin-file-manager/src/storages/ali-oss.ts b/packages/plugin-file-manager/src/storages/ali-oss.ts index 7c3d19f37..95fbf3eea 100644 --- a/packages/plugin-file-manager/src/storages/ali-oss.ts +++ b/packages/plugin-file-manager/src/storages/ali-oss.ts @@ -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 }); diff --git a/packages/plugin-file-manager/src/storages/index.ts b/packages/plugin-file-manager/src/storages/index.ts index fb02afe01..e6907ebe6 100644 --- a/packages/plugin-file-manager/src/storages/index.ts +++ b/packages/plugin-file-manager/src/storages/index.ts @@ -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(); +const map = new Map(); 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); +}; diff --git a/packages/plugin-file-manager/src/storages/local.ts b/packages/plugin-file-manager/src/storages/local.ts index a277f33c2..bf836db20 100644 --- a/packages/plugin-file-manager/src/storages/local.ts +++ b/packages/plugin-file-manager/src/storages/local.ts @@ -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` + }; + } +}; diff --git a/packages/plugin-file-manager/src/storages/s3.ts b/packages/plugin-file-manager/src/storages/s3.ts new file mode 100644 index 000000000..d6d827193 --- /dev/null +++ b/packages/plugin-file-manager/src/storages/s3.ts @@ -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, + } + } + } +} diff --git a/packages/plugin-file-manager/src/utils.ts b/packages/plugin-file-manager/src/utils.ts index c06881fa6..a9b06f319 100644 --- a/packages/plugin-file-manager/src/utils.ts +++ b/packages/plugin-file-manager/src/utils.ts @@ -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}`); + }); } diff --git a/yarn.lock b/yarn.lock index 013232add..cd363ba24 100644 --- a/yarn.lock +++ b/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"