/** * Cloudflare Worker for generating signed URLs for R2 objects * * This worker provides secure access to private R2 buckets by generating * time-limited signed URLs. * * Endpoint: /signed-url?file=&bucket=&expires= * * Example: * GET /signed-url?file=manuals/Crane/manual.pdf&bucket=vending-vm-manuals&expires=3600 * * Returns: { url: "https://signed-url..." } */ export interface Env { // R2 bucket bindings (configured in wrangler.toml) MANUALS_BUCKET?: R2Bucket THUMBNAILS_BUCKET?: R2Bucket // Default bucket if not specified DEFAULT_BUCKET?: R2Bucket } export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url) // Only allow GET requests if (request.method !== "GET") { return new Response("Method not allowed", { status: 405 }) } // Handle signed URL generation if (url.pathname === "/signed-url") { return handleSignedUrl(request, env, url) } // Health check endpoint if (url.pathname === "/health") { return new Response( JSON.stringify({ status: "ok", timestamp: new Date().toISOString() }), { headers: { "Content-Type": "application/json" }, } ) } return new Response("Not found", { status: 404 }) }, } /** * Generate a signed URL for an R2 object */ async function handleSignedUrl( request: Request, env: Env, url: URL ): Promise { const filePath = url.searchParams.get("file") const bucketName = url.searchParams.get("bucket") || "manuals" const expiresIn = parseInt(url.searchParams.get("expires") || "3600", 10) // Default 1 hour if (!filePath) { return new Response( JSON.stringify({ error: "file parameter is required" }), { status: 400, headers: { "Content-Type": "application/json" } } ) } // Validate expires parameter if (isNaN(expiresIn) || expiresIn < 60 || expiresIn > 604800) { return new Response( JSON.stringify({ error: "expires must be between 60 and 604800 seconds (1 minute to 7 days)", }), { status: 400, headers: { "Content-Type": "application/json" } } ) } // Get the appropriate bucket let bucket: R2Bucket | undefined if (bucketName === "manuals" && env.MANUALS_BUCKET) { bucket = env.MANUALS_BUCKET } else if (bucketName === "thumbnails" && env.THUMBNAILS_BUCKET) { bucket = env.THUMBNAILS_BUCKET } else if (env.DEFAULT_BUCKET) { bucket = env.DEFAULT_BUCKET } else { return new Response( JSON.stringify({ error: `Bucket '${bucketName}' not found or not configured`, }), { status: 404, headers: { "Content-Type": "application/json" } } ) } try { // Generate signed URL // Note: R2 signed URLs are generated using the bucket's presigned URL method // This is a simplified example - actual implementation may vary based on R2 API const signedUrl = await bucket.createMultipartUpload(filePath, { httpMetadata: { contentType: getContentType(filePath), }, }) // For R2, we need to use the R2 API to generate presigned URLs // This is a placeholder - actual R2 signed URL generation may require // using the R2 API directly or a different method const object = await bucket.get(filePath) if (!object) { return new Response(JSON.stringify({ error: "File not found" }), { status: 404, headers: { "Content-Type": "application/json" }, }) } // Generate presigned URL (this is a simplified example) // In production, you would use R2's actual presigned URL API const presignedUrl = await generatePresignedUrl(bucket, filePath, expiresIn) return new Response( JSON.stringify({ url: presignedUrl, expiresIn, expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(), }), { headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET", }, } ) } catch (error) { console.error("Error generating signed URL:", error) return new Response( JSON.stringify({ error: "Failed to generate signed URL", message: error instanceof Error ? error.message : "Unknown error", }), { status: 500, headers: { "Content-Type": "application/json" } } ) } } /** * Generate a presigned URL for R2 object * Note: This is a placeholder - actual implementation depends on R2 API */ async function generatePresignedUrl( bucket: R2Bucket, key: string, expiresIn: number ): Promise { // R2 doesn't have a direct presigned URL API like S3 // You would need to use R2's public URL with custom domain or // implement a custom signing mechanism // // For now, this returns a placeholder URL // In production, you would: // 1. Use R2's public bucket URL if bucket is public // 2. Or implement custom signing using R2's API // 3. Or use a custom domain with R2 throw new Error( "Presigned URL generation not yet implemented. Use public buckets or implement custom signing." ) } /** * Get content type based on file extension */ function getContentType(filename: string): string { const ext = filename.split(".").pop()?.toLowerCase() const types: Record = { pdf: "application/pdf", jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", webp: "image/webp", gif: "image/gif", } return types[ext || ""] || "application/octet-stream" }