Three bugs fixed:
1. Vision failure was marking memories as 'failed', triggering orphan
cleanup that permanently deleted them. Vision is optional — on error,
now marks 'ready' so the image stays visible without AI captions.
2. Thumbnail failure was blocking vision from running (rejected promise
swallowed the chain). Fixed by catching thumbnail error separately so
processMemoryVision always executes and always sets a final status.
3. /api/img proxy was rejecting thumbnail keys (families/{id}/thumbnails/…)
because 'families/' was not in ALLOWED_PREFIXES. Added it.
Also: replaced the full-bleed 'Processing…' overlay with a small corner
badge so the uploaded photo is visible immediately after upload.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
50 lines
1.8 KiB
TypeScript
50 lines
1.8 KiB
TypeScript
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
|
import { NextRequest, NextResponse } from "next/server";
|
|
|
|
const ALLOWED_PREFIXES = ["avatars/", "profiles/", "memories/", "thumbnails/", "families/"];
|
|
|
|
export async function GET(req: NextRequest) {
|
|
const key = req.nextUrl.searchParams.get("key");
|
|
if (!key) return NextResponse.json({ error: "key required" }, { status: 400 });
|
|
|
|
// Only proxy our own R2 objects
|
|
if (!ALLOWED_PREFIXES.some(p => key.startsWith(p))) {
|
|
return NextResponse.json({ error: "Invalid key" }, { status: 403 });
|
|
}
|
|
|
|
const accountId = process.env.R2_ACCOUNT_ID;
|
|
const accessKeyId = process.env.R2_ACCESS_KEY_ID;
|
|
const secretKey = process.env.R2_SECRET_ACCESS_KEY;
|
|
const bucket = process.env.R2_BUCKET_NAME;
|
|
|
|
if (!accountId || !accessKeyId || !secretKey || !bucket) {
|
|
return NextResponse.json({ error: "Storage not configured" }, { status: 500 });
|
|
}
|
|
|
|
const client = new S3Client({
|
|
region: "auto",
|
|
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
|
credentials: { accessKeyId, secretAccessKey: secretKey },
|
|
});
|
|
|
|
try {
|
|
const obj = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
if (!obj.Body) return new NextResponse(null, { status: 404 });
|
|
|
|
const bytes = await (obj.Body as any).transformToByteArray();
|
|
return new NextResponse(bytes, {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": obj.ContentType || "image/jpeg",
|
|
"Cache-Control": "public, max-age=604800, immutable",
|
|
...(obj.ContentLength ? { "Content-Length": String(obj.ContentLength) } : {}),
|
|
},
|
|
});
|
|
} catch (e: any) {
|
|
if (e?.name === "NoSuchKey" || e?.$metadata?.httpStatusCode === 404) {
|
|
return new NextResponse(null, { status: 404 });
|
|
}
|
|
console.error("R2 img proxy error:", e);
|
|
return new NextResponse(null, { status: 502 });
|
|
}
|
|
}
|