From a05908d28f23662e76143fd9b4868d12b4f0e526 Mon Sep 17 00:00:00 2001 From: fantasticit Date: Sat, 4 Jun 2022 17:01:33 +0800 Subject: [PATCH 01/12] feat: add local file oss --- .eslintignore | 1 + config/dev.yaml | 15 ++- packages/client/package.json | 1 + packages/client/src/services/file.ts | 121 +++++++++++++++-- .../tiptap/core/wrappers/attachment/index.tsx | 38 +++++- packages/domains/lib/api/file.d.ts | 17 +++ packages/domains/lib/api/file.js | 19 ++- packages/domains/src/api/file.ts | 20 +++ packages/server/.gitignore | 5 +- packages/server/package.json | 1 + packages/server/src/app.module.ts | 4 + .../server/src/controllers/file.controller.ts | 40 +++++- .../server/src/helpers/file.helper/index.ts | 14 ++ .../src/helpers/file.helper/local.client.ts | 124 ++++++++++++++++++ .../src/helpers/file.helper/oss.client.ts | 36 +++++ packages/server/src/main.ts | 11 ++ packages/server/src/services/file.service.ts | 27 ++-- packages/server/tsconfig.json | 3 + pnpm-lock.yaml | 22 ++-- 19 files changed, 465 insertions(+), 54 deletions(-) create mode 100644 packages/server/src/helpers/file.helper/index.ts create mode 100644 packages/server/src/helpers/file.helper/local.client.ts create mode 100644 packages/server/src/helpers/file.helper/oss.client.ts 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..e89d08a8 100644 --- a/config/dev.yaml +++ b/config/dev.yaml @@ -42,12 +42,17 @@ db: # oss 文件存储服务 oss: + local: + enable: true + server: 'http://localhost:5002' 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..9a9af4b1 100644 --- a/packages/client/src/services/file.ts +++ b/packages/client/src/services/file.ts @@ -1,30 +1,123 @@ +import { FILE_CHUNK_SIZE, FileApiDefinition } from '@think/domains'; +import SparkMD5 from 'spark-md5'; + import { HttpClient } from './http-client'; -const ONE_MB = 1 * 1024 * 1024; +const splitBigFile = (file: File): Promise<{ chunks: File[]; md5: string }> => { + return new Promise((resolve, reject) => { + const spark = new SparkMD5.ArrayBuffer(); + const fileReader = new FileReader(); + const chunks = []; + const len = Math.ceil(file.size / FILE_CHUNK_SIZE); + let current = 0; -export const readFileAsDataURL = (file): Promise => { - if (file.size > ONE_MB) { - return Promise.reject(new Error('文件过大,请实现文件上传到存储服务!')); - } + fileReader.onload = (e) => { + current++; - return new Promise((resolve) => { - const reader = new FileReader(); - reader.addEventListener('load', (e) => resolve(e.target.result), { once: true }); - reader.readAsDataURL(file); + const chunk = e.target.result; + spark.append(chunk); + + if (current < len) { + loadChunk(); + } else { + resolve({ chunks, md5: spark.end() }); + } + }; + + fileReader.onerror = (err) => { + reject(err); + }; + + const loadChunk = () => { + const start = current * FILE_CHUNK_SIZE; + const end = Math.min(start + FILE_CHUNK_SIZE, file.size); + const chunk = file.slice(start, end); + + chunks.push(chunk); + fileReader.readAsArrayBuffer(chunk); + }; + + loadChunk(); }); }; -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) => { + const wraponUploadProgress = (percent) => { + return onUploadProgress && onUploadProgress(Math.ceil(percent * 100)); + }; + + const filename = file.name; + 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 = {}; + + await Promise.all( + chunks.map((chunk, index) => + uploadFileToServer({ + filename, + file: chunk, + chunkIndex: index + 1, + md5, + isChunk: true, + onUploadProgress: (progress) => { + progressMap[index] = progress * unitPercent; + wraponUploadProgress( + Object.keys(progressMap).reduce((a, c) => { + return (a += progressMap[c]); + }, 0) + ); + }, + }) + ) + ); + const url = await HttpClient.request({ + method: FileApiDefinition.mergeChunk.method, + url: FileApiDefinition.mergeChunk.client(), + params: { + filename, + md5, + }, + }); + return url; + } +}; diff --git a/packages/client/src/tiptap/core/wrappers/attachment/index.tsx b/packages/client/src/tiptap/core/wrappers/attachment/index.tsx index 67910e32..afdc7788 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, 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,26 @@ 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); 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 +75,21 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
- {loading ? '正在上传中' : '请选择文件'} + {loading ? ( + showProgress ? ( + + ) : ( + '正在上传中' + ) + ) : ( + '请选择文件' + )} 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..59b04516 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -31,6 +31,7 @@ "@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", diff --git a/packages/server/src/app.module.ts b/packages/server/src/app.module.ts index fc29e2a6..b606bb41 100644 --- a/packages/server/src/app.module.ts +++ b/packages/server/src/app.module.ts @@ -25,6 +25,7 @@ import { Cron, ScheduleModule } from '@nestjs/schedule'; import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; import { getConfig } from '@think/config'; import * as fs from 'fs-extra'; +import * as lodash from 'lodash'; import { LoggerModule } from 'nestjs-pino'; import * as path from 'path'; import pino from 'pino'; @@ -54,6 +55,8 @@ const MODULES = [ ViewModule, ]; +console.log(lodash.get(getConfig(), 'oss.local.enable')); + @Module({ imports: [ ConfigModule.forRoot({ @@ -85,6 +88,7 @@ const MODULES = [ } as TypeOrmModuleOptions; }, }), + ...MODULES, ].filter(Boolean), controllers: [], 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/index.ts b/packages/server/src/helpers/file.helper/index.ts new file mode 100644 index 00000000..822b2b20 --- /dev/null +++ b/packages/server/src/helpers/file.helper/index.ts @@ -0,0 +1,14 @@ +import { ConfigService } from '@nestjs/config'; + +import { LocalOssClient } from './local.client'; +import { OssClient } from './oss.client'; + +export { OssClient }; + +export const getOssClient = (configService: ConfigService): OssClient => { + if (configService.get('oss.local.enable')) { + return new LocalOssClient(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..54f10620 --- /dev/null +++ b/packages/server/src/helpers/file.helper/local.client.ts @@ -0,0 +1,124 @@ +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); + +const pipeWriteStream = (filepath, writeStream): Promise => { + return new Promise((resolve) => { + const readStream = fs.createReadStream(filepath); + readStream.on('end', () => { + fs.removeSync(filepath); + resolve(); + }); + readStream.pipe(writeStream); + }); +}; + +export class LocalOssClient extends BaseOssClient { + /** + * 文件存储路径 + * @param md5 + * @returns + */ + protected storeFilePath(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.storeFilePath(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 { md5, chunkIndex } = query; + + if (!('chunkIndex' in query)) { + throw new Error('请指定 chunkIndex'); + } + + const { absolute } = this.storeFilePath(md5); + 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.storeFilePath(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) => { + const writeStream = fs.createWriteStream(absoluteFilepath, { + start: index * FILE_CHUNK_SIZE, + }); + + if (index === chunks.length - 1) { + writeStream.on('finish', () => { + fs.removeSync(chunksDir); + }); + } + + pipeWriteStream(path.join(chunksDir, chunk), writeStream); + }) + ); + } + + 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..3270d2a3 --- /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/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..f658beb3 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 @@ -350,6 +353,7 @@ 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 @@ -3103,13 +3107,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 +3158,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 +3166,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 +3236,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 +3245,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 +3354,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 +3385,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==} @@ -9920,6 +9922,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 From 1f94d0e46524a85a3b8977ad794e26f099218eb4 Mon Sep 17 00:00:00 2001 From: fantasticit Date: Sat, 4 Jun 2022 19:03:39 +0800 Subject: [PATCH 02/12] client: use web-woker to calculate md5 --- packages/client/src/services/file.ts | 119 ++++++++++-------- packages/client/src/services/spark-md5.js | 31 +++++ .../tiptap/core/wrappers/attachment/index.tsx | 6 +- packages/client/tsconfig.json | 2 +- 4 files changed, 103 insertions(+), 55 deletions(-) create mode 100644 packages/client/src/services/spark-md5.js diff --git a/packages/client/src/services/file.ts b/packages/client/src/services/file.ts index 9a9af4b1..6676c242 100644 --- a/packages/client/src/services/file.ts +++ b/packages/client/src/services/file.ts @@ -4,40 +4,22 @@ import SparkMD5 from 'spark-md5'; import { HttpClient } from './http-client'; const splitBigFile = (file: File): Promise<{ chunks: File[]; md5: string }> => { - return new Promise((resolve, reject) => { - const spark = new SparkMD5.ArrayBuffer(); - const fileReader = new FileReader(); + return new Promise((resolve) => { const chunks = []; const len = Math.ceil(file.size / FILE_CHUNK_SIZE); - let current = 0; - - fileReader.onload = (e) => { - current++; - - const chunk = e.target.result; - spark.append(chunk); - - if (current < len) { - loadChunk(); - } else { - resolve({ chunks, md5: spark.end() }); - } + const sparkWorker = new Worker(new URL('./spark-md5.js', import.meta.url)); + sparkWorker.onmessage = (evt) => { + resolve({ md5: evt.data.md5, chunks }); }; - fileReader.onerror = (err) => { - reject(err); - }; - - const loadChunk = () => { - const start = current * FILE_CHUNK_SIZE; + 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); - fileReader.readAsArrayBuffer(chunk); - }; + } - loadChunk(); + sparkWorker.postMessage({ chunks }); }); }; @@ -74,12 +56,21 @@ const uploadFileToServer = (arg: { }); }; -export const uploadFile = async (file: File, onUploadProgress?: (progress: number) => void) => { +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); @@ -91,33 +82,57 @@ export const uploadFile = async (file: File, onUploadProgress?: (progress: numbe const unitPercent = 1 / chunks.length; const progressMap = {}; - await Promise.all( - chunks.map((chunk, index) => - uploadFileToServer({ - filename, - file: chunk, - chunkIndex: index + 1, - md5, - isChunk: true, - onUploadProgress: (progress) => { - progressMap[index] = progress * unitPercent; - wraponUploadProgress( - Object.keys(progressMap).reduce((a, c) => { - return (a += progressMap[c]); - }, 0) - ); - }, - }) - ) - ); - const url = await HttpClient.request({ - method: FileApiDefinition.mergeChunk.method, - url: FileApiDefinition.mergeChunk.client(), - params: { - filename, - md5, + /** + * 先上传一块分块,如果文件已上传,即无需上传后续分块 + */ + let url = await uploadFileToServer({ + filename, + file: chunks[0], + chunkIndex: 1, + md5, + isChunk: true, + onUploadProgress: (progress) => { + progressMap[0] = 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) => + uploadFileToServer({ + filename, + file: chunk, + chunkIndex: index + 1 + 1, + md5, + isChunk: true, + onUploadProgress: (progress) => { + progressMap[index + 1] = 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/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 afdc7788..9346ef9f 100644 --- a/packages/client/src/tiptap/core/wrappers/attachment/index.tsx +++ b/packages/client/src/tiptap/core/wrappers/attachment/index.tsx @@ -1,5 +1,5 @@ import { IconClose, IconDownload, IconPlayCircle } from '@douyinfe/semi-icons'; -import { Button, Collapsible, Progress, 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'; @@ -46,7 +46,9 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => { } try { - const url = await uploadFile(file, setUploadProgress); + const url = await uploadFile(file, setUploadProgress, () => { + Toast.info('文件较大,文件将在后台进行上传处理,您可继续其他操作'); + }); updateAttributes({ ...fileInfo, url }); toggleLoading(false); setUploadProgress(0); 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"] } From 757eac171ae8ce968d522f088005f026edf488b6 Mon Sep 17 00:00:00 2001 From: fantasticit Date: Sat, 4 Jun 2022 19:03:58 +0800 Subject: [PATCH 03/12] server: return url if file alreay exist --- .../src/helpers/file.helper/local.client.ts | 35 +++++++++++-------- .../src/helpers/file.helper/oss.client.ts | 4 +-- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/server/src/helpers/file.helper/local.client.ts b/packages/server/src/helpers/file.helper/local.client.ts index 54f10620..c336e1b1 100644 --- a/packages/server/src/helpers/file.helper/local.client.ts +++ b/packages/server/src/helpers/file.helper/local.client.ts @@ -11,7 +11,7 @@ const pipeWriteStream = (filepath, writeStream): Promise => { return new Promise((resolve) => { const readStream = fs.createReadStream(filepath); readStream.on('end', () => { - fs.removeSync(filepath); + fs.unlinkSync(filepath); resolve(); }); readStream.pipe(writeStream); @@ -73,14 +73,21 @@ export class LocalOssClient extends BaseOssClient { * @param file * @param query */ - async uploadChunk(file: Express.Multer.File, query: FileQuery): Promise { - const { md5, chunkIndex } = 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 } = this.storeFilePath(md5); + const { absolute, relative } = this.storeFilePath(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); @@ -104,19 +111,17 @@ export class LocalOssClient extends BaseOssClient { await Promise.all( chunks.map((chunk, index) => { - const writeStream = fs.createWriteStream(absoluteFilepath, { - start: index * FILE_CHUNK_SIZE, - }); - - if (index === chunks.length - 1) { - writeStream.on('finish', () => { - fs.removeSync(chunksDir); - }); - } - - pipeWriteStream(path.join(chunksDir, chunk), writeStream); + 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 index 3270d2a3..03d99ca9 100644 --- a/packages/server/src/helpers/file.helper/oss.client.ts +++ b/packages/server/src/helpers/file.helper/oss.client.ts @@ -8,7 +8,7 @@ export type FileQuery = { export abstract class OssClient { abstract uploadFile(file: Express.Multer.File, query: FileQuery): Promise; - abstract uploadChunk(file: Express.Multer.File, query: FileQuery): Promise; + abstract uploadChunk(file: Express.Multer.File, query: FileQuery): Promise; abstract mergeChunk(query: FileQuery): Promise; } @@ -25,7 +25,7 @@ export class BaseOssClient implements OssClient { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - uploadChunk(file: Express.Multer.File, query: FileQuery): Promise { + uploadChunk(file: Express.Multer.File, query: FileQuery): Promise { throw new Error('Method not implemented.'); } From ed03f1e90fadceb966bb96563eceeaf4a8ea514f Mon Sep 17 00:00:00 2001 From: fantasticit Date: Sat, 4 Jun 2022 19:48:43 +0800 Subject: [PATCH 04/12] server: add ali-oss --- packages/client/src/services/http-client.ts | 2 +- .../src/helpers/file.helper/aliyun.client.ts | 152 ++++++++++++++++++ .../server/src/helpers/file.helper/index.ts | 5 + .../src/helpers/file.helper/local.client.ts | 2 +- 4 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 packages/server/src/helpers/file.helper/aliyun.client.ts 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/server/src/helpers/file.helper/aliyun.client.ts b/packages/server/src/helpers/file.helper/aliyun.client.ts new file mode 100644 index 00000000..963dfca6 --- /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 配置是否正确'); + } + } + + /** + * 获取上传文件名 + * @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 index 822b2b20..db22b9be 100644 --- a/packages/server/src/helpers/file.helper/index.ts +++ b/packages/server/src/helpers/file.helper/index.ts @@ -1,11 +1,16 @@ import { ConfigService } from '@nestjs/config'; +import { AliyunOssClient } from './aliyun.client'; import { LocalOssClient } from './local.client'; import { OssClient } from './oss.client'; export { OssClient }; export const getOssClient = (configService: ConfigService): OssClient => { + if (configService.get('oss.aliyun.enable')) { + return new AliyunOssClient(configService); + } + if (configService.get('oss.local.enable')) { 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 index c336e1b1..c31842c6 100644 --- a/packages/server/src/helpers/file.helper/local.client.ts +++ b/packages/server/src/helpers/file.helper/local.client.ts @@ -7,7 +7,7 @@ import { BaseOssClient, FileQuery } from './oss.client'; export const FILE_DEST = '/' + 'static'; export const FILE_ROOT_PATH = path.join(__dirname, '../../../', FILE_DEST); -const pipeWriteStream = (filepath, writeStream): Promise => { +export const pipeWriteStream = (filepath, writeStream): Promise => { return new Promise((resolve) => { const readStream = fs.createReadStream(filepath); readStream.on('end', () => { From 77435213ec4bf314f8626dce032ee8bbce3f457e Mon Sep 17 00:00:00 2001 From: fantasticit Date: Sat, 4 Jun 2022 19:49:34 +0800 Subject: [PATCH 05/12] Update local.client.ts --- packages/server/src/helpers/file.helper/local.client.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/src/helpers/file.helper/local.client.ts b/packages/server/src/helpers/file.helper/local.client.ts index c31842c6..7becc435 100644 --- a/packages/server/src/helpers/file.helper/local.client.ts +++ b/packages/server/src/helpers/file.helper/local.client.ts @@ -24,7 +24,7 @@ export class LocalOssClient extends BaseOssClient { * @param md5 * @returns */ - protected storeFilePath(md5: string): { + protected getStoreDir(md5: string): { relative: string; absolute: string; } { @@ -57,7 +57,7 @@ export class LocalOssClient extends BaseOssClient { */ async uploadFile(file: Express.Multer.File, query: FileQuery): Promise { const { filename, md5 } = query; - const { absolute, relative } = this.storeFilePath(md5); + const { absolute, relative } = this.getStoreDir(md5); const absoluteFilepath = path.join(absolute, filename); const relativeFilePath = path.join(relative, filename); @@ -80,7 +80,7 @@ export class LocalOssClient extends BaseOssClient { throw new Error('请指定 chunkIndex'); } - const { absolute, relative } = this.storeFilePath(md5); + const { absolute, relative } = this.getStoreDir(md5); const absoluteFilepath = path.join(absolute, filename); if (fs.existsSync(absoluteFilepath)) { @@ -100,7 +100,7 @@ export class LocalOssClient extends BaseOssClient { */ async mergeChunk(query: FileQuery): Promise { const { filename, md5 } = query; - const { absolute, relative } = this.storeFilePath(md5); + const { absolute, relative } = this.getStoreDir(md5); const absoluteFilepath = path.join(absolute, filename); const relativeFilePath = path.join(relative, filename); From 76ccf3892570e8487e872a1b809c91339a10afea Mon Sep 17 00:00:00 2001 From: fantasticit Date: Sat, 4 Jun 2022 22:25:18 +0800 Subject: [PATCH 06/12] server: support tencent cos --- packages/server/package.json | 1 + .../server/src/helpers/file.helper/index.ts | 9 +- .../src/helpers/file.helper/tencent.client.ts | 279 +++++++++++++++ pnpm-lock.yaml | 327 +++++++++++++++++- 4 files changed, 604 insertions(+), 12 deletions(-) create mode 100644 packages/server/src/helpers/file.helper/tencent.client.ts diff --git a/packages/server/package.json b/packages/server/package.json index 59b04516..a3e7f873 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -38,6 +38,7 @@ "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/helpers/file.helper/index.ts b/packages/server/src/helpers/file.helper/index.ts index db22b9be..34c92620 100644 --- a/packages/server/src/helpers/file.helper/index.ts +++ b/packages/server/src/helpers/file.helper/index.ts @@ -3,17 +3,18 @@ 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); } - if (configService.get('oss.local.enable')) { - return new LocalOssClient(configService); - } - return new LocalOssClient(configService); }; 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/pnpm-lock.yaml b/pnpm-lock.yaml index f658beb3..cdbcd6d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -302,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 @@ -360,6 +361,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 express: 4.17.2 express-rate-limit: 6.2.0_express@4.17.2 @@ -3760,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: @@ -3793,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: @@ -3990,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'} @@ -4016,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==} @@ -4027,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: @@ -4186,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 @@ -4395,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'} @@ -4624,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==} @@ -4703,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 @@ -4780,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 @@ -4792,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'} @@ -4893,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'} @@ -4928,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: @@ -5067,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==} @@ -5155,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 @@ -5182,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: @@ -5250,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: @@ -5728,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'} @@ -5737,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==} @@ -5871,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'} @@ -5904,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'} @@ -5936,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'} @@ -6113,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'} @@ -6207,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'} @@ -6310,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'} @@ -6617,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'} @@ -6721,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==} @@ -7333,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'} @@ -7399,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 @@ -7407,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 @@ -7459,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'} @@ -7653,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'} @@ -7975,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==} @@ -8276,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'} @@ -8373,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==} @@ -8471,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'} @@ -8582,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==} @@ -8634,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 @@ -8744,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 @@ -9049,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==} @@ -9071,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'} @@ -9451,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 @@ -9971,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'} @@ -10620,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'} @@ -10775,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'} @@ -11072,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 @@ -11107,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 From 9edf9a71bb32b627768884f9386dbcc67671ef5e Mon Sep 17 00:00:00 2001 From: fantasticit Date: Sat, 4 Jun 2022 22:25:35 +0800 Subject: [PATCH 07/12] Update aliyun.client.ts --- packages/server/src/helpers/file.helper/aliyun.client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/helpers/file.helper/aliyun.client.ts b/packages/server/src/helpers/file.helper/aliyun.client.ts index 963dfca6..d092e57f 100644 --- a/packages/server/src/helpers/file.helper/aliyun.client.ts +++ b/packages/server/src/helpers/file.helper/aliyun.client.ts @@ -25,7 +25,7 @@ export class AliyunOssClient extends BaseOssClient { this.client = new AliyunOSS(config); return this.client; } catch (err) { - console.log('无法启动阿里云存储服务,请检查阿里云 OSS 配置是否正确'); + console.log('无法启动阿里云存储服务,请检查阿里云 OSS 配置是否正确', err.message); } } From 3b2b21e9a58fd94bd9876eb5c8e914d4768e987e Mon Sep 17 00:00:00 2001 From: fantasticit Date: Sat, 4 Jun 2022 22:25:48 +0800 Subject: [PATCH 08/12] client: update index --- packages/client/src/services/file.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/client/src/services/file.ts b/packages/client/src/services/file.ts index 6676c242..14faffb5 100644 --- a/packages/client/src/services/file.ts +++ b/packages/client/src/services/file.ts @@ -92,7 +92,7 @@ export const uploadFile = async ( md5, isChunk: true, onUploadProgress: (progress) => { - progressMap[0] = progress * unitPercent; + progressMap[1] = progress * unitPercent; wraponUploadProgress( Object.keys(progressMap).reduce((a, c) => { return (a += progressMap[c]); @@ -103,23 +103,24 @@ export const uploadFile = async ( if (!url) { await Promise.all( - chunks.slice(1).map((chunk, index) => - uploadFileToServer({ + chunks.slice(1).map((chunk, index) => { + const currentIndex = 1 + index + 1; + return uploadFileToServer({ filename, file: chunk, - chunkIndex: index + 1 + 1, + chunkIndex: currentIndex, md5, isChunk: true, onUploadProgress: (progress) => { - progressMap[index + 1] = progress * unitPercent; + progressMap[currentIndex] = progress * unitPercent; wraponUploadProgress( Object.keys(progressMap).reduce((a, c) => { return (a += progressMap[c]); }, 0) ); }, - }) - ) + }); + }) ); url = await HttpClient.request({ method: FileApiDefinition.mergeChunk.method, From ed248be1061587f50e8f6fc4b004ced25c7e38f5 Mon Sep 17 00:00:00 2001 From: fantasticit Date: Sat, 4 Jun 2022 22:30:22 +0800 Subject: [PATCH 09/12] Update dev.yaml --- config/dev.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/config/dev.yaml b/config/dev.yaml index e89d08a8..5d2cde57 100644 --- a/config/dev.yaml +++ b/config/dev.yaml @@ -43,8 +43,17 @@ db: # oss 文件存储服务 oss: local: - enable: true + enable: false + # 线上更改为服务端地址(如:https://api.codingit.cn) server: 'http://localhost:5002' + # 以下为各厂商 sdk 配置,不要修改字段,填入值即可 + tencent: + enable: false + config: + SecretId: '' + SecretKey: '' + Bucket: '' + Region: '' aliyun: enable: false config: From 86ca7c7fd2df0dea5525d073cd8458156f77cfdc Mon Sep 17 00:00:00 2001 From: fantasticit Date: Sat, 4 Jun 2022 22:40:31 +0800 Subject: [PATCH 10/12] root: use local file default --- config/dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/dev.yaml b/config/dev.yaml index 5d2cde57..b38705eb 100644 --- a/config/dev.yaml +++ b/config/dev.yaml @@ -43,7 +43,7 @@ db: # oss 文件存储服务 oss: local: - enable: false + enable: true # 线上更改为服务端地址(如:https://api.codingit.cn) server: 'http://localhost:5002' # 以下为各厂商 sdk 配置,不要修改字段,填入值即可 From 319fb50ab3010ff6694a333c85203f2eeab9dee0 Mon Sep 17 00:00:00 2001 From: fantasticit Date: Sat, 4 Jun 2022 22:40:43 +0800 Subject: [PATCH 11/12] server: remove unused code --- packages/server/src/app.module.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/server/src/app.module.ts b/packages/server/src/app.module.ts index b606bb41..410239ac 100644 --- a/packages/server/src/app.module.ts +++ b/packages/server/src/app.module.ts @@ -25,7 +25,6 @@ import { Cron, ScheduleModule } from '@nestjs/schedule'; import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; import { getConfig } from '@think/config'; import * as fs from 'fs-extra'; -import * as lodash from 'lodash'; import { LoggerModule } from 'nestjs-pino'; import * as path from 'path'; import pino from 'pino'; @@ -55,8 +54,6 @@ const MODULES = [ ViewModule, ]; -console.log(lodash.get(getConfig(), 'oss.local.enable')); - @Module({ imports: [ ConfigModule.forRoot({ From 62d4f720aade566416201f7fa51104b2498f9bb4 Mon Sep 17 00:00:00 2001 From: fantasticit Date: Sat, 4 Jun 2022 22:41:53 +0800 Subject: [PATCH 12/12] Update app.module.ts --- packages/server/src/app.module.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/app.module.ts b/packages/server/src/app.module.ts index 410239ac..fc29e2a6 100644 --- a/packages/server/src/app.module.ts +++ b/packages/server/src/app.module.ts @@ -85,7 +85,6 @@ const MODULES = [ } as TypeOrmModuleOptions; }, }), - ...MODULES, ].filter(Boolean), controllers: [],