image cache on cloudflare r2

feat: add upload endpoint

dunkirk.sh 3b880555 298c839f

verified
Changed files
+46
src
+5
.env.example
··· 36 36 # Service Configuration 37 37 # ----------------------------------------------------------------------------- 38 38 39 + # Auth token for API uploads (optional, but recommended) 40 + # Generate with: openssl rand -hex 32 41 + # Used for POST /upload endpoint 42 + AUTH_TOKEN=your-secret-token-here 43 + 39 44 # Public URL where this service is accessible 40 45 # This is the domain/URL users will access 41 46 # Examples:
+41
src/index.ts
··· 4 4 // Configuration from env 5 5 const R2_PUBLIC_URL = process.env.R2_PUBLIC_URL!; 6 6 const PUBLIC_URL = process.env.PUBLIC_URL || "http://localhost:3000"; 7 + const AUTH_TOKEN = process.env.AUTH_TOKEN; 7 8 8 9 // S3 configuration 9 10 const S3_ACCESS_KEY_ID = process.env.S3_ACCESS_KEY_ID || process.env.AWS_ACCESS_KEY_ID!; ··· 62 63 }, 63 64 }, 64 65 66 + "/upload": { 67 + async POST(request) { 68 + return handleUpload(request); 69 + }, 70 + }, 71 + 65 72 "/health": { 66 73 async GET(request) { 67 74 return Response.json({ status: "ok" }); ··· 88 95 return new Response("Not found", { status: 404 }); 89 96 }, 90 97 }); 98 + 99 + async function handleUpload(request: Request) { 100 + try { 101 + // Check auth token 102 + const authHeader = request.headers.get("Authorization"); 103 + if (!AUTH_TOKEN || authHeader !== `Bearer ${AUTH_TOKEN}`) { 104 + return new Response("Unauthorized", { status: 401 }); 105 + } 106 + 107 + // Parse multipart form data 108 + const formData = await request.formData(); 109 + const file = formData.get("file") as File; 110 + 111 + if (!file) { 112 + return Response.json({ success: false, error: "No file provided" }, { status: 400 }); 113 + } 114 + 115 + // Read file buffer 116 + const originalBuffer = Buffer.from(await file.arrayBuffer()); 117 + const contentType = file.type || "image/jpeg"; 118 + 119 + // Optimize image 120 + const { buffer: optimizedBuffer, contentType: newContentType } = await optimizeImage(originalBuffer, contentType); 121 + 122 + // Upload to R2 123 + const imageKey = await uploadImageToR2(optimizedBuffer, newContentType); 124 + const url = `${PUBLIC_URL}/i/${imageKey}`; 125 + 126 + return Response.json({ success: true, url }); 127 + } catch (error) { 128 + console.error("Error handling upload:", error); 129 + return Response.json({ success: false, error: "Upload failed" }, { status: 500 }); 130 + } 131 + } 91 132 92 133 async function handleSlackEvent(request: Request) { 93 134 try {