create-bubbles 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +15 -0
- package/package.json +1 -2
- package/template-nextjs-vinext-eslint/Dockerfile +4 -4
- package/template-nextjs-vinext-eslint/README.md +6 -2
- package/template-nextjs-vinext-eslint/docker-compose.yaml +4 -4
- package/template-nextjs-vinext-eslint/eslint.config.js +2 -0
- package/template-nextjs-vinext-eslint/package.json +28 -26
- package/template-nextjs-vinext-eslint/pnpm-workspace.yaml +13 -1
- package/template-nextjs-vinext-eslint/src/utils/request/core/index.ts +63 -45
- package/template-nextjs-vinext-eslint/src/utils/request/core/utils.ts +12 -6
- package/template-nextjs-vinext-eslint/vite.config.ts +1 -1
- package/template-react-rsbuild-biome/.agents/rules/SVG/344/275/277/347/224/250.md +8 -0
- package/template-react-rsbuild-biome/AGENTS.md +11 -0
- package/template-react-rsbuild-biome/index.html +2 -2
- package/template-react-rsbuild-biome/package.json +25 -24
- package/template-react-rsbuild-biome/pnpm-workspace.yaml +5 -0
- package/template-react-rsbuild-biome/rsbuild.config.ts +1 -1
- package/template-react-rsbuild-biome/src/assets/icon/logo.svg +1 -1
- package/template-react-rsbuild-biome/src/assets/svg/draft.svg +4 -0
- package/template-react-rsbuild-biome/src/components/Icon/svg-icon/README.md +37 -0
- package/template-react-rsbuild-biome/src/components/Icon/svg-icon/SvgIcon.tsx +27 -0
- package/template-react-rsbuild-biome/src/env.d.ts +7 -0
- package/template-react-rsbuild-biome/src/utils/request/index.ts +3 -4
- package/template-react-rsbuild-biome/tsconfig.json +1 -2
- package/template-taro-react-oxc/.oxlintrc.json +0 -1
- package/template-taro-react-oxc/package.json +53 -52
- package/template-taro-react-oxc/pnpm-workspace.yaml +46 -0
- package/template-taro-vue-eslint/package.json +56 -56
- package/template-vp-monorepo-react-hono/apps/web/.agents/rules/SVG/344/275/277/347/224/250.md +8 -0
- package/template-vp-monorepo-react-hono/apps/web/AGENTS.md +11 -0
- package/template-vp-monorepo-react-hono/apps/web/package.json +1 -0
- package/template-vp-monorepo-react-hono/apps/web/src/assets/svg/draft.svg +4 -0
- package/template-vp-monorepo-react-hono/apps/web/src/components/Icon/svg-icon/README.md +37 -0
- package/template-vp-monorepo-react-hono/apps/web/src/components/Icon/svg-icon/SvgIcon.tsx +27 -0
- package/template-vp-monorepo-react-hono/apps/web/tsconfig.json +1 -1
- package/template-vp-monorepo-react-hono/apps/web/vite.config.ts +2 -0
- package/template-vp-monorepo-react-hono/pnpm-workspace.yaml +1 -0
- package/template-vp-monorepo-react-nestjs/.agents/rules//344/273/243/347/240/201/350/256/276/350/256/241.md +4 -0
- package/template-vp-monorepo-react-nestjs/.agents/rules//345/205/261/344/272/253/344/273/243/347/240/201.md +4 -0
- package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//351/234/200/346/261/202/346/226/207/346/241/243.md +177 -0
- package/template-vp-monorepo-react-nestjs/.spaces/server/04.upload.md +4504 -0
- package/template-vp-monorepo-react-nestjs/.spaces/server/05./344/270/212/344/274/240/346/224/271/351/200/240/346/226/271/346/241/210/README.md +858 -0
- package/template-vp-monorepo-react-nestjs/.spaces/server/06./345/205/254/345/205/261/351/230/237/345/210/227/346/216/245/345/205/245/346/226/271/346/241/210/README.md +1890 -0
- package/template-vp-monorepo-react-nestjs/.spaces/server/06./345/205/254/345/205/261/351/230/237/345/210/227/346/216/245/345/205/245/346/226/271/346/241/210//344/270/212/344/274/240/346/270/205/347/220/206.md +1465 -0
- package/template-vp-monorepo-react-nestjs/AGENTS.md +8 -12
- package/template-vp-monorepo-react-nestjs/apps/server/.agents/rules/error-handling.md +84 -0
- package/template-vp-monorepo-react-nestjs/apps/server/.env.development +60 -0
- package/template-vp-monorepo-react-nestjs/apps/server/.env.production +16 -0
- package/template-vp-monorepo-react-nestjs/apps/server/AGENTS.md +9 -0
- package/template-vp-monorepo-react-nestjs/apps/server/drizzle/0000_military_colonel_america.sql +11 -0
- package/template-vp-monorepo-react-nestjs/apps/server/drizzle/0001_loud_siren.sql +27 -0
- package/template-vp-monorepo-react-nestjs/apps/server/drizzle/meta/0000_snapshot.json +97 -0
- package/template-vp-monorepo-react-nestjs/apps/server/drizzle/meta/0001_snapshot.json +354 -0
- package/template-vp-monorepo-react-nestjs/apps/server/drizzle/meta/_journal.json +20 -0
- package/template-vp-monorepo-react-nestjs/apps/server/package.json +4 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/app.module.ts +14 -2
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/adapters/fastify.adapter.ts +6 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/config/index.ts +2 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/config/queue.config.ts +105 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/config/storage.config.ts +33 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/database/schema.ts +57 -1
- package/template-vp-monorepo-react-nestjs/apps/server/src/main.ts +1 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.service.ts +1 -3
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/dto/auth-repsponse.dto.ts +2 -2
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/queue-worker.bootstrap.ts +24 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.constants.ts +24 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.contracts.ts +35 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.module.ts +56 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.processor.ts +62 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.service.ts +58 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/error-catelog.spec.ts +34 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/dto/initiate-mutipart-upload.dto.ts +26 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/dto/upload-params.dto.ts +15 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/storage/exact-size.transform.ts +66 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/storage/minio-storage.adapter.ts +239 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/storage/storage.port.ts +61 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.constants.ts +23 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.controller.ts +109 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.errors.ts +80 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.module.ts +20 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.reponsitory.ts +166 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.service.ts +696 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/utils/object-key.ts +10 -0
- package/template-vp-monorepo-react-nestjs/apps/server/test/task-queue.integration.spec.ts +120 -0
- package/template-vp-monorepo-react-nestjs/apps/server/test/task-queue.processor.spec.ts +63 -0
- package/template-vp-monorepo-react-nestjs/apps/server/test/task-queue.service.spec.ts +106 -0
- package/template-vp-monorepo-react-nestjs/apps/web/.agents/rules/SVG/344/275/277/347/224/250.md +8 -0
- package/template-vp-monorepo-react-nestjs/apps/web/.agents/rules//345/274/271/347/252/227/347/273/204/344/273/266.md +4 -0
- package/template-vp-monorepo-react-nestjs/apps/web/.agents/rules//346/226/207/344/273/266/346/213/206/345/210/206.md +4 -0
- package/template-vp-monorepo-react-nestjs/apps/web/.agents/rules//347/273/204/344/273/266/345/221/275/345/220/215.md +3 -0
- package/template-vp-monorepo-react-nestjs/apps/web/.agents/rules//350/241/250/346/240/274/351/241/265/351/235/242.md +11 -0
- package/template-vp-monorepo-react-nestjs/apps/web/.agents/rules//351/241/265/351/235/242/346/216/245/345/217/243.md +3 -0
- package/template-vp-monorepo-react-nestjs/apps/web/.env.dev +1 -1
- package/template-vp-monorepo-react-nestjs/apps/web/AGENTS.md +16 -0
- package/template-vp-monorepo-react-nestjs/apps/web/README.MD +153 -0
- package/template-vp-monorepo-react-nestjs/apps/web/index.html +3 -2
- package/template-vp-monorepo-react-nestjs/apps/web/package.json +6 -1
- package/template-vp-monorepo-react-nestjs/apps/web/public/favicon.svg +9 -1
- package/template-vp-monorepo-react-nestjs/apps/web/src/App.module.css +5 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/App.tsx +16 -9
- package/template-vp-monorepo-react-nestjs/apps/web/src/api/auth.ts +9 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/draft.svg +4 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/components/DraftProTable/DraftProTable.module.css +78 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/components/DraftProTable/DraftProTable.tsx +99 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/components/DraftProTable/README.md +34 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/components/FullHeightProTable/FullHeightProTable.module.css +54 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/components/FullHeightProTable/FullHeightProTable.tsx +41 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/components/Icon/svg-icon/README.md +37 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/components/Icon/svg-icon/SvgIcon.tsx +27 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/components/Loading/PageLoading.module.css +7 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/components/Loading/PageLoading.tsx +2 -1
- package/template-vp-monorepo-react-nestjs/apps/web/src/config/theme.ts +20 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/layouts/BasicLayout.module.css +17 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/layouts/BasicLayout.tsx +106 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/i18n/index.module.css +23 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/i18n/index.tsx +53 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/components/ProjectFormDialog.tsx +132 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/config/columns.tsx +121 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/config/index.ts +77 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/components/DraftProjectFormDialog.tsx +140 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/config/columns.tsx +127 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/config/index.ts +39 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/config/projects.ts +48 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/index.module.css +49 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/index.tsx +163 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/index.module.css +53 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/index.tsx +135 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/home/index.module.css +23 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/home/index.tsx +26 -43
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/login/api.ts +11 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/login/index.tsx +183 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/pages/login/login.css +164 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/router/index.tsx +36 -12
- package/template-vp-monorepo-react-nestjs/apps/web/src/styles/index.css +11 -4
- package/template-vp-monorepo-react-nestjs/apps/web/src/utils/request/core/index.ts +55 -47
- package/template-vp-monorepo-react-nestjs/apps/web/src/utils/request/index.ts +10 -5
- package/template-vp-monorepo-react-nestjs/apps/web/src/utils/storage/cookie.ts +50 -0
- package/template-vp-monorepo-react-nestjs/apps/web/src/utils/storage/session.ts +33 -0
- package/template-vp-monorepo-react-nestjs/apps/web/test/draft-projects.spec.ts +89 -0
- package/template-vp-monorepo-react-nestjs/apps/web/test/request.spec.ts +145 -0
- package/template-vp-monorepo-react-nestjs/apps/web/test/storage.spec.ts +108 -0
- package/template-vp-monorepo-react-nestjs/apps/web/test/tsconfig.json +7 -0
- package/template-vp-monorepo-react-nestjs/apps/web/tsconfig.json +1 -1
- package/template-vp-monorepo-react-nestjs/apps/web/vite.config.ts +7 -0
- package/template-vp-monorepo-react-nestjs/apps/web-vue/tsconfig.tsbuildinfo +1 -0
- package/template-vp-monorepo-react-nestjs/docker-compose.yml +19 -0
- package/template-vp-monorepo-react-nestjs/mise.toml +1 -1
- package/template-vp-monorepo-react-nestjs/package.json +1 -6
- package/template-vp-monorepo-react-nestjs/pnpm-workspace.yaml +9 -1
- package/template-vp-react/.agents/rules/SVG/344/275/277/347/224/250.md +8 -0
- package/template-vp-react/AGENTS.md +11 -0
- package/template-vp-react/commitlint.config.js +0 -0
- package/template-vp-react/package.json +21 -26
- package/template-vp-react/pnpm-workspace.yaml +6 -0
- package/template-vp-react/src/assets/svg/draft.svg +4 -0
- package/template-vp-react/src/components/Icon/svg-icon/README.md +37 -0
- package/template-vp-react/src/components/Icon/svg-icon/SvgIcon.tsx +27 -0
- package/template-vp-react/src/utils/request/index.ts +18 -5
- package/template-vp-react/tsconfig.json +2 -3
- package/template-vp-react/vite.config.ts +2 -0
- package/template-vp-react-shadcn/.agents/rules/SVG/344/275/277/347/224/250.md +8 -0
- package/template-vp-react-shadcn/.vscode/settings.json +2 -2
- package/template-vp-react-shadcn/{agents.md → AGENTS.md} +12 -2
- package/template-vp-react-shadcn/README.md +41 -0
- package/template-vp-react-shadcn/commitlint.config.js +1 -1
- package/template-vp-react-shadcn/components.json +1 -1
- package/template-vp-react-shadcn/package.json +25 -32
- package/template-vp-react-shadcn/pnpm-workspace.yaml +10 -0
- package/template-vp-react-shadcn/src/App.tsx +10 -10
- package/template-vp-react-shadcn/src/assets/svg/draft.svg +4 -0
- package/template-vp-react-shadcn/src/components/Icon/svg-icon/README.md +37 -0
- package/template-vp-react-shadcn/src/components/Icon/svg-icon/SvgIcon.tsx +27 -0
- package/template-vp-react-shadcn/src/components/Loading/PageLoading.tsx +4 -4
- package/template-vp-react-shadcn/src/components/Player/VideoJS/index.tsx +94 -78
- package/template-vp-react-shadcn/src/components/Player/index.ts +1 -1
- package/template-vp-react-shadcn/src/components/ui/button.tsx +25 -32
- package/template-vp-react-shadcn/src/components/ui/spinner.tsx +8 -7
- package/template-vp-react-shadcn/src/components/ui/toast.tsx +229 -0
- package/template-vp-react-shadcn/src/lib/utils.ts +3 -3
- package/template-vp-react-shadcn/src/main.tsx +5 -4
- package/template-vp-react-shadcn/src/pages/home/index.tsx +43 -35
- package/template-vp-react-shadcn/src/router/index.tsx +15 -15
- package/template-vp-react-shadcn/src/styles/index.css +4 -4
- package/template-vp-react-shadcn/src/styles/shadcn.css +2 -2
- package/template-vp-react-shadcn/src/utils/env/index.tsx +2 -2
- package/template-vp-react-shadcn/src/utils/request/alova-core/index.ts +102 -30
- package/template-vp-react-shadcn/src/utils/request/alova-core/utils.ts +5 -2
- package/template-vp-react-shadcn/src/utils/request/index.ts +32 -19
- package/template-vp-react-shadcn/src/utils/request/readme.md +41 -38
- package/template-vp-react-shadcn/tsconfig.json +2 -3
- package/template-vp-react-shadcn/vite.config.ts +29 -15
- package/template-vp-vue-eslint-vapor/package.json +26 -33
- package/template-vp-vue-eslint-vapor/pnpm-workspace.yaml +29 -1
- package/template-vue-vp-eslint/README.md +3 -10
- package/template-vue-vp-eslint/package.json +27 -34
- package/template-vue-vp-eslint/pnpm-workspace.yaml +27 -2
- package/template-vue-vp-eslint/src/layout/default/index.vue +4 -3
- package/dist/index.mjs +0 -422
- package/template-vp-monorepo-react-nestjs/.agents/auth.config.ts +0 -0
- package/template-vp-monorepo-react-nestjs/.spaces/server/todo.md +0 -991
- package/template-vp-monorepo-react-nestjs/apps/server/README.md +0 -3
- package/template-vp-react-shadcn/.oxfmtrc.jsonc +0 -8
- package/template-vp-react-shadcn/.oxlintrc.jsonc +0 -3
- package/template-vp-react-shadcn/src/components/ui/sonner.tsx +0 -43
package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/storage/storage.port.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Readable } from 'stream'
|
|
2
|
+
|
|
3
|
+
export const STORAGE_PORT = Symbol('STORAGE_PORT')
|
|
4
|
+
|
|
5
|
+
export class StorageMultipartNotFoundError extends Error {
|
|
6
|
+
constructor(cause: unknown) {
|
|
7
|
+
super('Multipart upload does not exist', { cause })
|
|
8
|
+
this.name = StorageMultipartNotFoundError.name
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CreateMultipartInput {
|
|
13
|
+
bucket: string
|
|
14
|
+
objectKey: string
|
|
15
|
+
contentType: string
|
|
16
|
+
metadata: Record<string, string>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UploadPartInput {
|
|
20
|
+
bucket: string
|
|
21
|
+
objectKey: string
|
|
22
|
+
storageUploadId: string
|
|
23
|
+
partNumber: number
|
|
24
|
+
body: Readable
|
|
25
|
+
contentLength: number
|
|
26
|
+
abortSignal?: AbortSignal
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MultipartIdentity {
|
|
30
|
+
bucket: string
|
|
31
|
+
objectKey: string
|
|
32
|
+
storageUploadId: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface StoragePart {
|
|
36
|
+
partNumber: number
|
|
37
|
+
etag: string
|
|
38
|
+
size: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface StorageObject {
|
|
42
|
+
etag: string | null
|
|
43
|
+
contentLength: number | null
|
|
44
|
+
metadata: Record<string, string>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface StoragePort {
|
|
48
|
+
createMultipartUpload(input: CreateMultipartInput): Promise<{ storageUploadId: string }>
|
|
49
|
+
|
|
50
|
+
uploadPart(input: UploadPartInput): Promise<{ etag: string }>
|
|
51
|
+
|
|
52
|
+
listParts(input: MultipartIdentity): Promise<StoragePart[]>
|
|
53
|
+
|
|
54
|
+
completeMultipartUpload(
|
|
55
|
+
input: MultipartIdentity & { parts: readonly StoragePart[] },
|
|
56
|
+
): Promise<{ etag: string | null }>
|
|
57
|
+
|
|
58
|
+
abortMultipartUpload(input: MultipartIdentity): Promise<void>
|
|
59
|
+
|
|
60
|
+
headObject(input: { bucket: string; objectKey: string }): Promise<StorageObject | null>
|
|
61
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const UPLOAD_PART_SIZE = 10 * 1024 * 1024
|
|
2
|
+
export const UPLOAD_MAX_PARTS = 10_000
|
|
3
|
+
export const UPLOAD_MAX_FILE_SIZE = 90 * 1024 * 1024 * 1024
|
|
4
|
+
|
|
5
|
+
export function calculateTotalParts(fileSize: number): number {
|
|
6
|
+
return Math.ceil(fileSize / UPLOAD_PART_SIZE)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function calculateExpectedPartSize(
|
|
10
|
+
fileSize: number,
|
|
11
|
+
totalParts: number,
|
|
12
|
+
partNumber: number,
|
|
13
|
+
): number {
|
|
14
|
+
if (partNumber < 1 || partNumber > totalParts) {
|
|
15
|
+
throw new RangeError('partNumber is outside the upload range')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (partNumber < totalParts) {
|
|
19
|
+
return UPLOAD_PART_SIZE
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return fileSize - (totalParts - 1) * UPLOAD_PART_SIZE
|
|
23
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { CurrentAuth } from '@/common/decorators/current-auth.decorator'
|
|
2
|
+
import {
|
|
3
|
+
Body,
|
|
4
|
+
Controller,
|
|
5
|
+
Delete,
|
|
6
|
+
Get,
|
|
7
|
+
Headers,
|
|
8
|
+
HttpCode,
|
|
9
|
+
HttpStatus,
|
|
10
|
+
Param,
|
|
11
|
+
Post,
|
|
12
|
+
Put,
|
|
13
|
+
Req,
|
|
14
|
+
Res,
|
|
15
|
+
} from '@nestjs/common'
|
|
16
|
+
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger'
|
|
17
|
+
import { type FastifyReply, FastifyRequest } from 'fastify'
|
|
18
|
+
import type { Readable } from 'stream'
|
|
19
|
+
import { type CurrentAuthType } from '../auth/session/session.types'
|
|
20
|
+
import { InitiateMultipartUploadDto } from './dto/initiate-mutipart-upload.dto'
|
|
21
|
+
import { UploadPartParamsDto, UploadSessionParamsDto } from './dto/upload-params.dto'
|
|
22
|
+
import { UploadService } from './upload.service'
|
|
23
|
+
|
|
24
|
+
type OctetStreamRequest = FastifyRequest & {
|
|
25
|
+
body: Readable
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@ApiTags('大文件分片上传')
|
|
29
|
+
@ApiBearerAuth('session')
|
|
30
|
+
@Controller('uploads/multipart')
|
|
31
|
+
export class UploadController {
|
|
32
|
+
constructor(private readonly uploadService: UploadService) {}
|
|
33
|
+
|
|
34
|
+
@ApiOperation({ summary: '初始化大文件分片上传' })
|
|
35
|
+
@Post()
|
|
36
|
+
initiate(@CurrentAuth() auth: CurrentAuthType, @Body() body: InitiateMultipartUploadDto) {
|
|
37
|
+
return this.uploadService.initiate(auth.userId, body)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@ApiOperation({ summary: '查询上传状态和已上传分片' })
|
|
41
|
+
@Get(':uploadSessionId')
|
|
42
|
+
getStatus(@CurrentAuth() auth: CurrentAuthType, @Param() params: UploadSessionParamsDto) {
|
|
43
|
+
return this.uploadService.getStatus(auth.userId, params.uploadSessionId)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@ApiOperation({ summary: '上传一个原始二进制分片' })
|
|
47
|
+
@ApiConsumes('application/octet-stream')
|
|
48
|
+
@ApiBody({
|
|
49
|
+
schema: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
format: 'binary',
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
@HttpCode(HttpStatus.OK)
|
|
55
|
+
@Put(':uploadSessionId/parts/:partNumber')
|
|
56
|
+
uploadPart(
|
|
57
|
+
@CurrentAuth() auth: CurrentAuthType,
|
|
58
|
+
@Param() params: UploadPartParamsDto,
|
|
59
|
+
@Headers('content-length') contentLength: string | undefined,
|
|
60
|
+
@Headers('content-encoding') contentEncoding: string | undefined,
|
|
61
|
+
@Req() request: OctetStreamRequest,
|
|
62
|
+
@Res({ passthrough: true }) reply: FastifyReply,
|
|
63
|
+
) {
|
|
64
|
+
const abortController = new AbortController()
|
|
65
|
+
const onAborted = () => abortController.abort()
|
|
66
|
+
const onReplyClose = () => {
|
|
67
|
+
if (!reply.raw.writableEnded) {
|
|
68
|
+
abortController.abort()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
request.raw.once('aborted', onAborted)
|
|
73
|
+
reply.raw.once('close', onReplyClose)
|
|
74
|
+
|
|
75
|
+
return this.uploadService
|
|
76
|
+
.uploadPart({
|
|
77
|
+
ownerId: auth.userId,
|
|
78
|
+
uploadSessionId: params.uploadSessionId,
|
|
79
|
+
partNumber: params.partNumber,
|
|
80
|
+
contentLength,
|
|
81
|
+
contentEncoding,
|
|
82
|
+
body: request.body,
|
|
83
|
+
abortSignal: abortController.signal,
|
|
84
|
+
})
|
|
85
|
+
.finally(() => {
|
|
86
|
+
// 前置校验失败时 Service 可能尚未 pipe 请求体。
|
|
87
|
+
// 统一排空剩余字节,避免连接一直占用到代理超时;这里不会聚合 Buffer。
|
|
88
|
+
if (!request.body.readableEnded && !request.body.destroyed) {
|
|
89
|
+
request.body.resume()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
request.raw.off('aborted', onAborted)
|
|
93
|
+
reply.raw.off('close', onReplyClose)
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@ApiOperation({ summary: '校验并完成分片上传' })
|
|
98
|
+
@HttpCode(HttpStatus.OK)
|
|
99
|
+
@Post(':uploadSessionId/complete')
|
|
100
|
+
complete(@CurrentAuth() auth: CurrentAuthType, @Param() params: UploadSessionParamsDto) {
|
|
101
|
+
return this.uploadService.complete(auth.userId, params.uploadSessionId)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@ApiOperation({ summary: '取消分片上传' })
|
|
105
|
+
@Delete(':uploadSessionId')
|
|
106
|
+
abort(@CurrentAuth() auth: CurrentAuthType, @Param() params: UploadSessionParamsDto) {
|
|
107
|
+
return this.uploadService.abort(auth.userId, params.uploadSessionId)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { AppErrorDefinition } from '@/common/exceptions/app.exception'
|
|
2
|
+
import { HttpStatus } from '@nestjs/common'
|
|
3
|
+
|
|
4
|
+
export const UPLOAD_ERRORS = {
|
|
5
|
+
FILE_EMPTY: {
|
|
6
|
+
code: 'UPLOAD.FILE_EMPTY',
|
|
7
|
+
publicMessage: '空文件不能使用分片上传',
|
|
8
|
+
status: HttpStatus.UNPROCESSABLE_ENTITY,
|
|
9
|
+
},
|
|
10
|
+
FILE_TOO_LARGE: {
|
|
11
|
+
code: 'UPLOAD.FILE_TOO_LARGE',
|
|
12
|
+
publicMessage: '文件超过允许的最大大小',
|
|
13
|
+
status: HttpStatus.PAYLOAD_TOO_LARGE,
|
|
14
|
+
},
|
|
15
|
+
CLIENT_UPLOAD_ID_CONFLICT: {
|
|
16
|
+
code: 'UPLOAD.CLIENT_UPLOAD_ID_CONFLICT',
|
|
17
|
+
publicMessage: '该客户端上传标识已用于另一个文件',
|
|
18
|
+
status: HttpStatus.CONFLICT,
|
|
19
|
+
},
|
|
20
|
+
SESSION_NOT_FOUND: {
|
|
21
|
+
code: 'UPLOAD.SESSION_NOT_FOUND',
|
|
22
|
+
publicMessage: '上传会话不存在',
|
|
23
|
+
status: HttpStatus.NOT_FOUND,
|
|
24
|
+
},
|
|
25
|
+
INVALID_STATE: {
|
|
26
|
+
code: 'UPLOAD.INVALID_STATE',
|
|
27
|
+
publicMessage: '当前上传状态不允许执行此操作',
|
|
28
|
+
status: HttpStatus.CONFLICT,
|
|
29
|
+
},
|
|
30
|
+
UPLOAD_EXPIRED: {
|
|
31
|
+
code: 'UPLOAD.UPLOAD_EXPIRED',
|
|
32
|
+
publicMessage: '上传会话已过期,请重新开始',
|
|
33
|
+
status: HttpStatus.GONE,
|
|
34
|
+
},
|
|
35
|
+
PART_NUMBER_INVALID: {
|
|
36
|
+
code: 'UPLOAD.PART_NUMBER_INVALID',
|
|
37
|
+
publicMessage: '分片编号超出当前文件范围',
|
|
38
|
+
status: HttpStatus.UNPROCESSABLE_ENTITY,
|
|
39
|
+
},
|
|
40
|
+
PART_LENGTH_REQUIRED: {
|
|
41
|
+
code: 'UPLOAD.PART_LENGTH_REQUIRED',
|
|
42
|
+
publicMessage: '分片请求缺少有效的 Content-Length',
|
|
43
|
+
status: HttpStatus.LENGTH_REQUIRED,
|
|
44
|
+
},
|
|
45
|
+
PART_SIZE_MISMATCH: {
|
|
46
|
+
code: 'UPLOAD.PART_SIZE_MISMATCH',
|
|
47
|
+
publicMessage: '分片实际大小与预期不一致',
|
|
48
|
+
status: HttpStatus.UNPROCESSABLE_ENTITY,
|
|
49
|
+
},
|
|
50
|
+
UNSUPPORTED_CONTENT_ENCODING: {
|
|
51
|
+
code: 'UPLOAD.UNSUPPORTED_CONTENT_ENCODING',
|
|
52
|
+
publicMessage: '分片请求不支持压缩编码',
|
|
53
|
+
status: HttpStatus.UNSUPPORTED_MEDIA_TYPE,
|
|
54
|
+
},
|
|
55
|
+
PARTS_INCOMPLETE: {
|
|
56
|
+
code: 'UPLOAD.PARTS_INCOMPLETE',
|
|
57
|
+
publicMessage: '分片尚未全部上传或大小不正确',
|
|
58
|
+
status: HttpStatus.CONFLICT,
|
|
59
|
+
},
|
|
60
|
+
PARTS_STILL_ACTIVE: {
|
|
61
|
+
code: 'UPLOAD.PARTS_STILL_ACTIVE',
|
|
62
|
+
publicMessage: '仍有分片正在上传,请稍后重试',
|
|
63
|
+
status: HttpStatus.CONFLICT,
|
|
64
|
+
},
|
|
65
|
+
PART_UPLOAD_ABORTED: {
|
|
66
|
+
code: 'UPLOAD.PART_UPLOAD_ABORTED',
|
|
67
|
+
publicMessage: '分片上传已取消',
|
|
68
|
+
status: HttpStatus.REQUEST_TIMEOUT,
|
|
69
|
+
},
|
|
70
|
+
STORAGE_UPLOAD_MISSING: {
|
|
71
|
+
code: 'UPLOAD.STORAGE_UPLOAD_MISSING',
|
|
72
|
+
publicMessage: '存储中的上传会话已失效,请重新开始',
|
|
73
|
+
status: HttpStatus.GONE,
|
|
74
|
+
},
|
|
75
|
+
STORAGE_UNAVAILABLE: {
|
|
76
|
+
code: 'UPLOAD.STORAGE_UNAVAILABLE',
|
|
77
|
+
publicMessage: '文件存储服务暂时不可用,请稍后重试',
|
|
78
|
+
status: HttpStatus.SERVICE_UNAVAILABLE,
|
|
79
|
+
},
|
|
80
|
+
} as const satisfies Record<string, AppErrorDefinition>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common'
|
|
2
|
+
import { MinioStorageAdapter } from './storage/minio-storage.adapter'
|
|
3
|
+
import { STORAGE_PORT } from './storage/storage.port'
|
|
4
|
+
import { UploadController } from './upload.controller'
|
|
5
|
+
import { UploadRepository } from './upload.reponsitory'
|
|
6
|
+
import { UploadService } from './upload.service'
|
|
7
|
+
|
|
8
|
+
@Module({
|
|
9
|
+
controllers: [UploadController],
|
|
10
|
+
providers: [
|
|
11
|
+
UploadRepository,
|
|
12
|
+
UploadService,
|
|
13
|
+
MinioStorageAdapter,
|
|
14
|
+
{
|
|
15
|
+
provide: STORAGE_PORT,
|
|
16
|
+
useExisting: MinioStorageAdapter,
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
})
|
|
20
|
+
export class UploadModule {}
|
package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.reponsitory.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { DRIZZLE, type DrizzleDB } from '@/database/db.module'
|
|
2
|
+
import { uploadSessions } from '@/database/schema'
|
|
3
|
+
import { Inject, Injectable } from '@nestjs/common'
|
|
4
|
+
import { and, eq, inArray } from 'drizzle-orm'
|
|
5
|
+
|
|
6
|
+
export type UploadSession = typeof uploadSessions.$inferSelect
|
|
7
|
+
export type CreateUploadSession = typeof uploadSessions.$inferInsert
|
|
8
|
+
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class UploadRepository {
|
|
11
|
+
constructor(@Inject(DRIZZLE) private readonly db: DrizzleDB) {}
|
|
12
|
+
|
|
13
|
+
async create(input: CreateUploadSession): Promise<UploadSession | null> {
|
|
14
|
+
const [session] = await this.db
|
|
15
|
+
.insert(uploadSessions)
|
|
16
|
+
.values(input)
|
|
17
|
+
.onConflictDoNothing({
|
|
18
|
+
target: [uploadSessions.ownerId, uploadSessions.clientUploadId],
|
|
19
|
+
})
|
|
20
|
+
.returning()
|
|
21
|
+
|
|
22
|
+
return session ?? null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async findById(ownerId: string, uploadSessionId: string): Promise<UploadSession | null> {
|
|
26
|
+
const [session] = await this.db
|
|
27
|
+
.select()
|
|
28
|
+
.from(uploadSessions)
|
|
29
|
+
.where(and(eq(uploadSessions.id, uploadSessionId), eq(uploadSessions.ownerId, ownerId)))
|
|
30
|
+
.limit(1)
|
|
31
|
+
return session ?? null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async findByClientUploadId(
|
|
35
|
+
ownerId: string,
|
|
36
|
+
clientUploadId: string,
|
|
37
|
+
): Promise<UploadSession | null> {
|
|
38
|
+
const [session] = await this.db
|
|
39
|
+
.select()
|
|
40
|
+
.from(uploadSessions)
|
|
41
|
+
.where(
|
|
42
|
+
and(eq(uploadSessions.ownerId, ownerId), eq(uploadSessions.clientUploadId, clientUploadId)),
|
|
43
|
+
)
|
|
44
|
+
.limit(1)
|
|
45
|
+
|
|
46
|
+
return session ?? null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async claimCompleting(ownerId: string, uploadSessionId: string): Promise<UploadSession | null> {
|
|
50
|
+
const [session] = await this.db
|
|
51
|
+
.update(uploadSessions)
|
|
52
|
+
.set({
|
|
53
|
+
status: 'completing',
|
|
54
|
+
updatedAt: new Date(),
|
|
55
|
+
})
|
|
56
|
+
.where(
|
|
57
|
+
and(
|
|
58
|
+
eq(uploadSessions.id, uploadSessionId),
|
|
59
|
+
eq(uploadSessions.ownerId, ownerId),
|
|
60
|
+
eq(uploadSessions.status, 'uploading'),
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
.returning()
|
|
64
|
+
|
|
65
|
+
return session ?? null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async markCompleted(
|
|
69
|
+
ownerId: string,
|
|
70
|
+
uploadSessionId: string,
|
|
71
|
+
objectEtag: string | null,
|
|
72
|
+
): Promise<UploadSession | null> {
|
|
73
|
+
const now = new Date()
|
|
74
|
+
const [session] = await this.db
|
|
75
|
+
.update(uploadSessions)
|
|
76
|
+
.set({
|
|
77
|
+
status: 'completed',
|
|
78
|
+
objectEtag,
|
|
79
|
+
completedAt: now,
|
|
80
|
+
updatedAt: now,
|
|
81
|
+
})
|
|
82
|
+
.where(
|
|
83
|
+
and(
|
|
84
|
+
eq(uploadSessions.id, uploadSessionId),
|
|
85
|
+
eq(uploadSessions.ownerId, ownerId),
|
|
86
|
+
eq(uploadSessions.status, 'completing'),
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
.returning()
|
|
90
|
+
|
|
91
|
+
return session ?? null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async claimAborting(ownerId: string, uploadSessionId: string): Promise<UploadSession | null> {
|
|
95
|
+
const [session] = await this.db
|
|
96
|
+
.update(uploadSessions)
|
|
97
|
+
.set({
|
|
98
|
+
status: 'aborting',
|
|
99
|
+
updatedAt: new Date(),
|
|
100
|
+
})
|
|
101
|
+
.where(
|
|
102
|
+
and(
|
|
103
|
+
eq(uploadSessions.id, uploadSessionId),
|
|
104
|
+
eq(uploadSessions.ownerId, ownerId),
|
|
105
|
+
inArray(uploadSessions.status, ['uploading', 'aborting', 'expired']),
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
.returning()
|
|
109
|
+
|
|
110
|
+
return session ?? null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async markAborted(ownerId: string, uploadSessionId: string): Promise<UploadSession | null> {
|
|
114
|
+
const [session] = await this.db
|
|
115
|
+
.update(uploadSessions)
|
|
116
|
+
.set({
|
|
117
|
+
status: 'aborted',
|
|
118
|
+
updatedAt: new Date(),
|
|
119
|
+
})
|
|
120
|
+
.where(
|
|
121
|
+
and(
|
|
122
|
+
eq(uploadSessions.id, uploadSessionId),
|
|
123
|
+
eq(uploadSessions.ownerId, ownerId),
|
|
124
|
+
eq(uploadSessions.status, 'aborting'),
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
.returning()
|
|
128
|
+
|
|
129
|
+
return session ?? null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async markExpired(ownerId: string, uploadSessionId: string): Promise<UploadSession | null> {
|
|
133
|
+
const [session] = await this.db
|
|
134
|
+
.update(uploadSessions)
|
|
135
|
+
.set({
|
|
136
|
+
status: 'expired',
|
|
137
|
+
updatedAt: new Date(),
|
|
138
|
+
})
|
|
139
|
+
.where(
|
|
140
|
+
and(
|
|
141
|
+
eq(uploadSessions.id, uploadSessionId),
|
|
142
|
+
eq(uploadSessions.ownerId, ownerId),
|
|
143
|
+
eq(uploadSessions.status, 'uploading'),
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
.returning()
|
|
147
|
+
|
|
148
|
+
return session ?? null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async resetCompleting(ownerId: string, uploadSessionId: string): Promise<void> {
|
|
152
|
+
await this.db
|
|
153
|
+
.update(uploadSessions)
|
|
154
|
+
.set({
|
|
155
|
+
status: 'uploading',
|
|
156
|
+
updatedAt: new Date(),
|
|
157
|
+
})
|
|
158
|
+
.where(
|
|
159
|
+
and(
|
|
160
|
+
eq(uploadSessions.id, uploadSessionId),
|
|
161
|
+
eq(uploadSessions.ownerId, ownerId),
|
|
162
|
+
eq(uploadSessions.status, 'completing'),
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
}
|