diff --git a/.eslintignore b/.eslintignore index fb652e36..3966754b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,6 +5,7 @@ lib **/.next/** **/dist/** +**/static/** **/build/** **/public/** **/diagram.js diff --git a/config/dev.yaml b/config/dev.yaml index 8d598341..b38705eb 100644 --- a/config/dev.yaml +++ b/config/dev.yaml @@ -42,12 +42,26 @@ db: # oss 文件存储服务 oss: + local: + enable: true + # 线上更改为服务端地址(如:https://api.codingit.cn) + server: 'http://localhost:5002' + # 以下为各厂商 sdk 配置,不要修改字段,填入值即可 + tencent: + enable: false + config: + SecretId: '' + SecretKey: '' + Bucket: '' + Region: '' aliyun: - accessKeyId: '' - accessKeySecret: '' - bucket: '' - https: true - region: '' + enable: false + config: + accessKeyId: '' + accessKeySecret: '' + bucket: '' + https: true + region: '' # jwt 配置 jwt: diff --git a/packages/client/package.json b/packages/client/package.json index db72ca32..eaef52c9 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -91,6 +91,7 @@ "requestidlecallback-polyfill": "^1.0.2", "resize-observer-polyfill": "^1.5.1", "scroll-into-view-if-needed": "^2.2.29", + "spark-md5": "^3.0.2", "timeago.js": "^4.0.2", "tippy.js": "^6.3.7", "toggle-selection": "^1.0.6", diff --git a/packages/client/src/services/file.ts b/packages/client/src/services/file.ts index c43fbf27..14faffb5 100644 --- a/packages/client/src/services/file.ts +++ b/packages/client/src/services/file.ts @@ -1,30 +1,139 @@ +import { FILE_CHUNK_SIZE, FileApiDefinition } from '@think/domains'; +import SparkMD5 from 'spark-md5'; + import { HttpClient } from './http-client'; -const ONE_MB = 1 * 1024 * 1024; - -export const readFileAsDataURL = (file): Promise => { - if (file.size > ONE_MB) { - return Promise.reject(new Error('文件过大,请实现文件上传到存储服务!')); - } - +const splitBigFile = (file: File): Promise<{ chunks: File[]; md5: string }> => { return new Promise((resolve) => { - const reader = new FileReader(); - reader.addEventListener('load', (e) => resolve(e.target.result), { once: true }); - reader.readAsDataURL(file); + const chunks = []; + const len = Math.ceil(file.size / FILE_CHUNK_SIZE); + const sparkWorker = new Worker(new URL('./spark-md5.js', import.meta.url)); + sparkWorker.onmessage = (evt) => { + resolve({ md5: evt.data.md5, chunks }); + }; + + for (let i = 0; i < len; i++) { + const start = i * FILE_CHUNK_SIZE; + const end = Math.min(start + FILE_CHUNK_SIZE, file.size); + const chunk = file.slice(start, end); + chunks.push(chunk); + } + + sparkWorker.postMessage({ chunks }); }); }; -export const uploadFile = async (file: Blob): Promise => { - if (!process.env.ENABLE_ALIYUN_OSS) { - return readFileAsDataURL(file); - } +const uploadFileToServer = (arg: { + filename: string; + file: File; + md5: string; + isChunk?: boolean; + chunkIndex?: number; + onUploadProgress?: (progress: number) => void; +}) => { + const { filename, file, md5, isChunk, chunkIndex, onUploadProgress } = arg; + const api = isChunk ? 'uploadChunk' : 'upload'; const formData = new FormData(); formData.append('file', file); - return HttpClient.post('/file/upload', formData, { + return HttpClient.request({ + method: FileApiDefinition[api].method, + url: FileApiDefinition[api].client(), + data: formData, headers: { 'Content-Type': 'multipart/form-data', }, + params: { + filename, + md5, + chunkIndex, + }, + onUploadProgress: (progress) => { + const percent = progress.loaded / progress.total; + onUploadProgress && onUploadProgress(percent); + }, }); }; + +export const uploadFile = async ( + file: File, + onUploadProgress?: (progress: number) => void, + onTooLarge?: () => void +) => { + const wraponUploadProgress = (percent) => { + return onUploadProgress && onUploadProgress(Math.ceil(percent * 100)); + }; + + const filename = file.name; + + if (file.size > FILE_CHUNK_SIZE * 5) { + onTooLarge && onTooLarge(); + } + + if (file.size <= FILE_CHUNK_SIZE) { + const spark = new SparkMD5.ArrayBuffer(); + spark.append(file); + const md5 = spark.end(); + const url = await uploadFileToServer({ filename, file, md5, onUploadProgress: wraponUploadProgress }); + return url; + } else { + const { chunks, md5 } = await splitBigFile(file); + const unitPercent = 1 / chunks.length; + const progressMap = {}; + + /** + * 先上传一块分块,如果文件已上传,即无需上传后续分块 + */ + let url = await uploadFileToServer({ + filename, + file: chunks[0], + chunkIndex: 1, + md5, + isChunk: true, + onUploadProgress: (progress) => { + progressMap[1] = progress * unitPercent; + wraponUploadProgress( + Object.keys(progressMap).reduce((a, c) => { + return (a += progressMap[c]); + }, 0) + ); + }, + }); + + if (!url) { + await Promise.all( + chunks.slice(1).map((chunk, index) => { + const currentIndex = 1 + index + 1; + return uploadFileToServer({ + filename, + file: chunk, + chunkIndex: currentIndex, + md5, + isChunk: true, + onUploadProgress: (progress) => { + progressMap[currentIndex] = progress * unitPercent; + wraponUploadProgress( + Object.keys(progressMap).reduce((a, c) => { + return (a += progressMap[c]); + }, 0) + ); + }, + }); + }) + ); + url = await HttpClient.request({ + method: FileApiDefinition.mergeChunk.method, + url: FileApiDefinition.mergeChunk.client(), + params: { + filename, + md5, + }, + }); + } else { + wraponUploadProgress(1); + } + + return url; + } +}; diff --git a/packages/client/src/services/http-client.ts b/packages/client/src/services/http-client.ts index ca1cff96..c3e93c38 100644 --- a/packages/client/src/services/http-client.ts +++ b/packages/client/src/services/http-client.ts @@ -10,7 +10,7 @@ interface AxiosInstance extends Axios { export const HttpClient = axios.create({ baseURL: process.env.SERVER_API_URL, - timeout: 10 * 1000, + timeout: 10 * 60 * 1000, withCredentials: true, }) as AxiosInstance; diff --git a/packages/client/src/services/spark-md5.js b/packages/client/src/services/spark-md5.js new file mode 100644 index 00000000..f93fdddd --- /dev/null +++ b/packages/client/src/services/spark-md5.js @@ -0,0 +1,31 @@ +import SparkMD5 from 'spark-md5'; + +addEventListener('message', (e) => { + const chunks = e.data.chunks || []; + + if (!chunks.length) return; + + const spark = new SparkMD5.ArrayBuffer(); + const reader = new FileReader(); + let index = 0; + + const load = () => { + const chunk = chunks[index]; + reader.readAsArrayBuffer(chunk); + }; + + reader.onload = (e) => { + spark.append(e.target.result); + + if (index === chunks.length - 1) { + const md5 = spark.end(); + postMessage({ md5 }); + self.close(); + } else { + index++; + load(); + } + }; + + load(); +}); diff --git a/packages/client/src/tiptap/core/wrappers/attachment/index.tsx b/packages/client/src/tiptap/core/wrappers/attachment/index.tsx index 67910e32..9346ef9f 100644 --- a/packages/client/src/tiptap/core/wrappers/attachment/index.tsx +++ b/packages/client/src/tiptap/core/wrappers/attachment/index.tsx @@ -1,10 +1,11 @@ import { IconClose, IconDownload, IconPlayCircle } from '@douyinfe/semi-icons'; -import { Button, Collapsible, Space, Spin, Typography } from '@douyinfe/semi-ui'; +import { Button, Collapsible, Progress, Space, Spin, Toast, Typography } from '@douyinfe/semi-ui'; +import { FILE_CHUNK_SIZE } from '@think/domains'; import { NodeViewWrapper } from '@tiptap/react'; import cls from 'classnames'; import { Tooltip } from 'components/tooltip'; import { useToggle } from 'hooks/use-toggle'; -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { uploadFile } from 'services/file'; import { download, extractFileExtension, extractFilename, normalizeFileSize } from 'tiptap/prose-utils'; @@ -20,6 +21,8 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => { const { hasTrigger, fileName, fileSize, fileExt, fileType, url, error } = node.attrs; const [loading, toggleLoading] = useToggle(false); const [visible, toggleVisible] = useToggle(false); + const [showProgress, toggleShowProgress] = useToggle(false); + const [uploadProgress, setUploadProgress] = useState(0); const selectFile = useCallback(() => { if (!isEditable || url) return; @@ -29,6 +32,7 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => { const handleFile = useCallback( async (e) => { const file = e.target.files && e.target.files[0]; + if (!file) return; const fileInfo = { fileName: extractFilename(file.name), fileSize: file.size, @@ -36,16 +40,28 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => { fileExt: extractFileExtension(file.name), }; toggleLoading(true); + + if (file.size > FILE_CHUNK_SIZE) { + toggleShowProgress(true); + } + try { - const url = await uploadFile(file); + const url = await uploadFile(file, setUploadProgress, () => { + Toast.info('文件较大,文件将在后台进行上传处理,您可继续其他操作'); + }); updateAttributes({ ...fileInfo, url }); toggleLoading(false); + setUploadProgress(0); + toggleShowProgress(false); } catch (error) { updateAttributes({ error: '文件上传失败:' + (error && error.message) || '未知错误' }); toggleLoading(false); + setUploadProgress(0); + toggleShowProgress(false); + $upload.current.value = ''; } }, - [toggleLoading, updateAttributes] + [toggleLoading, toggleShowProgress, updateAttributes] ); useEffect(() => { @@ -61,7 +77,21 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
- {loading ? '正在上传中' : '请选择文件'} + {loading ? ( + showProgress ? ( + + ) : ( + '正在上传中' + ) + ) : ( + '请选择文件' + )} diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 0cc3693c..025a791a 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -31,6 +31,6 @@ "thirtypart/*": ["thirtypart/*"] } }, - "include": ["next-env.d.ts", "global.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["next-env.d.ts", "global.d.ts", "**/*.ts", "**/*.tsx", "src/services/spark-md5.js"], "exclude": ["node_modules", "next.config.js"] } diff --git a/packages/domains/lib/api/file.d.ts b/packages/domains/lib/api/file.d.ts index 57faf957..ca7d397a 100644 --- a/packages/domains/lib/api/file.d.ts +++ b/packages/domains/lib/api/file.d.ts @@ -7,4 +7,21 @@ export declare const FileApiDefinition: { server: "upload"; client: () => string; }; + /** + * 上传分块文件 + */ + uploadChunk: { + method: "post"; + server: "upload/chunk"; + client: () => string; + }; + /** + * 上传分块文件 + */ + mergeChunk: { + method: "post"; + server: "merge/chunk"; + client: () => string; + }; }; +export declare const FILE_CHUNK_SIZE: number; diff --git a/packages/domains/lib/api/file.js b/packages/domains/lib/api/file.js index 84c16576..e8029423 100644 --- a/packages/domains/lib/api/file.js +++ b/packages/domains/lib/api/file.js @@ -1,6 +1,6 @@ "use strict"; exports.__esModule = true; -exports.FileApiDefinition = void 0; +exports.FILE_CHUNK_SIZE = exports.FileApiDefinition = void 0; exports.FileApiDefinition = { /** * 上传文件 @@ -9,5 +9,22 @@ exports.FileApiDefinition = { method: 'post', server: 'upload', client: function () { return '/file/upload'; } + }, + /** + * 上传分块文件 + */ + uploadChunk: { + method: 'post', + server: 'upload/chunk', + client: function () { return '/file/upload/chunk'; } + }, + /** + * 上传分块文件 + */ + mergeChunk: { + method: 'post', + server: 'merge/chunk', + client: function () { return '/file/merge/chunk'; } } }; +exports.FILE_CHUNK_SIZE = 2 * 1024 * 1024; diff --git a/packages/domains/src/api/file.ts b/packages/domains/src/api/file.ts index c89e0314..b724d511 100644 --- a/packages/domains/src/api/file.ts +++ b/packages/domains/src/api/file.ts @@ -7,4 +7,24 @@ export const FileApiDefinition = { server: 'upload' as const, client: () => '/file/upload', }, + + /** + * 上传分块文件 + */ + uploadChunk: { + method: 'post' as const, + server: 'upload/chunk' as const, + client: () => '/file/upload/chunk', + }, + + /** + * 上传分块文件 + */ + mergeChunk: { + method: 'post' as const, + server: 'merge/chunk' as const, + client: () => '/file/merge/chunk', + }, }; + +export const FILE_CHUNK_SIZE = 2 * 1024 * 1024; diff --git a/packages/server/.gitignore b/packages/server/.gitignore index 22f55adc..c7f46507 100644 --- a/packages/server/.gitignore +++ b/packages/server/.gitignore @@ -32,4 +32,7 @@ lerna-debug.log* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json + +# 静态文件 +/static diff --git a/packages/server/package.json b/packages/server/package.json index 484e2fcf..a3e7f873 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -31,12 +31,14 @@ "@think/config": "workspace:^1.0.0", "@think/constants": "workspace:^1.0.0", "@think/domains": "workspace:^1.0.0", + "@types/multer": "^1.4.7", "ali-oss": "^6.16.0", "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", "class-validator": "^0.13.2", "compression": "^1.7.4", "cookie-parser": "^1.4.6", + "cos-nodejs-sdk-v5": "^2.11.9", "date-fns": "^2.28.0", "express": "^4.17.2", "express-rate-limit": "^6.2.0", diff --git a/packages/server/src/controllers/file.controller.ts b/packages/server/src/controllers/file.controller.ts index d9bab8e3..692f4dae 100644 --- a/packages/server/src/controllers/file.controller.ts +++ b/packages/server/src/controllers/file.controller.ts @@ -1,27 +1,55 @@ import { JwtGuard } from '@guard/jwt.guard'; -import { Controller, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; +import { FileQuery } from '@helpers/file.helper/oss.client'; +import { Controller, Post, Query, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { FileService } from '@services/file.service'; -import { FileApiDefinition } from '@think/domains'; +import { FILE_CHUNK_SIZE, FileApiDefinition } from '@think/domains'; @Controller('file') export class FileController { constructor(private readonly fileService: FileService) {} /** - * 上传文件 + * 上传小文件 * @param file */ @Post(FileApiDefinition.upload.server) @UseInterceptors( FileInterceptor('file', { limits: { - fieldSize: 50 * 1024 * 1024, + fieldSize: FILE_CHUNK_SIZE, }, }) ) @UseGuards(JwtGuard) - uploadFile(@UploadedFile() file) { - return this.fileService.uploadFile(file); + uploadFile(@UploadedFile() file: Express.Multer.File, @Query() query: FileQuery) { + return this.fileService.uploadFile(file, query); + } + + /** + * 上传分块文件 + * @param file + */ + @Post(FileApiDefinition.uploadChunk.server) + @UseInterceptors( + FileInterceptor('file', { + limits: { + fieldSize: FILE_CHUNK_SIZE, + }, + }) + ) + @UseGuards(JwtGuard) + uploadChunk(@UploadedFile() file: Express.Multer.File, @Query() query: FileQuery) { + return this.fileService.uploadChunk(file, query); + } + + /** + * 合并分块文件 + * @param file + */ + @Post(FileApiDefinition.mergeChunk.server) + @UseGuards(JwtGuard) + mergeChunk(@Query() query: FileQuery) { + return this.fileService.mergeChunk(query); } } diff --git a/packages/server/src/helpers/file.helper/aliyun.client.ts b/packages/server/src/helpers/file.helper/aliyun.client.ts new file mode 100644 index 00000000..d092e57f --- /dev/null +++ b/packages/server/src/helpers/file.helper/aliyun.client.ts @@ -0,0 +1,152 @@ +import { FILE_CHUNK_SIZE } from '@think/domains'; +import * as AliyunOSS from 'ali-oss'; +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; + +import { pipeWriteStream } from './local.client'; +import { BaseOssClient, FileQuery } from './oss.client'; + +export class AliyunOssClient extends BaseOssClient { + private client: AliyunOSS | null; + + /** + * 构建 ali-oss 客户端 + * @returns + */ + private ensureAliyunOssClient(): AliyunOSS { + if (this.client) { + return this.client; + } + + const config = this.configService.get('oss.aliyun.config'); + + try { + this.client = new AliyunOSS(config); + return this.client; + } catch (err) { + console.log('无法启动阿里云存储服务,请检查阿里云 OSS 配置是否正确', err.message); + } + } + + /** + * 获取上传文件名 + * @param md5 + * @param filename + * @returns + */ + private getInOssFileName(md5, filename) { + return `/think/${md5}/${filename}`; + } + + /** + * 检查文件是否已存储到 oss + * @param md5 + * @param filename + * @returns + */ + private async checkIfAlreadyInOss(md5, filename) { + this.ensureAliyunOssClient(); + const inOssFileName = this.getInOssFileName(md5, filename); + const ifExist = await this.client.head(inOssFileName).catch(() => false); + + if (ifExist) { + return ifExist.res.requestUrls[0]; + } + + return false; + } + + /** + * 获取文件临时存储路径 + * @param md5 + * @returns + */ + private getStoreDir(md5: string) { + const tmpdir = os.tmpdir(); + const dir = path.join(tmpdir, md5); + fs.ensureDirSync(dir); + return dir; + } + + /** + * 上传小文件 + * @param file + * @param query + * @returns + */ + async uploadFile(file: Express.Multer.File, query: FileQuery): Promise { + const client = this.ensureAliyunOssClient(); + const { filename, md5 } = query; + + const maybeOssURL = await this.checkIfAlreadyInOss(md5, filename); + if (maybeOssURL) { + return maybeOssURL; + } + + const inOssFileName = this.getInOssFileName(md5, filename); + const res = await client.put(inOssFileName, file.buffer); + return res.url; + } + + /** + * 将切片临时存储到服务器 + * FIXME: 阿里云的文档没看懂,故做成这种服务器中转的蠢模式 + * @param file + * @param query + * @returns + */ + async uploadChunk(file: Express.Multer.File, query: FileQuery): Promise { + const { md5, filename, chunkIndex } = query; + + if (!('chunkIndex' in query)) { + throw new Error('请指定 chunkIndex'); + } + + const maybeOssURL = await this.checkIfAlreadyInOss(md5, filename); + if (maybeOssURL) { + return maybeOssURL; + } + + const dir = this.getStoreDir(md5); + const chunksDir = path.join(dir, 'chunks'); + fs.ensureDirSync(chunksDir); + fs.writeFileSync(path.join(chunksDir, '' + chunkIndex), file.buffer); + } + + /** + * 合并切片后上传到阿里云 + * FIXME: 阿里云的文档没看懂,故做成这种服务器中转的蠢模式 + * @param query + * @returns + */ + async mergeChunk(query: FileQuery): Promise { + const { filename, md5 } = query; + + this.ensureAliyunOssClient(); + const inOssFileName = this.getInOssFileName(md5, filename); + + const dir = this.getStoreDir(md5); + const absoluteFilepath = path.join(dir, filename); + const chunksDir = path.join(dir, 'chunks'); + const chunks = fs.readdirSync(chunksDir); + chunks.sort((a, b) => Number(a) - Number(b)); + + await Promise.all( + chunks.map((chunk, index) => { + return pipeWriteStream( + path.join(chunksDir, chunk), + fs.createWriteStream(absoluteFilepath, { + start: index * FILE_CHUNK_SIZE, + end: (index + 1) * FILE_CHUNK_SIZE, + }) + ); + }) + ); + + fs.removeSync(chunksDir); + const ret = await this.client.multipartUpload(inOssFileName, absoluteFilepath); + fs.removeSync(absoluteFilepath); + return ret.res.requestUrls[0]; + } +} diff --git a/packages/server/src/helpers/file.helper/index.ts b/packages/server/src/helpers/file.helper/index.ts new file mode 100644 index 00000000..34c92620 --- /dev/null +++ b/packages/server/src/helpers/file.helper/index.ts @@ -0,0 +1,20 @@ +import { ConfigService } from '@nestjs/config'; + +import { AliyunOssClient } from './aliyun.client'; +import { LocalOssClient } from './local.client'; +import { OssClient } from './oss.client'; +import { TencentOssClient } from './tencent.client'; + +export { OssClient }; + +export const getOssClient = (configService: ConfigService): OssClient => { + if (configService.get('oss.tencent.enable')) { + return new TencentOssClient(configService); + } + + if (configService.get('oss.aliyun.enable')) { + return new AliyunOssClient(configService); + } + + return new LocalOssClient(configService); +}; diff --git a/packages/server/src/helpers/file.helper/local.client.ts b/packages/server/src/helpers/file.helper/local.client.ts new file mode 100644 index 00000000..7becc435 --- /dev/null +++ b/packages/server/src/helpers/file.helper/local.client.ts @@ -0,0 +1,129 @@ +import { FILE_CHUNK_SIZE } from '@think/domains'; +import * as fs from 'fs-extra'; +import * as path from 'path'; + +import { BaseOssClient, FileQuery } from './oss.client'; + +export const FILE_DEST = '/' + 'static'; +export const FILE_ROOT_PATH = path.join(__dirname, '../../../', FILE_DEST); + +export const pipeWriteStream = (filepath, writeStream): Promise => { + return new Promise((resolve) => { + const readStream = fs.createReadStream(filepath); + readStream.on('end', () => { + fs.unlinkSync(filepath); + resolve(); + }); + readStream.pipe(writeStream); + }); +}; + +export class LocalOssClient extends BaseOssClient { + /** + * 文件存储路径 + * @param md5 + * @returns + */ + protected getStoreDir(md5: string): { + relative: string; + absolute: string; + } { + const filepath = path.join(FILE_ROOT_PATH, md5); + fs.ensureDirSync(filepath); + return { relative: filepath.replace(FILE_ROOT_PATH, FILE_DEST), absolute: filepath }; + } + + /** + * 将文件存储的相对路径拼接为可访问 URL + * @param serverRoot + * @param relativeFilePath + * @returns + */ + protected serveFilePath(relativeFilePath: string) { + const serverRoot = this.configService.get('oss.local.server'); + + if (!serverRoot) { + throw new Error(`本地文件存储已启动,但未配置 oss.local.server,请在 config 完善!`); + } + + return new URL(relativeFilePath, serverRoot).href; + } + + /** + * 小文件上传 + * @param file + * @param query + * @returns + */ + async uploadFile(file: Express.Multer.File, query: FileQuery): Promise { + const { filename, md5 } = query; + const { absolute, relative } = this.getStoreDir(md5); + const absoluteFilepath = path.join(absolute, filename); + const relativeFilePath = path.join(relative, filename); + + if (!fs.existsSync(absoluteFilepath)) { + fs.writeFileSync(absoluteFilepath, file.buffer); + } + + return this.serveFilePath(relativeFilePath); + } + + /** + * 文件分块上传 + * @param file + * @param query + */ + async uploadChunk(file: Express.Multer.File, query: FileQuery): Promise { + const { filename, md5, chunkIndex } = query; + + if (!('chunkIndex' in query)) { + throw new Error('请指定 chunkIndex'); + } + + const { absolute, relative } = this.getStoreDir(md5); + const absoluteFilepath = path.join(absolute, filename); + + if (fs.existsSync(absoluteFilepath)) { + const relativeFilePath = path.join(relative, filename); + return this.serveFilePath(relativeFilePath); + } + + const chunksDir = path.join(absolute, 'chunks'); + fs.ensureDirSync(chunksDir); + fs.writeFileSync(path.join(chunksDir, '' + chunkIndex), file.buffer); + } + + /** + * 合并分块 + * @param query + * @returns + */ + async mergeChunk(query: FileQuery): Promise { + const { filename, md5 } = query; + const { absolute, relative } = this.getStoreDir(md5); + const absoluteFilepath = path.join(absolute, filename); + const relativeFilePath = path.join(relative, filename); + + if (!fs.existsSync(absoluteFilepath)) { + const chunksDir = path.join(absolute, 'chunks'); + const chunks = fs.readdirSync(chunksDir); + chunks.sort((a, b) => Number(a) - Number(b)); + + await Promise.all( + chunks.map((chunk, index) => { + return pipeWriteStream( + path.join(chunksDir, chunk), + fs.createWriteStream(absoluteFilepath, { + start: index * FILE_CHUNK_SIZE, + end: (index + 1) * FILE_CHUNK_SIZE, + }) + ); + }) + ); + + fs.removeSync(chunksDir); + } + + return this.serveFilePath(relativeFilePath); + } +} diff --git a/packages/server/src/helpers/file.helper/oss.client.ts b/packages/server/src/helpers/file.helper/oss.client.ts new file mode 100644 index 00000000..03d99ca9 --- /dev/null +++ b/packages/server/src/helpers/file.helper/oss.client.ts @@ -0,0 +1,36 @@ +import { ConfigService } from '@nestjs/config'; + +export type FileQuery = { + filename: string; + md5: string; + chunkIndex?: number; +}; + +export abstract class OssClient { + abstract uploadFile(file: Express.Multer.File, query: FileQuery): Promise; + abstract uploadChunk(file: Express.Multer.File, query: FileQuery): Promise; + abstract mergeChunk(query: FileQuery): Promise; +} + +export class BaseOssClient implements OssClient { + protected configService: ConfigService; + + constructor(configService) { + this.configService = configService; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + uploadFile(file: Express.Multer.File, query: FileQuery): Promise { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + uploadChunk(file: Express.Multer.File, query: FileQuery): Promise { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + mergeChunk(query: FileQuery): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/server/src/helpers/file.helper/tencent.client.ts b/packages/server/src/helpers/file.helper/tencent.client.ts new file mode 100644 index 00000000..c416dd44 --- /dev/null +++ b/packages/server/src/helpers/file.helper/tencent.client.ts @@ -0,0 +1,279 @@ +import * as TencentCos from 'cos-nodejs-sdk-v5'; + +import { BaseOssClient, FileQuery } from './oss.client'; + +export class TencentOssClient extends BaseOssClient { + private client: TencentCos | null; + private uploadIdMap: Map = new Map(); + private uploadChunkEtagMap: Map< + string, + { + PartNumber: number; + ETag: string; + }[] + > = new Map(); + + /** + * 构建客户端 + * @returns + */ + private ensureOssClient(): TencentCos { + if (this.client) { + return this.client; + } + + const config = this.configService.get('oss.tencent.config'); + + try { + this.client = new TencentCos(config); + return this.client; + } catch (err) { + console.log('无法启动腾讯云存储服务,请检查腾讯云 COS 配置是否正确', err.message); + } + } + + /** + * 获取上传文件名 + * @param md5 + * @param filename + * @returns + */ + private getInOssFileName(md5, filename) { + return `/think/${md5}/${filename}`; + } + + /** + * 检查文件是否已存储到 oss + * @param md5 + * @param filename + * @returns + */ + private async checkIfAlreadyInOss(inOssFileName): Promise { + const params = { + Bucket: this.configService.get('oss.tencent.config.Bucket'), + Region: this.configService.get('oss.tencent.config.Region'), + Key: inOssFileName, + }; + + return new Promise((resolve, reject) => { + this.ensureOssClient(); + this.client.headObject(params, (err) => { + if (err) { + resolve(false); + } else { + this.client.getObjectUrl(params, (err, data) => { + if (err) { + reject(err); + } else { + resolve(data.Url); + } + }); + } + }); + }); + } + + /** + * 上传小文件到 oss + * @param inOssFileName + * @param file + * @returns + */ + private putObject(inOssFileName, file): Promise { + return new Promise((resolve, reject) => { + const params = { + Bucket: this.configService.get('oss.tencent.config.Bucket'), + Region: this.configService.get('oss.tencent.config.Region'), + Key: inOssFileName, + }; + this.ensureOssClient(); + this.client.putObject( + { + ...params, + StorageClass: 'STANDARD', + Body: file.buffer, // 上传文件对象 + }, + (err) => { + if (err) { + reject(err); + } + this.client.getObjectUrl(params, (err, data) => { + if (err) { + reject(err); + } else { + resolve(data.Url); + } + }); + } + ); + }); + } + + /** + * 初始化分块上传 + * @param inOssFileName + * @returns + */ + private getUploadChunkId(inOssFileName): Promise { + if (this.uploadIdMap.has(inOssFileName)) { + return Promise.resolve(this.uploadIdMap.get(inOssFileName)); + } + + return new Promise((resolve, reject) => { + const params = { + Bucket: this.configService.get('oss.tencent.config.Bucket'), + Region: this.configService.get('oss.tencent.config.Region'), + Key: inOssFileName, + }; + this.ensureOssClient(); + this.client.multipartInit(params, (err, data) => { + if (err) { + reject(err); + } else { + const uploadId = data.UploadId; + this.uploadIdMap.set(inOssFileName, uploadId); + resolve(uploadId); + } + }); + }); + } + + /** + * 上传分片 + * @param uploadId + * @param inOssFileName + * @param chunkIndex + * @param file + * @returns + */ + private uploadChunkToCos(uploadId, inOssFileName, chunkIndex, file): Promise { + return new Promise((resolve, reject) => { + const params = { + Bucket: this.configService.get('oss.tencent.config.Bucket'), + Region: this.configService.get('oss.tencent.config.Region'), + Key: inOssFileName, + UploadId: uploadId, + PartNumber: chunkIndex, + Body: file.buffer, + }; + this.ensureOssClient(); + this.client.multipartUpload(params, (err, data) => { + if (err) { + reject(err); + } else { + if (!this.uploadChunkEtagMap.has(uploadId)) { + this.uploadChunkEtagMap.set(uploadId, []); + } + this.uploadChunkEtagMap.get(uploadId).push({ + PartNumber: chunkIndex, + ETag: data.ETag, + }); + resolve(); + } + }); + }); + } + + /** + * 完成上传分片 + * @param uploadId + * @param inOssFileName + * @param chunkIndex + * @param file + * @returns + */ + private completeUploadChunkToCos(uploadId, inOssFileName): Promise { + return new Promise((resolve, reject) => { + const params = { + Bucket: this.configService.get('oss.tencent.config.Bucket'), + Region: this.configService.get('oss.tencent.config.Region'), + Key: inOssFileName, + }; + this.ensureOssClient(); + const parts = this.uploadChunkEtagMap.get(uploadId); + parts.sort((a, b) => a.PartNumber - b.PartNumber); + + this.client.multipartComplete( + { + ...params, + UploadId: uploadId, + Parts: parts, + }, + (err) => { + if (err) { + reject(err); + } else { + this.client.getObjectUrl(params, (err, data) => { + if (err) { + reject(err); + } else { + resolve(data.Url); + } + }); + } + } + ); + }); + } + + /** + * 上传小文件 + * @param file + * @param query + * @returns + */ + async uploadFile(file: Express.Multer.File, query: FileQuery): Promise { + this.ensureOssClient(); + const { filename, md5 } = query; + const inOssFileName = this.getInOssFileName(md5, filename); + + const maybeOssURL = await this.checkIfAlreadyInOss(inOssFileName); + if (maybeOssURL) { + return maybeOssURL as string; + } + + const res = await this.putObject(inOssFileName, file); + return res as string; + } + + /** + * 上传分片 + * @param file + * @param query + * @returns + */ + async uploadChunk(file: Express.Multer.File, query: FileQuery): Promise { + const { md5, filename, chunkIndex } = query; + + if (!('chunkIndex' in query)) { + throw new Error('请指定 chunkIndex'); + } + + this.ensureOssClient(); + const inOssFileName = this.getInOssFileName(md5, filename); + + const maybeOssURL = await this.checkIfAlreadyInOss(inOssFileName); + if (maybeOssURL) { + return maybeOssURL as string; + } + + const uploadId = await this.getUploadChunkId(inOssFileName); + await this.uploadChunkToCos(uploadId, inOssFileName, chunkIndex, file); + return ''; + } + + /** + * 合并分片 + * @param query + * @returns + */ + async mergeChunk(query: FileQuery): Promise { + const { filename, md5 } = query; + const inOssFileName = this.getInOssFileName(md5, filename); + const uploadId = await this.getUploadChunkId(inOssFileName); + const data = await this.completeUploadChunkToCos(uploadId, inOssFileName); + this.uploadIdMap.delete(inOssFileName); + this.uploadChunkEtagMap.delete(uploadId); + return data; + } +} diff --git a/packages/server/src/main.ts b/packages/server/src/main.ts index 6ff18043..c9347bfd 100644 --- a/packages/server/src/main.ts +++ b/packages/server/src/main.ts @@ -1,5 +1,6 @@ import { HttpResponseExceptionFilter } from '@exceptions/http-response.exception'; import { IS_PRODUCTION } from '@helpers/env.helper'; +import { FILE_DEST, FILE_ROOT_PATH } from '@helpers/file.helper/local.client'; import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@pipes/validation.pipe'; @@ -31,6 +32,7 @@ async function bootstrap() { max: config.get('server.rateLimitMax'), }) ); + app.use(cookieParser()); app.use(compression()); app.use(helmet()); @@ -41,7 +43,16 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe()); app.setGlobalPrefix(config.get('server.prefix') || '/'); + if (config.get('oss.local.enable')) { + const serverStatic = express.static(FILE_ROOT_PATH); + app.use(FILE_DEST, (req, res, next) => { + res.header('Cross-Origin-Resource-Policy', 'cross-origin'); + return serverStatic(req, res, next); + }); + } + await app.listen(port); + console.log(`[think] 主服务启动成功,端口:${port}`); } diff --git a/packages/server/src/services/file.service.ts b/packages/server/src/services/file.service.ts index be1c3b09..5d0767db 100644 --- a/packages/server/src/services/file.service.ts +++ b/packages/server/src/services/file.service.ts @@ -1,25 +1,24 @@ -import { AliyunOssClient } from '@helpers/aliyun.helper'; -import { dateFormat } from '@helpers/date.helper'; -import { uniqueid } from '@helpers/uniqueid.helper'; +import { getOssClient, OssClient } from '@helpers/file.helper'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @Injectable() export class FileService { - private ossClient: AliyunOssClient; + private ossClient: OssClient; constructor(private readonly configService: ConfigService) { - this.ossClient = new AliyunOssClient(this.configService); + this.ossClient = getOssClient(this.configService); } - /** - * 上传文件 - * @param file - */ - async uploadFile(file) { - const { originalname, buffer } = file; - const filename = `/${dateFormat(new Date(), 'yyyy-MM-dd')}/${uniqueid()}/${originalname}`; - const url = await this.ossClient.putFile(filename, buffer); - return url; + async uploadFile(file, query) { + return this.ossClient.uploadFile(file, query); + } + + async uploadChunk(file, query) { + return this.ossClient.uploadChunk(file, query); + } + + async mergeChunk(query) { + return this.ossClient.mergeChunk(query); } } diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 5d24bad8..c3550dda 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -23,5 +23,8 @@ "@controllers/*": ["src/controllers/*"], "@modules/*": ["src/modules/*"] } + }, + "watchOptions": { + "excludeFiles": ["static"] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32846be4..cdbcd6d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,6 +136,7 @@ importers: requestidlecallback-polyfill: ^1.0.2 resize-observer-polyfill: ^1.5.1 scroll-into-view-if-needed: ^2.2.29 + spark-md5: ^3.0.2 timeago.js: ^4.0.2 tippy.js: ^6.3.7 toggle-selection: ^1.0.6 @@ -226,6 +227,7 @@ importers: requestidlecallback-polyfill: 1.0.2 resize-observer-polyfill: 1.5.1 scroll-into-view-if-needed: 2.2.29 + spark-md5: 3.0.2 timeago.js: 4.0.2 tippy.js: 6.3.7 toggle-selection: 1.0.6 @@ -289,6 +291,7 @@ importers: '@types/express': ^4.17.13 '@types/jest': 27.0.2 '@types/lodash': ^4.14.182 + '@types/multer': ^1.4.7 '@types/node': ^16.0.0 '@types/supertest': ^2.0.11 '@typescript-eslint/eslint-plugin': ^5.21.0 @@ -299,6 +302,7 @@ importers: class-validator: ^0.13.2 compression: ^1.7.4 cookie-parser: ^1.4.6 + cos-nodejs-sdk-v5: ^2.11.9 date-fns: ^2.28.0 eslint: ^8.14.0 eslint-config-prettier: ^8.5.0 @@ -350,12 +354,14 @@ importers: '@think/config': link:../config '@think/constants': link:../constants '@think/domains': link:../domains + '@types/multer': 1.4.7 ali-oss: 6.16.0 bcryptjs: 2.4.3 class-transformer: 0.5.1 class-validator: 0.13.2 compression: 1.7.4 cookie-parser: 1.4.6 + cos-nodejs-sdk-v5: 2.11.9 date-fns: 2.28.0 express: 4.17.2 express-rate-limit: 6.2.0_express@4.17.2 @@ -3103,13 +3109,11 @@ packages: dependencies: '@types/connect': 3.4.35 '@types/node': 16.11.21 - dev: true /@types/connect/3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: '@types/node': 16.11.21 - dev: true /@types/cookie-parser/1.4.3: resolution: {integrity: sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==} @@ -3156,7 +3160,6 @@ packages: '@types/node': 16.11.21 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 - dev: true /@types/express/4.17.13: resolution: {integrity: sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==} @@ -3165,7 +3168,6 @@ packages: '@types/express-serve-static-core': 4.17.28 '@types/qs': 6.9.7 '@types/serve-static': 1.13.10 - dev: true /@types/glob/7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} @@ -3236,7 +3238,6 @@ packages: /@types/mime/1.3.2: resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} - dev: true /@types/minimatch/3.0.5: resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} @@ -3246,6 +3247,12 @@ packages: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} dev: true + /@types/multer/1.4.7: + resolution: {integrity: sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==} + dependencies: + '@types/express': 4.17.13 + dev: false + /@types/node/16.11.21: resolution: {integrity: sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A==} @@ -3349,11 +3356,9 @@ packages: /@types/qs/6.9.7: resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} - dev: true /@types/range-parser/1.2.4: resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} - dev: true /@types/react-window/1.8.5: resolution: {integrity: sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==} @@ -3382,7 +3387,6 @@ packages: dependencies: '@types/mime': 1.3.2 '@types/node': 16.11.21 - dev: true /@types/stack-utils/2.0.1: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} @@ -3758,6 +3762,15 @@ packages: indent-string: 4.0.0 dev: true + /ajv-formats/1.6.1: + resolution: {integrity: sha512-4CjkH20If1lhR5CGtqkrVg3bbOtFEG80X9v6jDOIUhbzzbB+UzPBGy8GQhUNVZ0yvMHdMpawCOcy5ydGMsagGQ==} + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 7.2.4 + dev: false + /ajv-formats/2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependenciesMeta: @@ -3791,6 +3804,15 @@ packages: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + /ajv/7.2.4: + resolution: {integrity: sha512-nBeQgg/ZZA3u3SYxyaDvpvDtgZ/EZPF547ARgZBrG9Bhu1vKDwAIjtIf+sDtJUKa2zOcEbmRLBRSyMraS/Oy1A==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: false + /ajv/8.6.3: resolution: {integrity: sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==} dependencies: @@ -3988,6 +4010,17 @@ packages: resolution: {integrity: sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=} dev: true + /asn1/0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /assert-plus/1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + dev: false + /ast-types/0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -4014,7 +4047,6 @@ packages: /asynckit/0.4.0: resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=} - dev: true /at-least-node/1.0.0: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} @@ -4025,11 +4057,24 @@ packages: engines: {node: '>=8.0.0'} dev: false + /atomically/1.7.0: + resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} + engines: {node: '>=10.12.0'} + dev: false + /available-typed-arrays/1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} dev: false + /aws-sign2/0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + dev: false + + /aws4/1.11.0: + resolution: {integrity: sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==} + dev: false + /axios/0.24.0: resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==} dependencies: @@ -4184,6 +4229,12 @@ packages: /base64-js/1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + /bcrypt-pbkdf/1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + dependencies: + tweetnacl: 0.14.5 + dev: false + /bcryptjs/2.4.3: resolution: {integrity: sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=} dev: false @@ -4393,6 +4444,10 @@ packages: resolution: {integrity: sha512-2SodVrFFtvGENGCv0ChVJIDQ0KPaS1cg7/qtfMaICgeMolDdo/Z2OD32F0Aq9yl6F4YFwGPBS5AaPqNYiW4PoA==} dev: false + /caseless/0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + dev: false + /chalk/2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -4622,7 +4677,6 @@ packages: engines: {node: '>= 0.8'} dependencies: delayed-stream: 1.0.0 - dev: true /commander/2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -4701,6 +4755,23 @@ packages: yargs: 16.2.0 dev: false + /conf/9.0.2: + resolution: {integrity: sha512-rLSiilO85qHgaTBIIHQpsv8z+NnVfZq3cKuYNCXN1AOqPzced0GWZEe/A517VldRLyQYXUMyV+vszavE2jSAqw==} + engines: {node: '>=10'} + dependencies: + ajv: 7.2.4 + ajv-formats: 1.6.1 + atomically: 1.7.0 + debounce-fn: 4.0.0 + dot-prop: 6.0.1 + env-paths: 2.2.1 + json-schema-typed: 7.0.3 + make-dir: 3.1.0 + onetime: 5.1.2 + pkg-up: 3.1.0 + semver: 7.3.5 + dev: false + /consola/2.15.3: resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} dev: false @@ -4778,6 +4849,10 @@ packages: requiresBuild: true dev: false + /core-util-is/1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + dev: false + /core-util-is/1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: false @@ -4790,6 +4865,16 @@ packages: vary: 1.1.2 dev: false + /cos-nodejs-sdk-v5/2.11.9: + resolution: {integrity: sha512-szsUw/8hx1RWUfMNwgErzYcdPM3EwcmgbylqQf82HPZALMCAcaa7qCeAxVQHNvCumWYeQLy7EEloZjMUyjg7Ug==} + engines: {node: '>= 6'} + dependencies: + conf: 9.0.2 + mime-types: 2.1.34 + request: 2.88.2 + xml2js: 0.4.23 + dev: false + /cosmiconfig/6.0.0: resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==} engines: {node: '>=8'} @@ -4891,6 +4976,13 @@ packages: /csstype/3.0.10: resolution: {integrity: sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==} + /dashdash/1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + dependencies: + assert-plus: 1.0.0 + dev: false + /data-uri-to-buffer/3.0.1: resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} engines: {node: '>= 6'} @@ -4926,6 +5018,13 @@ packages: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} dev: false + /debounce-fn/4.0.0: + resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==} + engines: {node: '>=10'} + dependencies: + mimic-fn: 3.1.0 + dev: false + /debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} dependencies: @@ -5065,7 +5164,6 @@ packages: /delayed-stream/1.0.0: resolution: {integrity: sha1-3zrhmayt+31ECqrgsp4icrJOxhk=} engines: {node: '>=0.4.0'} - dev: true /denque/2.0.1: resolution: {integrity: sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==} @@ -5153,6 +5251,13 @@ packages: resolution: {integrity: sha512-kD+f8qEaa42+mjdOpKeztu9Mfx5bv9gVLO6K9jRx4uGvh6Wv06Srn4jr1wPNY2OOUGGSKHNFN+A8MA3v0E0QAQ==} dev: false + /dot-prop/6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + dependencies: + is-obj: 2.0.0 + dev: false + /dotenv-expand/5.1.0: resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} dev: false @@ -5180,6 +5285,13 @@ packages: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true + /ecc-jsbn/0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + dev: false + /ecdsa-sig-formatter/1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} dependencies: @@ -5248,6 +5360,11 @@ packages: resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==} dev: false + /env-paths/2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + dev: false + /error-ex/1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: @@ -5726,6 +5843,10 @@ packages: is-extendable: 0.1.1 dev: false + /extend/3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + dev: false + /external-editor/3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -5735,6 +5856,11 @@ packages: tmp: 0.0.33 dev: true + /extsprintf/1.3.0: + resolution: {integrity: sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=} + engines: {'0': node >=0.6.0} + dev: false + /fast-deep-equal/3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -5869,6 +5995,13 @@ packages: locate-path: 2.0.0 dev: true + /find-up/3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + dependencies: + locate-path: 3.0.0 + dev: false + /find-up/4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -5902,6 +6035,10 @@ packages: resolution: {integrity: sha1-C+4AUBiusmDQo6865ljdATbsG5k=} dev: false + /forever-agent/0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + dev: false + /fork-ts-checker-webpack-plugin/6.5.0_787dd39517260957bc59f00cd4915d0b: resolution: {integrity: sha512-cS178Y+xxtIjEUorcHddKS7yCMlrDPV31mt47blKKRfMd70Kxu5xruAFE2o9sDY6wVC5deuob/u/alD04YYHnw==} engines: {node: '>=10', yarn: '>=1.0.0'} @@ -5934,6 +6071,15 @@ packages: webpack: 5.66.0 dev: true + /form-data/2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.34 + dev: false + /form-data/3.0.1: resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} engines: {node: '>= 6'} @@ -6111,6 +6257,12 @@ packages: - supports-color dev: false + /getpass/0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + dependencies: + assert-plus: 1.0.0 + dev: false + /glob-parent/5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -6205,6 +6357,20 @@ packages: /graceful-fs/4.2.9: resolution: {integrity: sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==} + /har-schema/2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + dev: false + + /har-validator/5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + dependencies: + ajv: 6.12.6 + har-schema: 2.0.0 + dev: false + /hard-rejection/2.1.0: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} engines: {node: '>=6'} @@ -6308,6 +6474,15 @@ packages: transitivePeerDependencies: - supports-color + /http-signature/1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + dependencies: + assert-plus: 1.0.0 + jsprim: 1.4.2 + sshpk: 1.17.0 + dev: false + /https-proxy-agent/5.0.0: resolution: {integrity: sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==} engines: {node: '>= 6'} @@ -6615,6 +6790,11 @@ packages: engines: {node: '>=0.10.0'} dev: false + /is-obj/2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + dev: false + /is-path-cwd/2.2.0: resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} engines: {node: '>=6'} @@ -6719,7 +6899,6 @@ packages: /is-typedarray/1.0.0: resolution: {integrity: sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=} - dev: true /is-unicode-supported/0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} @@ -7331,6 +7510,10 @@ packages: dependencies: argparse: 2.0.1 + /jsbn/0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + dev: false + /jsdom/16.7.0: resolution: {integrity: sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==} engines: {node: '>=10'} @@ -7397,6 +7580,10 @@ packages: /json-schema-traverse/1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + /json-schema-typed/7.0.3: + resolution: {integrity: sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==} + dev: false + /json-schema/0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} dev: false @@ -7405,6 +7592,10 @@ packages: resolution: {integrity: sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=} dev: true + /json-stringify-safe/5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + dev: false + /json5/1.0.1: resolution: {integrity: sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==} hasBin: true @@ -7457,6 +7648,16 @@ packages: semver: 5.7.1 dev: false + /jsprim/1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + dev: false + /jstoxml/0.2.4: resolution: {integrity: sha1-/z+2eFaIOgMpU8fOjOdIYhD0hEc=} engines: {node: '>=0.2.0'} @@ -7651,6 +7852,14 @@ packages: path-exists: 3.0.0 dev: true + /locate-path/3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + dev: false + /locate-path/5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -7973,7 +8182,11 @@ packages: /mimic-fn/2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - dev: true + + /mimic-fn/3.1.0: + resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} + engines: {node: '>=8'} + dev: false /min-indent/1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} @@ -8274,6 +8487,10 @@ packages: resolution: {integrity: sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==} dev: true + /oauth-sign/0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + dev: false + /object-assign/4.1.1: resolution: {integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=} engines: {node: '>=0.10.0'} @@ -8371,7 +8588,6 @@ packages: engines: {node: '>=6'} dependencies: mimic-fn: 2.1.0 - dev: true /optional/0.1.4: resolution: {integrity: sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw==} @@ -8469,6 +8685,13 @@ packages: p-limit: 1.3.0 dev: true + /p-locate/3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + dependencies: + p-limit: 2.3.0 + dev: false + /p-locate/4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -8580,7 +8803,6 @@ packages: /path-exists/3.0.0: resolution: {integrity: sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=} engines: {node: '>=4'} - dev: true /path-exists/4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} @@ -8632,6 +8854,10 @@ packages: optional: true dev: false + /performance-now/2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + dev: false + /picocolors/0.2.1: resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} dev: false @@ -8742,6 +8968,13 @@ packages: dependencies: find-up: 4.1.0 + /pkg-up/3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + dependencies: + find-up: 3.0.0 + dev: false + /platform/1.3.6: resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} dev: false @@ -9047,7 +9280,6 @@ packages: /psl/1.8.0: resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==} - dev: true /pump/3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} @@ -9069,6 +9301,11 @@ packages: dependencies: side-channel: 1.0.4 + /qs/6.5.3: + resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} + engines: {node: '>=0.6'} + dev: false + /qs/6.9.3: resolution: {integrity: sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==} engines: {node: '>=0.6'} @@ -9449,6 +9686,33 @@ packages: resolution: {integrity: sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=} dev: false + /request/2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + dependencies: + aws-sign2: 0.7.0 + aws4: 1.11.0 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + har-validator: 5.1.5 + http-signature: 1.2.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.34 + oauth-sign: 0.9.0 + performance-now: 2.1.0 + qs: 6.5.3 + safe-buffer: 5.2.1 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 3.4.0 + dev: false + /requestidlecallback-polyfill/1.0.2: resolution: {integrity: sha512-zzkRzvMe7UdV0M7AIU70vl2fh4rFnNYDL8U0ISwWiOX/5MowBV1ESYCWSQP/KsgJNUOC/AS6X3DApOmxoyE6MA==} dev: false @@ -9920,6 +10184,10 @@ packages: /sourcemap-codec/1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + /spark-md5/3.0.2: + resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} + dev: false + /spawn-command/0.0.2-1: resolution: {integrity: sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=} dev: false @@ -9965,6 +10233,22 @@ packages: engines: {node: '>= 0.6'} dev: false + /sshpk/1.17.0: + resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + dev: false + /stack-utils/2.0.5: resolution: {integrity: sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==} engines: {node: '>=10'} @@ -10614,6 +10898,14 @@ packages: engines: {node: '>=0.6'} dev: false + /tough-cookie/2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + dependencies: + psl: 1.8.0 + punycode: 2.1.1 + dev: false + /tough-cookie/4.0.0: resolution: {integrity: sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==} engines: {node: '>=6'} @@ -10769,6 +11061,16 @@ packages: typescript: 4.5.5 dev: true + /tunnel-agent/0.6.0: + resolution: {integrity: sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /tweetnacl/0.14.5: + resolution: {integrity: sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=} + dev: false + /type-check/0.3.2: resolution: {integrity: sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=} engines: {node: '>= 0.8.0'} @@ -11066,6 +11368,12 @@ packages: engines: {node: '>= 0.4.0'} dev: false + /uuid/3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + dev: false + /uuid/8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -11101,6 +11409,15 @@ packages: engines: {node: '>= 0.8'} dev: false + /verror/1.10.0: + resolution: {integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=} + engines: {'0': node >=0.6.0} + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + dev: false + /viewerjs/1.10.4: resolution: {integrity: sha512-CjMt64yC9D+XUx2t3F0TPbh/Yt5+/ke8/s3IizXa6NtksdJUFDoCcNxi/KRZ9eiZPR/D77pHnnQzAtCoLDaGIw==} dev: false