Fix upload: route through server to avoid CORS
This commit is contained in:
parent
e3c33bb0dc
commit
b39f344426
2 changed files with 72 additions and 49 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { S3Client, PutObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
|
import { S3Client, PutObjectCommand, ListObjectsV2Command, GetObjectCommand } from "@aws-sdk/client-s3";
|
||||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ function getR2() {
|
||||||
const bucket = process.env.R2_BUCKET_NAME;
|
const bucket = process.env.R2_BUCKET_NAME;
|
||||||
|
|
||||||
if (!accountId || !accessKeyId || !secretKey || !bucket) {
|
if (!accountId || !accessKeyId || !secretKey || !bucket) {
|
||||||
throw new Error(`Missing R2 config: accountId=${!!accountId}, accessKeyId=${!!accessKeyId}, secretKey=${!!secretKey}, bucket=${!!bucket}`);
|
throw new Error(`Missing R2 config`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -23,6 +23,31 @@ function getR2() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: List memories
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { client, bucket, baseUrl } = getR2();
|
||||||
|
const { searchParams } = new URL(req.url);
|
||||||
|
const childId = searchParams.get("childId") || "default";
|
||||||
|
const prefix = `memories/${childId}`;
|
||||||
|
|
||||||
|
const command = new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix });
|
||||||
|
const res = await client.send(command);
|
||||||
|
const objects = (res.Contents || []).map((obj) => ({
|
||||||
|
key: obj.Key,
|
||||||
|
url: `${baseUrl}/${obj.Key}`,
|
||||||
|
size: obj.Size,
|
||||||
|
lastModified: obj.LastModified?.toISOString(),
|
||||||
|
})).sort((a, b) => new Date(b.lastModified!).getTime() - new Date(a.lastModified!).getTime());
|
||||||
|
|
||||||
|
return NextResponse.json({ memories: objects });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("R2 list error:", error);
|
||||||
|
return NextResponse.json({ error: String(error) }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Upload (proxy through server to avoid CORS)
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
let body;
|
let body;
|
||||||
try {
|
try {
|
||||||
|
|
@ -42,48 +67,60 @@ export async function POST(req: NextRequest) {
|
||||||
const ext = filename.split(".").pop() || "jpg";
|
const ext = filename.split(".").pop() || "jpg";
|
||||||
const key = `memories/${childId || "default"}/${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`;
|
const key = `memories/${childId || "default"}/${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`;
|
||||||
|
|
||||||
|
// Instead of presigned URL, we're using PUT to our own server
|
||||||
|
// The frontend willPOST the file here directly
|
||||||
const command = new PutObjectCommand({
|
const command = new PutObjectCommand({
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Key: key,
|
Key: key,
|
||||||
ContentType: contentType,
|
ContentType: contentType,
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = await getSignedUrl(client, command, { expiresIn: 60 });
|
// Generate short-lived presigned URL that bypasses CORS issues
|
||||||
|
// R2 needs proper signing - let's use direct put with our server as proxy
|
||||||
|
const url = await getSignedUrl(client, command, { expiresIn: 3600 }); // 1 hour
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
uploadUrl: url,
|
uploadUrl: url,
|
||||||
key,
|
key,
|
||||||
publicUrl: `${baseUrl}/${key}`,
|
publicUrl: `${baseUrl}/${key}`,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("R2 error:", error);
|
||||||
|
return NextResponse.json({ error: String(error) }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT: Direct upload from file content
|
||||||
|
export async function PUT(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { client, bucket, baseUrl } = getR2();
|
||||||
|
const { searchParams } = new URL(req.url);
|
||||||
|
const key = searchParams.get("key");
|
||||||
|
const contentType = searchParams.get("contentType") || "image/jpeg";
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
return NextResponse.json({ error: "Missing key" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await req.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
|
const command = new PutObjectCommand({
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: key,
|
||||||
|
Body: buffer,
|
||||||
|
ContentType: contentType,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.send(command);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
key,
|
||||||
|
url: `${baseUrl}/${key}`,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("R2 upload error:", error);
|
console.error("R2 upload error:", error);
|
||||||
return NextResponse.json({ error: String(error) }, { status: 500 });
|
return NextResponse.json({ error: String(error) }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { client, bucket, baseUrl } = getR2();
|
|
||||||
const { searchParams } = new URL(req.url);
|
|
||||||
const childId = searchParams.get("childId") || "default";
|
|
||||||
const prefix = `memories/${childId}`;
|
|
||||||
|
|
||||||
const command = new ListObjectsV2Command({
|
|
||||||
Bucket: bucket,
|
|
||||||
Prefix: prefix,
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = await client.send(command);
|
|
||||||
const objects = (res.Contents || []).map((obj) => ({
|
|
||||||
key: obj.Key,
|
|
||||||
url: `${baseUrl}/${obj.Key}`,
|
|
||||||
size: obj.Size,
|
|
||||||
lastModified: obj.LastModified?.toISOString(),
|
|
||||||
})).sort((a, b) => new Date(b.lastModified!).getTime() - new Date(a.lastModified!).getTime());
|
|
||||||
|
|
||||||
return NextResponse.json({ memories: objects });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("R2 list error:", error);
|
|
||||||
return NextResponse.json({ error: String(error) }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -40,7 +40,7 @@ export default function MemoriesPage() {
|
||||||
setUploadProgress(0);
|
setUploadProgress(0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get presigned URL
|
// Get upload key
|
||||||
const res = await fetch("/api/upload", {
|
const res = await fetch("/api/upload", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|
@ -49,47 +49,33 @@ export default function MemoriesPage() {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
console.error("API error:", data.error);
|
|
||||||
alert("Error: " + data.error);
|
alert("Error: " + data.error);
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { uploadUrl, key, publicUrl } = data;
|
const { key, publicUrl } = data;
|
||||||
console.log("Got presigned URL:", uploadUrl);
|
|
||||||
|
|
||||||
// Upload to R2
|
// Upload through our server (no CORS issue)
|
||||||
const xhr = new XMLHttpRequest();
|
const uploadRes = await fetch(`/api/upload?key=${encodeURIComponent(key)}&contentType=${encodeURIComponent(file.type)}`, {
|
||||||
xhr.open("PUT", uploadUrl);
|
method: "PUT",
|
||||||
xhr.setRequestHeader("Content-Type", file.type);
|
body: file,
|
||||||
xhr.upload.onprogress = (e) => {
|
});
|
||||||
if (e.lengthComputable) {
|
|
||||||
setUploadProgress(Math.round((e.loaded / e.total) * 100));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.onerror = () => {
|
|
||||||
console.error("Upload XHR error");
|
|
||||||
alert("Upload failed");
|
|
||||||
setUploading(false);
|
|
||||||
};
|
|
||||||
xhr.send(file);
|
|
||||||
|
|
||||||
xhr.onload = () => {
|
const uploadData = await uploadRes.json();
|
||||||
if (xhr.status === 200) {
|
|
||||||
setMemories([{ key, url: publicUrl, size: file.size, lastModified: new Date().toISOString() }, ...memories]);
|
if (uploadData.success) {
|
||||||
} else {
|
setMemories([{ key, url: publicUrl, size: file.size, lastModified: new Date().toISOString() }, ...memories]);
|
||||||
console.error("Upload failed status:", xhr.status, xhr.statusText);
|
} else {
|
||||||
alert("Upload failed: " + xhr.statusText);
|
alert("Upload failed: " + uploadData.error);
|
||||||
}
|
}
|
||||||
setUploading(false);
|
|
||||||
setUploadProgress(0);
|
|
||||||
if (fileRef.current) fileRef.current.value = "";
|
|
||||||
};
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Upload failed:", err);
|
console.error("Upload failed:", err);
|
||||||
alert("Error: " + err);
|
alert("Error: " + err);
|
||||||
setUploading(false);
|
|
||||||
}
|
}
|
||||||
|
setUploading(false);
|
||||||
|
setUploadProgress(0);
|
||||||
|
if (fileRef.current) fileRef.current.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue