create-bubbles 0.1.25 → 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 +3 -9
- package/package.json +1 -1
- 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/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/auth/dto/auth-repsponse.dto.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createZodDto } from 'nestjs-zod'
|
|
|
2
2
|
import z from 'zod'
|
|
3
3
|
|
|
4
4
|
const authUserSchema = z.object({
|
|
5
|
-
id: z.
|
|
5
|
+
id: z.uuid(),
|
|
6
6
|
account: z.string(),
|
|
7
7
|
name: z.string(),
|
|
8
8
|
})
|
|
@@ -14,7 +14,7 @@ export class LoginResultDto extends createZodDto(
|
|
|
14
14
|
accessToken: z.string().min(1),
|
|
15
15
|
tokenType: z.literal('Bearer'),
|
|
16
16
|
idleExpiresIn: z.number().int().positive(),
|
|
17
|
-
absoluteExpiresAt: z.
|
|
17
|
+
absoluteExpiresAt: z.iso.datetime(),
|
|
18
18
|
}),
|
|
19
19
|
) {}
|
|
20
20
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { BullRegistrar } from '@nestjs/bullmq'
|
|
2
|
+
import { Injectable, type OnApplicationBootstrap } from '@nestjs/common'
|
|
3
|
+
import { ConfigService } from '@nestjs/config'
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class QueueWorkerBootstrap implements OnApplicationBootstrap {
|
|
7
|
+
private registered = false
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
private readonly config: ConfigService,
|
|
11
|
+
private readonly registrar: BullRegistrar,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
onApplicationBootstrap() {
|
|
15
|
+
const workerEnabled = this.config.getOrThrow<boolean>('queue.workerEnabled')
|
|
16
|
+
|
|
17
|
+
if (!workerEnabled || this.registered) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.registrar.register()
|
|
22
|
+
this.registered = true
|
|
23
|
+
}
|
|
24
|
+
}
|
package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.constants.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BullMQ 队列名称。
|
|
3
|
+
*
|
|
4
|
+
* 在 BullModule.registerQueue 注册、@InjectQueue 注入与 @Processor 绑定时共用,
|
|
5
|
+
* 同时作为契约层的队列标识(见 EnqueuedTask.queueName)。
|
|
6
|
+
*/
|
|
7
|
+
export const TASK_QUEUE_NAME = 'server-tasks'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Worker 并发数:单个 Processor 实例同时处理的任务数上限。
|
|
11
|
+
*/
|
|
12
|
+
export const TASK_QUEUE_CONCURRENCY = 5
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 任务名称注册表(全部任务的唯一来源)。
|
|
16
|
+
*
|
|
17
|
+
* 命名遵循 `域.动作.版本` 格式(如 `system.queue-smoke.v1`)。
|
|
18
|
+
* 新增任务时在此登记,并需在 contracts 中同步补充对应的 payload 契约
|
|
19
|
+
* (TaskPayloadMap 与 TASK_PAYLOAD_SCHEMAS)。
|
|
20
|
+
*/
|
|
21
|
+
export const TASK_JOB_NAMES = {
|
|
22
|
+
/** 队列连通性冒烟测试任务。 */
|
|
23
|
+
QUEUE_SMOKE: 'system.queue-smoke.v1',
|
|
24
|
+
} as const
|
package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.contracts.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { TASK_JOB_NAMES, TASK_QUEUE_NAME } from './task-queue.constants'
|
|
3
|
+
|
|
4
|
+
export const QueueSmokePayloadSchema = z.object({
|
|
5
|
+
marker: z.string().min(1).max(100),
|
|
6
|
+
enqueuedAt: z.iso.datetime(),
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export type TaskName = (typeof TASK_JOB_NAMES)[keyof typeof TASK_JOB_NAMES]
|
|
10
|
+
|
|
11
|
+
export interface TaskPayloadMap {
|
|
12
|
+
[TASK_JOB_NAMES.QUEUE_SMOKE]: z.infer<typeof QueueSmokePayloadSchema>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const TASK_PAYLOAD_SCHEMAS = {
|
|
16
|
+
[TASK_JOB_NAMES.QUEUE_SMOKE]: QueueSmokePayloadSchema,
|
|
17
|
+
} satisfies Record<TaskName, z.ZodType>
|
|
18
|
+
|
|
19
|
+
export function parseTaskPayload<Name extends TaskName>(
|
|
20
|
+
name: Name,
|
|
21
|
+
payload: unknown,
|
|
22
|
+
): TaskPayloadMap[Name] {
|
|
23
|
+
return TASK_PAYLOAD_SCHEMAS[name].parse(payload) as TaskPayloadMap[Name]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface EnqueueTaskOptions {
|
|
27
|
+
jobId?: string
|
|
28
|
+
delayMs?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface EnqueuedTask<Name extends TaskName = TaskName> {
|
|
32
|
+
queueName: typeof TASK_QUEUE_NAME
|
|
33
|
+
jobId: string
|
|
34
|
+
name: Name
|
|
35
|
+
}
|
package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.module.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { BullModule } from '@nestjs/bullmq'
|
|
2
|
+
import { Module } from '@nestjs/common'
|
|
3
|
+
import { ConfigModule, ConfigService } from '@nestjs/config'
|
|
4
|
+
import type { QueueConfig } from '@/config/queue.config'
|
|
5
|
+
import { QueueWorkerBootstrap } from './queue-worker.bootstrap'
|
|
6
|
+
import { TASK_QUEUE_NAME } from './task-queue.constants'
|
|
7
|
+
import { TaskQueueProcessor } from './task-queue.processor'
|
|
8
|
+
import { TaskQueueService } from './task-queue.service'
|
|
9
|
+
|
|
10
|
+
@Module({
|
|
11
|
+
imports: [
|
|
12
|
+
BullModule.forRootAsync({
|
|
13
|
+
imports: [ConfigModule],
|
|
14
|
+
inject: [ConfigService],
|
|
15
|
+
useFactory: (config: ConfigService) => {
|
|
16
|
+
const queue = config.getOrThrow<QueueConfig>('queue')
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
connection: {
|
|
20
|
+
host: queue.host,
|
|
21
|
+
port: queue.port,
|
|
22
|
+
username: queue.username,
|
|
23
|
+
password: queue.password,
|
|
24
|
+
db: queue.db,
|
|
25
|
+
connectTimeout: 10_000,
|
|
26
|
+
},
|
|
27
|
+
prefix: queue.prefix,
|
|
28
|
+
defaultJobOptions: {
|
|
29
|
+
attempts: queue.defaultAttempts,
|
|
30
|
+
backoff: {
|
|
31
|
+
type: 'exponential',
|
|
32
|
+
delay: queue.backoffDelayMs,
|
|
33
|
+
},
|
|
34
|
+
removeOnComplete: {
|
|
35
|
+
age: queue.completedAgeSeconds,
|
|
36
|
+
count: queue.completedCount,
|
|
37
|
+
},
|
|
38
|
+
removeOnFail: {
|
|
39
|
+
age: queue.failedAgeSeconds,
|
|
40
|
+
count: queue.failedCount,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
extraOptions: {
|
|
46
|
+
manualRegistration: true,
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
BullModule.registerQueue({
|
|
50
|
+
name: TASK_QUEUE_NAME,
|
|
51
|
+
}),
|
|
52
|
+
],
|
|
53
|
+
providers: [QueueWorkerBootstrap, TaskQueueProcessor, TaskQueueService],
|
|
54
|
+
exports: [TaskQueueService],
|
|
55
|
+
})
|
|
56
|
+
export class TaskQueueModule {}
|
package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.processor.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { OnWorkerEvent, Processor, WorkerHost } from '@nestjs/bullmq'
|
|
2
|
+
import { Logger } from '@nestjs/common'
|
|
3
|
+
import { type Job, UnrecoverableError } from 'bullmq'
|
|
4
|
+
import { TASK_JOB_NAMES, TASK_QUEUE_CONCURRENCY, TASK_QUEUE_NAME } from './task-queue.constants'
|
|
5
|
+
import { QueueSmokePayloadSchema } from './task-queue.contracts'
|
|
6
|
+
|
|
7
|
+
@Processor(TASK_QUEUE_NAME, { concurrency: TASK_QUEUE_CONCURRENCY })
|
|
8
|
+
export class TaskQueueProcessor extends WorkerHost {
|
|
9
|
+
private readonly logger = new Logger(TaskQueueProcessor.name)
|
|
10
|
+
|
|
11
|
+
async process(job: Job): Promise<unknown> {
|
|
12
|
+
switch (job.name) {
|
|
13
|
+
case TASK_JOB_NAMES.QUEUE_SMOKE: {
|
|
14
|
+
const parsed = QueueSmokePayloadSchema.safeParse(job.data)
|
|
15
|
+
|
|
16
|
+
if (!parsed.success) {
|
|
17
|
+
throw new UnrecoverableError('Invalid payload for ' + job.name)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
marker: parsed.data.marker,
|
|
22
|
+
processedAt: new Date().toISOString(),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
default:
|
|
27
|
+
throw new UnrecoverableError('Unsupported task name: ' + job.name)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@OnWorkerEvent('completed')
|
|
32
|
+
onCompleted(job: Job) {
|
|
33
|
+
this.logger.log({
|
|
34
|
+
event: 'queue_job_completed',
|
|
35
|
+
queueName: TASK_QUEUE_NAME,
|
|
36
|
+
jobId: job.id,
|
|
37
|
+
jobName: job.name,
|
|
38
|
+
attemptsMade: job.attemptsMade,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@OnWorkerEvent('failed')
|
|
43
|
+
onFailed(job: Job | undefined, error: Error) {
|
|
44
|
+
this.logger.error({
|
|
45
|
+
event: 'queue_job_failed',
|
|
46
|
+
queueName: TASK_QUEUE_NAME,
|
|
47
|
+
jobId: job?.id,
|
|
48
|
+
jobName: job?.name,
|
|
49
|
+
attemptsMade: job?.attemptsMade,
|
|
50
|
+
errorName: error.name,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@OnWorkerEvent('error')
|
|
55
|
+
onWorkerError(error: Error) {
|
|
56
|
+
this.logger.error({
|
|
57
|
+
event: 'queue_worker_error',
|
|
58
|
+
queueName: TASK_QUEUE_NAME,
|
|
59
|
+
errorName: error.name,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.service.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { InjectQueue } from '@nestjs/bullmq'
|
|
2
|
+
import { Injectable } from '@nestjs/common'
|
|
3
|
+
import type { JobsOptions, Queue } from 'bullmq'
|
|
4
|
+
import { TASK_QUEUE_NAME } from './task-queue.constants'
|
|
5
|
+
import {
|
|
6
|
+
type EnqueuedTask,
|
|
7
|
+
type EnqueueTaskOptions,
|
|
8
|
+
parseTaskPayload,
|
|
9
|
+
type TaskName,
|
|
10
|
+
type TaskPayloadMap,
|
|
11
|
+
} from './task-queue.contracts'
|
|
12
|
+
|
|
13
|
+
@Injectable()
|
|
14
|
+
export class TaskQueueService {
|
|
15
|
+
constructor(
|
|
16
|
+
@InjectQueue(TASK_QUEUE_NAME)
|
|
17
|
+
private readonly queue: Queue,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
async enqueue<Name extends TaskName>(
|
|
21
|
+
name: Name,
|
|
22
|
+
payload: TaskPayloadMap[Name],
|
|
23
|
+
options: EnqueueTaskOptions = {},
|
|
24
|
+
): Promise<EnqueuedTask<Name>> {
|
|
25
|
+
this.validateOptions(options)
|
|
26
|
+
|
|
27
|
+
const data = parseTaskPayload(name, payload)
|
|
28
|
+
const jobOptions: JobsOptions = {
|
|
29
|
+
...(options.jobId === undefined ? {} : { jobId: options.jobId }),
|
|
30
|
+
...(options.delayMs === undefined ? {} : { delay: options.delayMs }),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const job = await this.queue.add(name, data, jobOptions)
|
|
34
|
+
|
|
35
|
+
if (job.id === undefined) {
|
|
36
|
+
throw new Error('BullMQ returned a Job without an id')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
queueName: TASK_QUEUE_NAME,
|
|
41
|
+
jobId: job.id,
|
|
42
|
+
name,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private validateOptions(options: EnqueueTaskOptions) {
|
|
47
|
+
if (options.jobId?.includes(':')) {
|
|
48
|
+
throw new Error('BullMQ jobId must not contain a colon')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
options.delayMs !== undefined &&
|
|
53
|
+
(!Number.isSafeInteger(options.delayMs) || options.delayMs < 0)
|
|
54
|
+
) {
|
|
55
|
+
throw new Error('delayMs must be a non-negative safe integer')
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { COMMON_ERRORS } from '@/common/error/common.error'
|
|
2
|
+
import { AUTH_ERRORS } from '@/modules/auth/auth.errors'
|
|
3
|
+
import { describe, expect, it } from 'vite-plus/test'
|
|
4
|
+
import { UPLOAD_ERRORS } from '../upload/upload.errors'
|
|
5
|
+
|
|
6
|
+
const definitions = [
|
|
7
|
+
...Object.values(COMMON_ERRORS),
|
|
8
|
+
...Object.values(AUTH_ERRORS),
|
|
9
|
+
...Object.values(UPLOAD_ERRORS),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
describe('error catelog', () => {
|
|
13
|
+
it('uses valid and globally unique error codes', () => {
|
|
14
|
+
const codes = definitions.map((definition) => definition.code)
|
|
15
|
+
expect(new Set(codes).size).toBe(codes.length)
|
|
16
|
+
|
|
17
|
+
for (const definition of definitions) {
|
|
18
|
+
expect(definition.code).toMatch(/^[A-Z][A-Z0-9_]*(?:\.[A-Z][A-Z0-9_]*)+$/)
|
|
19
|
+
expect(definition.code.length).toBeLessThanOrEqual(80)
|
|
20
|
+
expect(definition.status).toBeGreaterThanOrEqual(400)
|
|
21
|
+
expect(definition.status).toBeLessThanOrEqual(599)
|
|
22
|
+
expect(definition.publicMessage.trim()).not.toBe('')
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('keeps bearerChallenge and HTTP 401 in sync', () => {
|
|
27
|
+
for (const definition of definitions) {
|
|
28
|
+
const hasBearerChallenge =
|
|
29
|
+
'bearerChallenge' in definition && definition.bearerChallenge === true
|
|
30
|
+
|
|
31
|
+
expect(hasBearerChallenge).toBe(definition.status === 401)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createZodDto } from 'nestjs-zod'
|
|
2
|
+
import z from 'zod'
|
|
3
|
+
|
|
4
|
+
export const initiateMultipartUploadSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
clientUploadId: z.uuid(),
|
|
7
|
+
fileName: z
|
|
8
|
+
.string()
|
|
9
|
+
.trim()
|
|
10
|
+
.min(1)
|
|
11
|
+
.max(255)
|
|
12
|
+
.refine((value) => !/[\u0000-\u001F\u007F]/u.test(value), 'fileName 不能包含控制字符'),
|
|
13
|
+
fileSize: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
14
|
+
contentType: z
|
|
15
|
+
.string()
|
|
16
|
+
.trim()
|
|
17
|
+
.min(1)
|
|
18
|
+
.max(255)
|
|
19
|
+
.regex(
|
|
20
|
+
/^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$/,
|
|
21
|
+
'contentType 必须是安全的 MIME media type,例如 image/png',
|
|
22
|
+
),
|
|
23
|
+
})
|
|
24
|
+
.strict()
|
|
25
|
+
|
|
26
|
+
export class InitiateMultipartUploadDto extends createZodDto(initiateMultipartUploadSchema) {}
|
package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/dto/upload-params.dto.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createZodDto } from 'nestjs-zod'
|
|
2
|
+
import z from 'zod'
|
|
3
|
+
import { UPLOAD_MAX_PARTS } from '../upload.constants'
|
|
4
|
+
|
|
5
|
+
export const uploadSessionParamsSchema = z.object({
|
|
6
|
+
uploadSessionId: z.uuid(),
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export const uploadPartParamsSchema = uploadSessionParamsSchema.extend({
|
|
10
|
+
partNumber: z.coerce.number().int().min(1).max(UPLOAD_MAX_PARTS),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export class UploadSessionParamsDto extends createZodDto(uploadPartParamsSchema) {}
|
|
14
|
+
|
|
15
|
+
export class UploadPartParamsDto extends createZodDto(uploadPartParamsSchema) {}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { TransformCallback } from 'node:stream'
|
|
2
|
+
import { Transform } from 'stream'
|
|
3
|
+
|
|
4
|
+
export class ExactSizeError extends Error {
|
|
5
|
+
constructor(
|
|
6
|
+
readonly expectedBytes: number,
|
|
7
|
+
readonly receivedBytes: number,
|
|
8
|
+
) {
|
|
9
|
+
super(`Expected ${expectedBytes} bytes, recevied ${receivedBytes}`)
|
|
10
|
+
this.name = ExactSizeError.name
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ExactSizeTransform extends Transform {
|
|
15
|
+
receivedBytes = 0
|
|
16
|
+
|
|
17
|
+
constructor(private readonly expectedBytes: number) {
|
|
18
|
+
super()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override _transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback): void {
|
|
22
|
+
const byteLength = Buffer.isBuffer(chunk)
|
|
23
|
+
? chunk.length
|
|
24
|
+
: Buffer.byteLength(String(chunk), encoding)
|
|
25
|
+
this.receivedBytes += byteLength
|
|
26
|
+
|
|
27
|
+
if (this.receivedBytes > this.expectedBytes) {
|
|
28
|
+
callback(new ExactSizeError(this.expectedBytes, this.receivedBytes))
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
callback(null, chunk)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 出场质检
|
|
37
|
+
* @param callback
|
|
38
|
+
* @returns
|
|
39
|
+
*/
|
|
40
|
+
override _flush(callback: TransformCallback): void {
|
|
41
|
+
if (this.receivedBytes !== this.expectedBytes) {
|
|
42
|
+
callback(new ExactSizeError(this.expectedBytes, this.receivedBytes))
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
callback()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function findExactSizeError(cause: unknown): ExactSizeError | null {
|
|
51
|
+
let current = cause
|
|
52
|
+
|
|
53
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
54
|
+
if (current instanceof ExactSizeError) {
|
|
55
|
+
return current
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof current !== 'object' || current === null || !('cause' in current)) {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
current = current.cause
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AbortMultipartUploadCommand,
|
|
3
|
+
CompleteMultipartUploadCommand,
|
|
4
|
+
CreateMultipartUploadCommand,
|
|
5
|
+
HeadBucketCommand,
|
|
6
|
+
HeadObjectCommand,
|
|
7
|
+
ListPartsCommand,
|
|
8
|
+
S3Client,
|
|
9
|
+
UploadPartCommand,
|
|
10
|
+
} from '@aws-sdk/client-s3'
|
|
11
|
+
import { Injectable, OnModuleDestroy } from '@nestjs/common'
|
|
12
|
+
import { ConfigService } from '@nestjs/config'
|
|
13
|
+
import {
|
|
14
|
+
CreateMultipartInput,
|
|
15
|
+
MultipartIdentity,
|
|
16
|
+
StorageMultipartNotFoundError,
|
|
17
|
+
StorageObject,
|
|
18
|
+
StoragePart,
|
|
19
|
+
StoragePort,
|
|
20
|
+
UploadPartInput,
|
|
21
|
+
} from './storage.port'
|
|
22
|
+
|
|
23
|
+
function hasErrorName(cause: unknown, expected: string): boolean {
|
|
24
|
+
if (typeof cause !== 'object' || cause === null) {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
const record = cause as { name?: unknown; Code?: unknown }
|
|
28
|
+
return record.name === expected || record.Code === expected
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function throwMappedMultipartError(cause: unknown): never {
|
|
32
|
+
if (hasErrorName(cause, 'NoSuchUpload')) {
|
|
33
|
+
throw new StorageMultipartNotFoundError(cause)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
throw cause
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Injectable()
|
|
40
|
+
export class MinioStorageAdapter implements StoragePort, OnModuleDestroy {
|
|
41
|
+
private readonly client: S3Client
|
|
42
|
+
private readonly bucket: string
|
|
43
|
+
|
|
44
|
+
constructor(config: ConfigService) {
|
|
45
|
+
this.bucket = config.getOrThrow<string>('storage.bucket')
|
|
46
|
+
this.client = new S3Client({
|
|
47
|
+
endpoint: config.getOrThrow<string>('storage.endpoint'),
|
|
48
|
+
region: config.getOrThrow<string>('storage.region'),
|
|
49
|
+
credentials: {
|
|
50
|
+
accessKeyId: config.getOrThrow<string>('storage.accessKeyId'),
|
|
51
|
+
secretAccessKey: config.getOrThrow<string>('storage.secretAccessKey'),
|
|
52
|
+
},
|
|
53
|
+
forcePathStyle: config.getOrThrow<boolean>('storage.forcePathStyle'),
|
|
54
|
+
// UploadPart 的 Body 是不可回放流,禁止 SDK 在后端自动重放。
|
|
55
|
+
maxAttempts: 1,
|
|
56
|
+
requestChecksumCalculation: 'WHEN_REQUIRED',
|
|
57
|
+
responseChecksumValidation: 'WHEN_REQUIRED',
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private async assertBucketAvailable(): Promise<void> {
|
|
62
|
+
await this.client.send(
|
|
63
|
+
new HeadBucketCommand({
|
|
64
|
+
Bucket: this.bucket,
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onModuleDestroy() {
|
|
70
|
+
this.client.destroy()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async onModuleInit() {
|
|
74
|
+
await this.assertBucketAvailable()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async createMultipartUpload(input: CreateMultipartInput) {
|
|
78
|
+
const response = await this.client.send(
|
|
79
|
+
new CreateMultipartUploadCommand({
|
|
80
|
+
Bucket: input.bucket,
|
|
81
|
+
Key: input.objectKey,
|
|
82
|
+
ContentType: input.contentType,
|
|
83
|
+
Metadata: input.metadata,
|
|
84
|
+
}),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if (!response.UploadId) {
|
|
88
|
+
throw new Error('Storage did not return an UploadId')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
storageUploadId: response.UploadId,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async uploadPart(input: UploadPartInput) {
|
|
97
|
+
try {
|
|
98
|
+
const response = await this.client.send(
|
|
99
|
+
new UploadPartCommand({
|
|
100
|
+
Bucket: input.bucket,
|
|
101
|
+
Key: input.objectKey,
|
|
102
|
+
UploadId: input.storageUploadId,
|
|
103
|
+
PartNumber: input.partNumber,
|
|
104
|
+
Body: input.body,
|
|
105
|
+
ContentLength: input.contentLength,
|
|
106
|
+
}),
|
|
107
|
+
{
|
|
108
|
+
abortSignal: input.abortSignal,
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if (!response.ETag) {
|
|
113
|
+
throw new Error('Storage did not return an ETag')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
etag: response.ETag,
|
|
118
|
+
}
|
|
119
|
+
} catch (cause: unknown) {
|
|
120
|
+
throwMappedMultipartError(cause)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async listParts(input: MultipartIdentity): Promise<StoragePart[]> {
|
|
125
|
+
const parts: StoragePart[] = []
|
|
126
|
+
let partNumberMarker: string | undefined
|
|
127
|
+
try {
|
|
128
|
+
for (;;) {
|
|
129
|
+
const response = await this.client.send(
|
|
130
|
+
new ListPartsCommand({
|
|
131
|
+
Bucket: input.bucket,
|
|
132
|
+
Key: input.objectKey,
|
|
133
|
+
UploadId: input.storageUploadId,
|
|
134
|
+
PartNumberMarker: partNumberMarker,
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
for (const part of response.Parts ?? []) {
|
|
139
|
+
if (part.PartNumber === undefined || part.ETag === undefined || part.Size === undefined) {
|
|
140
|
+
throw new Error('Storage returned an incomplete part record')
|
|
141
|
+
}
|
|
142
|
+
parts.push({
|
|
143
|
+
partNumber: part.PartNumber,
|
|
144
|
+
etag: part.ETag,
|
|
145
|
+
size: part.Size,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!response.IsTruncated) {
|
|
150
|
+
break
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const nextMarker = response.NextPartNumberMarker
|
|
154
|
+
if (!nextMarker || nextMarker === partNumberMarker) {
|
|
155
|
+
throw new Error('Storage returned an invalid ListParts cursor')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
partNumberMarker = nextMarker
|
|
159
|
+
}
|
|
160
|
+
} catch (cause: unknown) {
|
|
161
|
+
throwMappedMultipartError(cause)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return parts.sort((left, right) => left.partNumber - right.partNumber)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async completeMultipartUpload(input: MultipartIdentity & { parts: readonly StoragePart[] }) {
|
|
168
|
+
try {
|
|
169
|
+
const response = await this.client.send(
|
|
170
|
+
new CompleteMultipartUploadCommand({
|
|
171
|
+
Bucket: input.bucket,
|
|
172
|
+
Key: input.objectKey,
|
|
173
|
+
UploadId: input.storageUploadId,
|
|
174
|
+
MultipartUpload: {
|
|
175
|
+
Parts: input.parts.map((part) => ({
|
|
176
|
+
PartNumber: part.partNumber,
|
|
177
|
+
ETag: part.etag,
|
|
178
|
+
})),
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
)
|
|
182
|
+
return {
|
|
183
|
+
etag: response.ETag ?? null,
|
|
184
|
+
}
|
|
185
|
+
} catch (cause: unknown) {
|
|
186
|
+
throwMappedMultipartError(cause)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async abortMultipartUpload(input: MultipartIdentity): Promise<void> {
|
|
191
|
+
try {
|
|
192
|
+
await this.client.send(
|
|
193
|
+
new AbortMultipartUploadCommand({
|
|
194
|
+
Bucket: input.bucket,
|
|
195
|
+
Key: input.objectKey,
|
|
196
|
+
UploadId: input.storageUploadId,
|
|
197
|
+
}),
|
|
198
|
+
)
|
|
199
|
+
} catch (cause: unknown) {
|
|
200
|
+
if (hasErrorName(cause, 'NoSuchUpload')) {
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
throw cause
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async headObject(input: { bucket: string; objectKey: string }): Promise<StorageObject | null> {
|
|
208
|
+
try {
|
|
209
|
+
const response = await this.client.send(
|
|
210
|
+
new HeadObjectCommand({
|
|
211
|
+
Bucket: input.bucket,
|
|
212
|
+
Key: input.objectKey,
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
return {
|
|
216
|
+
etag: response.ETag ?? null,
|
|
217
|
+
contentLength: response.ContentLength ?? null,
|
|
218
|
+
metadata: response.Metadata ?? {},
|
|
219
|
+
}
|
|
220
|
+
} catch (cause: unknown) {
|
|
221
|
+
if (hasErrorName(cause, 'NoSuchBucket')) {
|
|
222
|
+
throw cause
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (hasErrorName(cause, 'NoSuchKey')) {
|
|
226
|
+
return null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (hasErrorName(cause, 'NotFound')) {
|
|
230
|
+
// 某些 S3 兼容实现会把“对象不存在”和“Bucket 不存在”都表示成 404。
|
|
231
|
+
// 额外探测 Bucket;只有 Bucket 确实可用时,才把这次 404 当成对象不存在。
|
|
232
|
+
await this.assertBucketAvailable()
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
throw cause
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|