Files
photo-live/services/api/src/uploads/dto.ts
T
浅念 a2a3ccd340 feat(api): idempotent upload session init with presigned credential
POST /v1/uploads (Task 18, A03): @@unique(userId, clientTaskId) idempotency
anchor — same params return the original session (201, no duplicate photo);
param drift (album/bytes/contentType/extension) -> 409 UPLOAD_SESSION_CONFLICT.
photos.objectKey is set to the FINAL originals key at init while the session
holds the staging key; IDs come from an app-side cuid-like generator so both
self-referencing keys are written in one transaction. Credential is a real
MinIO presigned PUT (SIGNED_URL_TTL_SECONDS, lazily read). Permission order:
album invisible 404 -> member 403 -> uploadEnabled 409 -> size/ext 400 ->
category ownership 400. No audit on init (audit anchors on complete).

Pre-step for Task 19: QUEUE_PHOTO_PROCESS contract constant + lazy-safe
BullMQ queue producer (Redis down at boot must not crash the app; enqueue
failure only logs — sweep compensates). e2e reset now also wipes the MinIO
photo-live/dev prefix; jest maps @photo/storage to TS source.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 22:42:13 +08:00

51 lines
1.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// services/api/src/uploads/dto.ts —— 上传端点入参 DTO(Task 18)。校验约束对齐
// 契约 UploadInitRequest:
// · contentType 限白名单枚举(image/heic 暂定,G2 真机验证);
// · bytes ≥ 1(契约 minimum);上限 UPLOAD_MAX_BYTES 是环境相关量,逐请求
// 惰性读取,故在服务层校验(同 filename 扩展名白名单与 categoryId 属主校验);
// · clientTaskId/filename/albumId/categoryId 列宽对齐 db-cn schema
// (VarChar 128/255/64/64)。
import {
IsIn,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
MaxLength,
Min,
} from 'class-validator';
import { UPLOAD_CONTENT_TYPES, type UploadContentType } from './support';
export class UploadInitRequestDto {
@IsString()
@IsNotEmpty()
@MaxLength(64)
albumId!: string;
/** 缺省为相册默认分类(P1a 无默认分类概念,落 null)。 */
@IsOptional()
@IsString()
@IsNotEmpty()
@MaxLength(64)
categoryId?: string;
/** 客户端任务幂等键(A03:同任务重试复用会话与照片 ID)。 */
@IsString()
@IsNotEmpty()
@MaxLength(128)
clientTaskId!: string;
/** 原始文件名,仅元数据;扩展名白名单校验在服务层(对象键后缀派生)。 */
@IsString()
@IsNotEmpty()
@MaxLength(255)
filename!: string;
@IsInt()
@Min(1)
bytes!: number;
@IsIn(UPLOAD_CONTENT_TYPES)
contentType!: UploadContentType;
}