From a05908d28f23662e76143fd9b4868d12b4f0e526 Mon Sep 17 00:00:00 2001 From: fantasticit Date: Sat, 4 Jun 2022 17:01:33 +0800 Subject: [PATCH] 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