import "server-only"; import { createReadStream } from "node:fs"; import { PutObjectCommand } from "@aws-sdk/client-s3"; import { getDefaultStorageConfig, getStorageConfigById } from "@/lib/db/queries"; import { createS3Client, toDbS3UploadConfig, buildS3PublicUrl } from "./service"; /** Only a completely written archive is uploaded. Local GET remains a fallback. */ export async function publishCanvasZip(params: { id: string; path: string; bytes: number; disposition: string }) { const raw = await getDefaultStorageConfig(); const config = toDbS3UploadConfig(raw); if (config?.provider !== "qiniu-kodo" || !config.publicUrl?.startsWith("https://")) return null; const key = `canvas-downloads/${params.id}.zip`; const client = createS3Client(config); // Qiniu's S3 endpoint expects an ordinary known-length body, without AWS trailers. client.config.requestChecksumCalculation = async () => "WHEN_REQUIRED"; const body = createReadStream(params.path); try { await client.send(new PutObjectCommand({ Bucket: config.bucket, Key: key, Body: body, ContentLength: params.bytes, ContentType: "application/zip", ContentDisposition: params.disposition, CacheControl: "public, max-age=1800" }), { abortSignal: AbortSignal.timeout(90_000) }); const url = buildS3PublicUrl(config, key); const response = await fetch(url, { method: "HEAD", redirect: "error", signal: AbortSignal.timeout(10_000) }); const length = Number(response.headers.get("content-length")); if (!response.ok || length !== params.bytes) throw new Error("CDN object is not ready"); return { url, storageConfigId: raw!.id, key }; } catch (error) { console.warn("[CanvasDownloadZipTask] CDN upload/check failed; using complete local file", { taskId: params.id, kind: (error as Error).name }); return { url: null, storageConfigId: raw!.id, key }; } finally { body.destroy(); client.destroy(); } } export async function removeCanvasZipObject(object: { storageConfigId: string; key: string }) { if (!/^canvas-downloads\/[0-9a-f-]{36}\.zip$/i.test(object.key)) return; const config = toDbS3UploadConfig(await getStorageConfigById({ id: object.storageConfigId })); if (!config || config.provider !== "qiniu-kodo") return; const { DeleteObjectCommand } = await import("@aws-sdk/client-s3"); const client = createS3Client(config); try { await client.send(new DeleteObjectCommand({ Bucket: config.bucket, Key: object.key }), { abortSignal: AbortSignal.timeout(5000) }); } finally { client.destroy(); } }