/** * 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'; }