UNPKG

@d3oxy/s3-pilot

Version:

A TypeScript wrapper for AWS S3 with support for multiple clients, buckets, and secure file downloads.

298 lines (226 loc) 8.89 kB
# S3 Pilot File Download Migration Guide This guide helps you migrate from your current file download implementation to the new secure download methods in S3 Pilot v2.5.1+. ## Overview of Changes ### New Methods Added 1. **`getFile()`** - Download file content as Buffer (recommended for server-side) 2. **`getFileStream()`** - Download file content as ReadableStream (for large files) 3. **Enhanced `generateSignedUrl()`** - Support for download-specific headers ### Breaking Changes - `generateSignedUrl()` default expiration changed from 60 seconds to 3600 seconds (1 hour) ## Migration Scenarios ### Scenario 1: CORS Issues with Signed URLs **Before (Causing CORS Issues):** ```typescript // Current approach - causes CORS errors in browser const signedUrl = await s3Pilot.generateSignedUrl("main", bucketName, { key: fileKey, expiresIn: 3600, }); // Frontend tries to fetch this URL but gets CORS error const response = await fetch(signedUrl); ``` **After (Recommended Solution):** ```typescript // Option 1: Server-side download (recommended) const fileResponse = await s3Pilot.getFile("main", bucketName, { key: fileKey, }); // Stream file to client with proper headers res.setHeader("Content-Type", fileResponse.contentType || "application/octet-stream"); res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`); res.send(fileResponse.buffer); // Option 2: Enhanced signed URL with download headers const signedUrl = await s3Pilot.generateSignedUrl("main", bucketName, { key: fileKey, expiresIn: 3600, responseContentDisposition: `attachment; filename="${fileName}"`, responseContentType: fileType, }); ``` ### Scenario 2: Large File Downloads **Before (Memory Issues):** ```typescript // Downloading large files as buffer can cause memory issues const fileResponse = await s3Pilot.getFile("main", bucketName, { key: largeFileKey, }); ``` **After (Streaming for Large Files):** ```typescript // Use streaming for files larger than 100MB const streamResponse = await s3Pilot.getFileStream("main", bucketName, { key: largeFileKey, }); // Pipe stream directly to response res.setHeader("Content-Type", streamResponse.contentType || "application/octet-stream"); res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`); streamResponse.stream.pipe(res); ``` ### Scenario 3: Conditional Download Based on File Size **Before (Single Approach):** ```typescript // Always using the same method regardless of file size const fileResponse = await s3Pilot.getFile("main", bucketName, { key: fileKey, }); ``` **After (Optimized Approach):** ```typescript const sizeThreshold = 50 * 1024 * 1024; // 50MB // Check file size first const fileResponse = await s3Pilot.getFile("main", bucketName, { key: fileKey, }); if (fileResponse.contentLength && fileResponse.contentLength > sizeThreshold) { // Use streaming for large files const streamResponse = await s3Pilot.getFileStream("main", bucketName, { key: fileKey, }); streamResponse.stream.pipe(res); } else { // Use buffer for smaller files res.send(fileResponse.buffer); } ``` ## API Endpoint Migration Examples ### Express.js/Hono.js Migration **Before:** ```typescript app.get("/download/:fileKey", async (req, res) => { try { const signedUrl = await s3Pilot.generateSignedUrl("main", bucketName, { key: req.params.fileKey, expiresIn: 3600, }); // This can cause CORS issues res.redirect(signedUrl); } catch (error) { res.status(404).json({ error: "File not found" }); } }); ``` **After (Recommended):** ```typescript app.get("/download/:fileKey", async (req, res) => { try { const fileResponse = await s3Pilot.getFile("main", bucketName, { key: req.params.fileKey, }); res.setHeader("Content-Type", fileResponse.contentType || "application/octet-stream"); res.setHeader("Content-Disposition", `attachment; filename="${req.params.fileKey}"`); res.setHeader("Content-Length", fileResponse.contentLength?.toString() || "0"); res.send(fileResponse.buffer); } catch (error) { res.status(404).json({ error: "File not found" }); } }); ``` **After (For Large Files):** ```typescript app.get("/download/:fileKey", async (req, res) => { try { const streamResponse = await s3Pilot.getFileStream("main", bucketName, { key: req.params.fileKey, }); res.setHeader("Content-Type", streamResponse.contentType || "application/octet-stream"); res.setHeader("Content-Disposition", `attachment; filename="${req.params.fileKey}"`); res.setHeader("Content-Length", streamResponse.contentLength?.toString() || "0"); streamResponse.stream.pipe(res); } catch (error) { res.status(404).json({ error: "File not found" }); } }); ``` ## Frontend Migration ### React/Next.js Migration **Before (CORS Issues):** ```typescript const downloadFile = async (fileKey: string) => { try { const response = await fetch(`/api/download/${fileKey}`); const signedUrl = await response.json(); // This causes CORS issues const fileResponse = await fetch(signedUrl.url); const blob = await fileResponse.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = fileName; a.click(); } catch (error) { console.error("Download failed:", error); } }; ``` **After (Direct Download):** ```typescript const downloadFile = async (fileKey: string) => { try { // Direct download through your API server const response = await fetch(`/api/download/${fileKey}`); if (response.ok) { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = fileKey.split("/").pop() || "download"; a.click(); window.URL.revokeObjectURL(url); } else { throw new Error("Download failed"); } } catch (error) { console.error("Download failed:", error); } }; ``` ## Configuration Updates ### S3 Bucket CORS Configuration (If Using Enhanced Signed URLs) If you choose to use enhanced signed URLs with download headers, you may need to update your S3 bucket CORS configuration: ```json { "CORSRules": [ { "AllowedHeaders": ["*"], "AllowedMethods": ["GET"], "AllowedOrigins": ["https://yourdomain.com"], "ExposeHeaders": ["Content-Disposition", "Content-Type", "Content-Length"], "MaxAgeSeconds": 3000 } ] } ``` ## Performance Considerations ### Memory Usage - **Small files (< 50MB)**: Use `getFile()` with buffer - **Large files (> 50MB)**: Use `getFileStream()` with streaming - **Very large files (> 500MB)**: Always use streaming ### Network Efficiency - **Server-side download**: Better for security, control, and avoiding CORS - **Enhanced signed URLs**: Better for reducing server load and bandwidth ## Security Best Practices 1. **Always validate user permissions** before allowing downloads 2. **Use short expiration times** for signed URLs (max 1 hour) 3. **Implement rate limiting** on download endpoints 4. **Validate file types** before allowing downloads 5. **Use private S3 buckets** for sensitive files ## Testing Your Migration 1. **Test with small files** (< 1MB) first 2. **Test with large files** (> 100MB) to ensure streaming works 3. **Test CORS behavior** in different browsers 4. **Test error handling** for missing files 5. **Test concurrent downloads** to ensure stability ## Rollback Plan If you encounter issues, you can temporarily rollback by: 1. **Reverting to basic signed URLs** (but with CORS issues) 2. **Implementing server-side proxy** for signed URLs 3. **Using direct S3 SDK calls** as fallback ## Support If you encounter issues during migration: 1. Check the [examples](./examples/file-download-examples.ts) for working implementations 2. Review the [README](./README.md) for detailed documentation 3. Open an issue on GitHub with your specific use case ## Version Compatibility - **v2.5.1+**: New download methods available - **v2.4.x and below**: Only basic signed URLs available - **Migration required**: If you need CORS-free downloads or large file support