feat(api-watermark): 水印接口优化
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { DeleteObjectCommand, type S3Client } from '@aws-sdk/client-s3';
|
||||
import type { Database } from '@atlas/database';
|
||||
|
||||
type CleanupCandidate = {
|
||||
id: string;
|
||||
bucket: string;
|
||||
object_key: string;
|
||||
};
|
||||
|
||||
export class MediaUploadCleanup {
|
||||
private running: Promise<void> | null = null;
|
||||
|
||||
public constructor(
|
||||
private readonly database: Database,
|
||||
private readonly s3: S3Client,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 上传 URL 到期并不代表浏览器一定上传过对象;DeleteObject 对不存在对象也安全,
|
||||
* 所以终态意图统一尝试删除。只在删除成功后写 object_cleaned_at,失败下轮重试。
|
||||
*/
|
||||
public async run() {
|
||||
if (this.running) return this.running;
|
||||
this.running = this.runOnce().finally(() => {
|
||||
this.running = null;
|
||||
});
|
||||
return this.running;
|
||||
}
|
||||
|
||||
private async runOnce() {
|
||||
const workspaces = await this.database.query<{ id: string }>(
|
||||
'SELECT id FROM atlas.workspaces',
|
||||
);
|
||||
let expired = 0;
|
||||
let cleaned = 0;
|
||||
for (const workspace of workspaces.rows) {
|
||||
expired += await this.expirePending(workspace.id);
|
||||
const candidates = await this.claimCandidates(workspace.id);
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
await this.s3.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: candidate.bucket,
|
||||
Key: candidate.object_key,
|
||||
}),
|
||||
);
|
||||
await this.markCleaned(workspace.id, candidate.id);
|
||||
cleaned += 1;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'media_upload_cleanup_failed',
|
||||
JSON.stringify({
|
||||
workspace_id: workspace.id,
|
||||
upload_id: candidate.id,
|
||||
reason:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'unknown_error',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (expired || cleaned) {
|
||||
console.info(
|
||||
'media_upload_cleanup_completed',
|
||||
JSON.stringify({ expired, cleaned }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async expirePending(workspaceId: string): Promise<number> {
|
||||
return this.database.workspaceTransaction(workspaceId, async (tx) => {
|
||||
const result = await tx.query(
|
||||
`UPDATE atlas.media_upload_intents
|
||||
SET status = 'expired', error_code = 'UPLOAD_EXPIRED',
|
||||
error_message = '上传意图已过期', completed_at = now()
|
||||
WHERE status = 'pending' AND expires_at <= now()`,
|
||||
);
|
||||
return result.rowCount ?? 0;
|
||||
});
|
||||
}
|
||||
|
||||
private async claimCandidates(workspaceId: string) {
|
||||
return this.database.workspaceTransaction(workspaceId, async (tx) => {
|
||||
const candidates = await tx.query<CleanupCandidate>(
|
||||
`SELECT id, bucket, object_key
|
||||
FROM atlas.media_upload_intents
|
||||
WHERE status IN ('failed', 'expired') AND object_cleaned_at IS NULL
|
||||
ORDER BY created_at
|
||||
LIMIT 50`,
|
||||
);
|
||||
return candidates.rows;
|
||||
});
|
||||
}
|
||||
|
||||
private async markCleaned(workspaceId: string, intentId: string) {
|
||||
await this.database.workspaceTransaction(workspaceId, (tx) =>
|
||||
tx.query(
|
||||
`UPDATE atlas.media_upload_intents SET object_cleaned_at = now()
|
||||
WHERE id = $1 AND status IN ('failed', 'expired')
|
||||
AND object_cleaned_at IS NULL`,
|
||||
[intentId],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user