Merge pull request '避免原图下载查询绕过七牛缓存' (#73) from codex/original-download-metadata-cache-20261111 into main
This commit is contained in:
commit
fd8d5404c6
@ -4,6 +4,7 @@ import { findImageArchiveDownloadTask } from "./image-archive-db";
|
||||
import { inferOriginalImageUrl } from "@/lib/image-url";
|
||||
import { getImageDownloadExtension } from "@/lib/image-delivery";
|
||||
import { sanitizeCanvasDownloadFileName } from "@/lib/canvas/photoshop-safe-download";
|
||||
import { HeadObjectCommand } from "@aws-sdk/client-s3";
|
||||
|
||||
/** Resolve the archived original without waiting for a separate PNG conversion. */
|
||||
export async function resolveOriginalCdnDownload(source: string, name: string, signal: AbortSignal) {
|
||||
@ -28,8 +29,32 @@ export async function resolveOriginalCdnDownload(source: string, name: string, s
|
||||
// Only metadata crosses the application server. The browser receives the file
|
||||
// from the CDN, including when cross-origin fetch is disabled by storage CORS.
|
||||
const response = await fetch(target, {
|
||||
method: "HEAD", cache: "no-store",
|
||||
signal: AbortSignal.any([signal, AbortSignal.timeout(10_000)]),
|
||||
method: "HEAD", cache: "default",
|
||||
signal: AbortSignal.any([signal, AbortSignal.timeout(3_000)]),
|
||||
}).catch(async (error) => {
|
||||
signal.throwIfAborted();
|
||||
if (target.search) throw error;
|
||||
const { createS3Client, toDbS3UploadConfig } = await import("./service");
|
||||
const storage = toDbS3UploadConfig(config);
|
||||
if (!storage) throw error;
|
||||
const key = decodeURIComponent(target.pathname.slice(base.pathname.replace(/\/$/, "").length + 1));
|
||||
if (!key) throw error;
|
||||
const client = createS3Client(storage);
|
||||
try {
|
||||
const object = await client.send(new HeadObjectCommand({ Bucket: storage.bucket, Key: key }), {
|
||||
abortSignal: AbortSignal.any([signal, AbortSignal.timeout(5_000)]),
|
||||
});
|
||||
return new Response(null, { headers: {
|
||||
"content-type": object.ContentType || "",
|
||||
"content-length": String(object.ContentLength ?? 0),
|
||||
} });
|
||||
} catch (storageError) {
|
||||
const status = (storageError as { $metadata?: { httpStatusCode?: number } })?.$metadata?.httpStatusCode;
|
||||
if (status === 403 || status === 404) throw new Error(`HTTP ${status}`);
|
||||
throw error;
|
||||
} finally {
|
||||
client.destroy();
|
||||
}
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
|
||||
@ -10,7 +10,8 @@ test("original downloads use the archived JPEG rather than the prepared PNG and
|
||||
"./image-archive-db": { findImageArchiveDownloadTask: async () => ({ targetUrl: "https://cdn.example/original.jpg", metadata: {
|
||||
downloadAsset: { status: "ready", url: "https://cdn.example/converted.png", contentType: "image/png" },
|
||||
} }) },
|
||||
fetch: async (url: URL, options: { method: string }) => {
|
||||
fetch: async (url: URL, options: { method: string; cache: string }) => {
|
||||
expect(options.cache).toBe("default");
|
||||
calls.push({ url: url.toString(), method: options.method });
|
||||
return { ok: true, headers: new Headers({ "content-type": "image/jpeg", "content-length": "12345" }),
|
||||
arrayBuffer() { throw new Error("Image bytes must bypass the application"); } };
|
||||
@ -52,6 +53,28 @@ test("temporary third-party URLs are never checked with HEAD when no archive is
|
||||
expect(await module.resolveOriginalCdnDownload("https://upstream.example/image.png", "image", new AbortController().signal)).toBeNull();
|
||||
});
|
||||
|
||||
test("a CDN metadata timeout falls back to the original Kodo object without reading its body", async () => {
|
||||
let closed = false;
|
||||
const module = serverLoader({
|
||||
"@/lib/db/queries": { getDefaultStorageConfig: async () => config },
|
||||
"./image-archive-db": {},
|
||||
"./service": {
|
||||
toDbS3UploadConfig: () => ({ bucket: "images" }),
|
||||
createS3Client: () => ({
|
||||
send: async (command: { input: { Bucket: string; Key: string } }) => {
|
||||
expect(command.input).toEqual({ Bucket: "images", Key: "uploads/source.png" });
|
||||
return { ContentType: "image/jpeg", ContentLength: 12345 };
|
||||
},
|
||||
destroy: () => { closed = true; },
|
||||
}),
|
||||
},
|
||||
fetch: async () => { throw new DOMException("Timed out", "TimeoutError"); },
|
||||
})("lib/storage/original-image-download.ts");
|
||||
const result = await module.resolveOriginalCdnDownload("https://cdn.example/uploads/source.png", "图片.png", new AbortController().signal);
|
||||
expect(result).toMatchObject({ delivery: "cdn", filename: "图片.jpg", bytes: 12345 });
|
||||
expect(closed).toBe(true);
|
||||
});
|
||||
|
||||
test("original download metadata reports missing sources with cause, advice and a feedback ID", async () => {
|
||||
const route = serverLoader({
|
||||
"next/server": { NextResponse: Response },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user