72 lines
2.9 KiB
TypeScript
72 lines
2.9 KiB
TypeScript
import type {
|
||
CreateMediaUploadIntentRequest,
|
||
MediaAccessUrl,
|
||
MediaAsset,
|
||
MediaUploadIntent,
|
||
} from '@atlas/contracts';
|
||
import { api } from '@/services/api';
|
||
|
||
/** 水印图片的用途与限制(docs/api/watermarks-and-assets.md §1) */
|
||
export const WATERMARK_IMAGE_ACCEPT = '.jpg,.jpeg,.png,.webp';
|
||
export const WATERMARK_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
|
||
|
||
/** 异步操作查询,契约见 docs/api/conventions.md §5(GET /api/operations/{operation_id}) */
|
||
export const mediaApi = {
|
||
createUploadIntent: (payload: CreateMediaUploadIntentRequest) =>
|
||
api.post<MediaUploadIntent>('/media/upload-intents', payload, {
|
||
headers: { 'Idempotency-Key': crypto.randomUUID() },
|
||
}),
|
||
|
||
completeUpload: (uploadId: string, checksumSha256: string) =>
|
||
api.post<{ asset: MediaAsset }>(
|
||
`/media/upload-intents/${uploadId}/complete`,
|
||
{ checksum_sha256: checksumSha256 },
|
||
{ headers: { 'Idempotency-Key': crypto.randomUUID() } }
|
||
),
|
||
|
||
/** 短时预览 URL:过期后重新请求,不能写回业务表或长期缓存 */
|
||
createPreviewUrl: (assetId: string) =>
|
||
api.post<MediaAccessUrl>(
|
||
`/media-assets/${assetId}/access-urls`,
|
||
{ purpose: 'preview' },
|
||
{ headers: { 'Idempotency-Key': crypto.randomUUID() } }
|
||
),
|
||
};
|
||
|
||
/** 浏览器端计算文件的 SHA-256 十六进制摘要(complete 校验用) */
|
||
export async function sha256Hex(file: File): Promise<string> {
|
||
const digest = await crypto.subtle.digest('SHA-256', await file.arrayBuffer());
|
||
return Array.from(new Uint8Array(digest))
|
||
.map((byte) => byte.toString(16).padStart(2, '0'))
|
||
.join('');
|
||
}
|
||
|
||
/**
|
||
* 图片水印素材三步直传(docs/api/watermarks-and-assets.md §1):
|
||
* 创建上传意图 → 按返回的 method/url/headers 原样 PUT 到对象存储 → 完成校验换取素材。
|
||
* 返回已通过校验的素材,可直接用于图片水印模板的 image_asset_id。
|
||
*/
|
||
export async function uploadWatermarkImage(file: File): Promise<MediaAsset> {
|
||
const checksumSha256 = await sha256Hex(file);
|
||
const intent = await mediaApi.createUploadIntent({
|
||
purpose: 'watermark_image',
|
||
filename: file.name,
|
||
content_type: file.type as CreateMediaUploadIntentRequest['content_type'],
|
||
byte_size: file.size,
|
||
checksum_sha256: checksumSha256,
|
||
});
|
||
|
||
// 必须以服务端返回的 method、URL 与 headers 原样上传,不能自行拼接对象路径
|
||
const putResponse = await fetch(intent.upload.url, {
|
||
method: intent.upload.method,
|
||
headers: intent.upload.headers,
|
||
body: file,
|
||
});
|
||
if (!putResponse.ok) {
|
||
throw new Error(`图片上传到对象存储失败(HTTP ${putResponse.status})`);
|
||
}
|
||
|
||
const completed = await mediaApi.completeUpload(intent.upload_id, checksumSha256);
|
||
return completed.asset;
|
||
}
|