Compare commits

...

2 Commits

4 changed files with 30 additions and 3 deletions

View File

@ -303,6 +303,20 @@ export async function fetchCanvasDownloadAsset(params: {
params.requestOrigin
);
if (process.env.POSTGRES_URL) {
// ZIPs and binary fallback also reuse the durable prepared object after the
// local byte cache expires, instead of converting the original JPEG again.
const { findImageArchiveDownloadTask } = await import("@/lib/storage/image-archive-db");
const task = await findImageArchiveDownloadTask(candidateUrls).catch(() => null);
const asset = (task?.metadata as { downloadAsset?: { status?: string; url?: string; contentType?: string } } | null)?.downloadAsset;
if (asset?.status === "ready" && asset.url && asset.contentType === "image/png") {
const readyUrl = resolveHttpUrl(asset.url, params.requestOrigin);
if (readyUrl && !candidateUrls.includes(readyUrl)) candidateUrls.unshift(readyUrl);
} else if (task?.targetUrl && !candidateUrls.includes(task.targetUrl)) {
candidateUrls.unshift(task.targetUrl);
}
}
for (const candidateUrl of candidateUrls) {
for (let attempt = 1; attempt <= retryCount; attempt += 1) {
try {

View File

@ -8,7 +8,7 @@ export async function resolveCanvasCdnDownload(urls: string[], filename: string)
const source = extractImageProxySourceUrl(url) || url;
return [source, inferOriginalImageUrl(source) || source];
})));
const task = await findImageArchiveDownloadTask(sources);
const task = await findImageArchiveDownloadTask(sources, true);
const asset = (task?.metadata as { downloadAsset?: { status?: string; url?: string; contentType?: string; bytes?: number } } | null)?.downloadAsset;
if (asset?.status !== "ready" || !asset.url || asset.contentType !== "image/png") return { task, direct: null };
const config = await getDefaultStorageConfig();

View File

@ -317,11 +317,11 @@ export async function resolveArchivedImageUrls<T>(value: T): Promise<T> {
// Download assets use an independent JSONB lease; original archiving/replacement
// can finish even if PNG preparation or thumbnail storage is temporarily unavailable.
export async function findImageArchiveDownloadTask(urls: string[]) {
export async function findImageArchiveDownloadTask(urls: string[], prepareOnDemand = false) {
const [task] = await db.select().from(imageArchiveTask).where(or(
inArray(imageArchiveTask.sourceUrl, urls), inArray(imageArchiveTask.targetUrl, urls),
)).limit(1);
if (task?.targetUrl && !(task.metadata as { downloadAsset?: unknown } | null)?.downloadAsset) {
if (prepareOnDemand && task?.targetUrl && !(task.metadata as { downloadAsset?: unknown } | null)?.downloadAsset) {
await db.update(imageArchiveTask).set({
metadata: sql`coalesce(${imageArchiveTask.metadata}, '{}'::jsonb) || jsonb_build_object('downloadAsset', jsonb_build_object('status', 'pending'))`,
}).where(and(eq(imageArchiveTask.id, task.id), sql`${imageArchiveTask.metadata}->'downloadAsset' is null`));

View File

@ -164,3 +164,16 @@ test("single-image route preserves authentication and returns a small CDN respon
expect(response.status).toBe(200); expect(response.headers.get("Cache-Control")).toBe("no-store");
expect((await response.json()).delivery).toBe("cdn"); expect(lookups).toBe(1);
});
test("batch downloads use the durable prepared PNG even when the source cache is cold", async () => {
const original = `https://provider.example/${crypto.randomUUID()}.jpg`;
const prepared = `https://cdn.example/${crypto.randomUUID()}.png`;
const bytes = await sharp({ create: { width: 12, height: 20, channels: 4, background: "#abcdef80" } }).png().toBuffer();
const fetched: string[] = [];
const module = serverLoader({
"@/lib/storage/image-archive-db": { findImageArchiveDownloadTask: async () => ({ targetUrl: original, metadata: { downloadAsset: { status: "ready", url: prepared, contentType: "image/png" } } }) },
fetch: async (url: string) => { fetched.push(url); expect(url).toBe(prepared); return new Response(bytes, { headers: { "Content-Type": "image/png" } }); },
}, { POSTGRES_URL: "mock-only" })("lib/canvas/photoshop-safe-download.ts");
const result = await module.fetchCanvasDownloadAsset({ file: { url: original, name: "作品" }, index: 0, requestOrigin: "https://app.example", cookie: "", retryCount: 1 });
expect(result.ok).toBe(true); expect(result.bytes.equals(bytes)).toBe(true);
expect(result.filename).toBe("001-作品.png"); expect(fetched).toEqual([prepared]);
});