feat(api): album asset upload chain
P1b Task 3: initAlbumAssetUpload / completeAlbumAssetUpload (tags
[assets]) over album_assets with in-row one-shot sessions.
- migration asset_state: album_assets + state (pending|effective),
expectedBytes, contentType (utf8mb4_0900_bin); pre-existing rows
backfilled to effective. Complete head-verifies bytes AND
contentType vs the init declaration (P1a Task 11 server-side
fallback), then conditionally flips pending->effective with the
winner writing the album.asset.add audit (resourceId=albumId,
metadata assetId/kind/locale). Effective rows re-complete
idempotently (re-signed URL, no re-verify, survives object
cleanup).
- object key is the final key (albums/{albumId}/assets/{assetId}.{ext});
the pending row is the logical staging - no copy step, keys embed
assetId so re-init can never overwrite another row's object.
- permissions: owner/admin only (collaborator 403, outsider 404);
uploadEnabled does NOT gate assets (switch governs photo uploads
per 4.9 - both endpoints carry no ALBUM_UPLOAD_DISABLED).
- public banner now locale-aware (req 4.7): effective rows resolved
as display-locale row -> global (locale NULL) row -> null, latest
wins per tier (same-kind re-upload replaces, old rows retained).
- e2e: test/assets.e2e-spec.ts (14 cases, real jpegs via sharp,
MinIO round-trips); public spec seed marked effective.
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,20 @@ export function originalsObjectKey(
|
||||
return `${OBJECT_KEY_ROOT}/albums/${albumId}/originals/${photoId}.${ext}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* assets 正式键(album_assets.objectKey,P1b Task 3)。与照片域不同,资产无
|
||||
* staging→originals 受控复制:键含 assetId(每次 init 新键,不存在覆盖他行
|
||||
* 对象的风险),行 state='pending' 期间公开查询不可见即「逻辑 staging」,
|
||||
* complete 核验通过置 effective 后同一键即最终键。
|
||||
*/
|
||||
export function assetsObjectKey(
|
||||
albumId: string,
|
||||
assetId: string,
|
||||
ext: string,
|
||||
): string {
|
||||
return `${OBJECT_KEY_ROOT}/albums/${albumId}/assets/${assetId}.${ext}`;
|
||||
}
|
||||
|
||||
/** 取对象键尾缀扩展名(幂等冲突比对用:filename 未持久化,以扩展名近似锚定)。 */
|
||||
export function extOfObjectKey(objectKey: string): string | null {
|
||||
const dot = objectKey.lastIndexOf('.');
|
||||
|
||||
Reference in New Issue
Block a user