src/api/upload.js
2,777 bytes · 84 lines · capsule://quake0day/[email protected]
raw on github
import { isAuthed, json, unauthorized } from "../_lib/auth.js";
// POST /api/upload (multipart/form-data)
// field "file": image blob (already resized client-side)
// field "name": optional desired filename (will be sanitized + uniquified)
// → returns { ok, key, url, w, h }
//
// The client is expected to have already resized the image to a sensible
// max dimension (we accept anything up to 8MB to be safe).
const MAX_BYTES = 8 * 1024 * 1024;
const ALLOWED_TYPES = new Set(["image/jpeg", "image/png", "image/webp"]);
function sanitizeBaseName(name) {
let base = (name || "upload").replace(/\.[a-z0-9]+$/i, "");
base = base.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "")
.slice(0, 60);
return base || "upload";
}
function pickExt(type) {
if (type === "image/png") return "png";
if (type === "image/webp") return "webp";
return "jpg";
}
export async function onRequest(context) {
const { request, env } = context;
if (request.method.toUpperCase() !== "POST")
return json({ error: "Method not allowed" }, { status: 405 });
if (!await isAuthed(request, env)) return unauthorized();
if (!env.YL_IMAGES) return json({ error: "R2 bucket YL_IMAGES is not bound" }, { status: 500 });
const ct = request.headers.get("Content-Type") || "";
if (!ct.startsWith("multipart/form-data"))
return json({ error: "Expected multipart/form-data" }, { status: 400 });
let form;
try { form = await request.formData(); } catch {
return json({ error: "Invalid form data" }, { status: 400 });
}
const file = form.get("file");
if (!file || typeof file === "string")
return json({ error: "Missing file field" }, { status: 400 });
const type = file.type || "image/jpeg";
if (!ALLOWED_TYPES.has(type))
return json({ error: `Unsupported type: ${type}. Use JPEG/PNG/WebP.` }, { status: 400 });
const ab = await file.arrayBuffer();
if (ab.byteLength > MAX_BYTES)
return json({ error: `File too large (${ab.byteLength} bytes, max ${MAX_BYTES})` }, { status: 413 });
const desired = (form.get("name") || file.name || "upload").toString();
const base = sanitizeBaseName(desired);
const ext = pickExt(type);
const stamp = Date.now().toString(36);
const rand = Math.random().toString(36).slice(2, 6);
const key = `${base}_${stamp}${rand}.${ext}`;
await env.YL_IMAGES.put(key, ab, {
httpMetadata: {
contentType: type,
cacheControl: "public, max-age=31536000, immutable"
}
});
// Width/height passed by client (post-resize)
const w = parseInt(form.get("w") || "0", 10) || null;
const h = parseInt(form.get("h") || "0", 10) || null;
return json({
ok: true,
key,
url: `/api/img/${key}`,
w, h,
bytes: ab.byteLength,
type
});
}