@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
Markdown
This guide helps you migrate from your current file download implementation to the new secure download methods in S3 Pilot v2.5.1+.
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,
});
```
**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);
```
**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);
}
```
**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" });
}
});
```
**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);
}
};
```
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
}
]
}
```
- **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