@d3oxy/s3-pilot
Version:
A TypeScript wrapper for AWS S3 and S3-compatible services (R2, MinIO, DigitalOcean Spaces) with a simplified single-client, single-bucket architecture.
601 lines (460 loc) • 15.8 kB
Markdown
# S3 Pilot Migration Guide
This guide helps you migrate between different versions of S3 Pilot.
## Migrating from v3.x to v4.x
### Overview of Breaking Changes
S3 Pilot v4.x introduces **S3-compatible service support** and a **project restructure**:
- **New `endpoint` config option**: Specify custom S3-compatible endpoints (R2, MinIO, Spaces)
- **New `publicBaseUrl` config option**: Specify the base URL for generating public URLs
- **`getUrl()` is now synchronous**: Returns a string directly instead of a Promise
- **REMOVED: `getKeyFromUrl()` method**: This method has been removed. Parse URLs in your application layer if needed.
- **Project restructure**: Source moved to `src/`, build output to `dist/`
### Migration Steps
#### Step 1: Update Method Calls for `getUrl()`
The `getUrl()` method is now **synchronous** and returns a string directly.
**Before (v3.x):**
```typescript
const url = await bucket1.getUrl("test.jpg");
```
**After (v4.x):**
```typescript
const url = bucket1.getUrl("test.jpg");
```
#### Step 2: Remove `getKeyFromUrl()` Usage
The `getKeyFromUrl()` method has been removed. If you need to extract keys from URLs, implement this in your application layer.
**Before (v3.x):**
```typescript
const key = await bucket1.getKeyFromUrl("https://bucket.s3.region.amazonaws.com/folder/file.jpg");
```
**After (v4.x):**
```typescript
// Implement URL parsing in your application layer
function getKeyFromUrl(url: string): string {
const parsedUrl = new URL(url);
return decodeURIComponent(parsedUrl.pathname.substring(1)); // Remove leading '/'
}
const key = getKeyFromUrl("https://bucket.s3.region.amazonaws.com/folder/file.jpg");
```
#### Step 3: Add S3-Compatible Service Support (Optional)
If you're using S3-compatible services like Cloudflare R2, DigitalOcean Spaces, or MinIO, you can now use the new `endpoint` and `publicBaseUrl` options.
**Before (v3.x) - Not possible without workarounds:**
```typescript
const bucket = new S3Pilot({
region: "us-east-1",
accessKeyId: "...",
secretAccessKey: "...",
bucket: "my-bucket",
// Had to use additionalConfig for endpoint
additionalConfig: {
endpoint: "https://account.r2.cloudflarestorage.com",
},
});
// getUrl() would still return AWS-style URLs
```
**After (v4.x) - Native S3-compatible support:**
```typescript
const bucket = new S3Pilot({
region: "auto", // R2 uses "auto"
accessKeyId: "...",
secretAccessKey: "...",
bucket: "my-bucket",
endpoint: "https://account.r2.cloudflarestorage.com",
publicBaseUrl: "https://pub-xxx.r2.dev", // Your public URL
});
// getUrl() now returns correct R2 URLs
const url = bucket.getUrl("test.jpg"); // https://pub-xxx.r2.dev/test.jpg
```
### Benefits of v4.x
1. **S3-Compatible Services**: Native support for Cloudflare R2, DigitalOcean Spaces, MinIO
2. **Cleaner API**: `getUrl()` is now synchronous (no unnecessary Promise)
3. **Flexible URL Generation**: Use `publicBaseUrl` for custom domains and CDNs
4. **Modern Project Structure**: Standard `src/` → `dist/` build pattern
---
## Migrating from v2.x to v3.x
### Overview of Breaking Changes
S3 Pilot v3.x introduces a **major API simplification**:
- **Single-client, single-bucket architecture**: Each `S3Pilot` instance now represents one S3 client connected to exactly one bucket
- **Removed multi-client management**: No more `clientName` parameters
- **Removed bucket parameters**: Bucket is specified at initialization, not in each method call
- **New bulk delete method**: `deleteFiles()` for efficient deletion of multiple files
- **Simplified `moveFile()` method**: Now moves files within the same bucket
- **New `moveToBucket()` method**: Moves files between buckets with automatic fallback for cross-account scenarios
- **Removed extension validation**: File extension validation has been removed. You can upload files with any extension. Implement validation in your application layer if needed.
### Migration Steps
#### Step 1: Update Imports
**Before (v2.x):**
```typescript
import { S3ClientSettings, S3ClientsSetup, S3Pilot } from "@d3oxy/s3-pilot";
```
**After (v3.x):**
```typescript
import { S3Pilot } from "@d3oxy/s3-pilot";
```
#### Step 2: Update Initialization
**Before (v2.x):**
```typescript
const s3Pilot = new S3Pilot<
S3ClientsSetup<{
client1: S3ClientSettings<"bucket1" | "bucket2">;
client2: S3ClientSettings<"bucket3">;
}>
>({
client1: {
region: "us-east-1",
accessKeyId: "...",
secretAccessKey: "...",
buckets: ["bucket1", "bucket2"],
},
client2: {
region: "us-west-2",
accessKeyId: "...",
secretAccessKey: "...",
buckets: ["bucket3"],
},
});
```
**After (v3.x):**
```typescript
// Create one instance per bucket
const bucket1 = new S3Pilot({
region: "us-east-1",
accessKeyId: "...",
secretAccessKey: "...",
bucket: "bucket1",
});
const bucket2 = new S3Pilot({
region: "us-east-1",
accessKeyId: "...",
secretAccessKey: "...",
bucket: "bucket2",
});
const bucket3 = new S3Pilot({
region: "us-west-2",
accessKeyId: "...",
secretAccessKey: "...",
bucket: "bucket3",
});
```
#### Step 3: Update Method Calls
All methods now remove the `clientName` and `bucket` parameters:
**Before (v2.x):**
```typescript
// Upload
await s3Pilot.uploadFile("client1", "bucket1", {
filename: "test.jpg",
file: buffer,
contentType: "image/jpeg",
});
// Download
const file = await s3Pilot.getFile("client1", "bucket1", {
key: "test.jpg",
});
// Delete
await s3Pilot.deleteFile("client1", "bucket1", {
key: "test.jpg",
});
// Generate signed URL
const url = await s3Pilot.generateSignedUrl("client1", "bucket1", {
key: "test.jpg",
expiresIn: 3600,
});
```
**After (v3.x):**
```typescript
// Upload
await bucket1.uploadFile({
filename: "test.jpg",
file: buffer,
contentType: "image/jpeg",
});
// Download
const file = await bucket1.getFile({
key: "test.jpg",
});
// Delete
await bucket1.deleteFile({
key: "test.jpg",
});
// Generate signed URL
const url = await bucket1.generateSignedUrl({
key: "test.jpg",
expiresIn: 3600,
});
```
#### Step 4: Update Bulk Delete (New Feature)
**Before (v2.x):**
```typescript
// Had to delete files one by one
for (const key of keys) {
await s3Pilot.deleteFile("client1", "bucket1", { key });
}
```
**After (v3.x):**
```typescript
// New bulk delete method
const result = await bucket1.deleteFiles({
keys: ["file1.jpg", "file2.jpg", "file3.jpg"],
});
console.log("Deleted:", result.deleted);
if (result.errors.length > 0) {
console.log("Errors:", result.errors);
}
```
#### Step 5: Handle Updated `moveFile()` Method
The `moveFile()` method now only supports **same-bucket moves**. For cross-bucket moves, use copy + delete.
**Before (v2.x) - Cross-bucket move:**
```typescript
await s3Pilot.moveFile("client1", "bucket1", "bucket2", {
sourceKey: "old-key.jpg",
destinationKey: "new-key.jpg",
});
```
**After (v3.x) - Same-bucket move (supported):**
```typescript
// Move within the same bucket - simplified API
await bucket1.moveFile({
sourceKey: "uploads/temp/photo.jpg",
destinationKey: "uploads/processed/photo.jpg",
});
```
**After (v3.x) - Cross-bucket move (new method):**
```typescript
const sourceBucket = new S3Pilot({ ...config1, bucket: "bucket1" });
const destBucket = new S3Pilot({ ...config2, bucket: "bucket2" });
// Use the new moveToBucket method
await sourceBucket.moveToBucket({
sourceKey: "old-key.jpg",
destinationKey: "new-key.jpg",
destination: destBucket,
});
```
The `moveToBucket` method automatically:
- Tries direct S3 copy first (fastest, works if same credentials)
- Falls back to download + upload if direct copy fails (cross-account support)
#### Step 6: Handle Removed Extension Validation
**Before (v2.x):**
```typescript
const s3Pilot = new S3Pilot({
client1: {
region: "us-east-1",
accessKeyId: "...",
secretAccessKey: "...",
buckets: ["bucket1"],
enableDefaultAllowedExtensions: true,
// or
allowedExtensions: ["jpg", "png", "pdf"],
},
});
// Extension validation was automatic
await s3Pilot.uploadFile("client1", "bucket1", {
filename: "test.jpg", // Would validate extension
file: buffer,
contentType: "image/jpeg",
});
```
**After (v3.x):**
```typescript
const bucket1 = new S3Pilot({
region: "us-east-1",
accessKeyId: "...",
secretAccessKey: "...",
bucket: "bucket1",
// Extension validation config options removed
});
// No extension validation - implement in your application layer
const allowedExtensions = ["jpg", "png", "pdf"];
const extension = filename.split(".").pop();
if (!extension || !allowedExtensions.includes(extension.toLowerCase())) {
throw new Error("Invalid file extension");
}
await bucket1.uploadFile({
filename: "test.jpg",
file: buffer,
contentType: "image/jpeg",
});
```
### Complete Migration Example
**Before (v2.x):**
```typescript
import { S3ClientSettings, S3ClientsSetup, S3Pilot } from "@d3oxy/s3-pilot";
const s3 = new S3Pilot<
S3ClientsSetup<{
main: S3ClientSettings<"uploads" | "downloads">;
}>
>({
main: {
region: "us-east-1",
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
buckets: ["uploads", "downloads"],
},
});
// Upload to uploads bucket
await s3.uploadFile("main", "uploads", {
filename: "photo.jpg",
file: buffer,
contentType: "image/jpeg",
});
// Download from downloads bucket
const file = await s3.getFile("main", "downloads", {
key: "document.pdf",
});
```
**After (v3.x):**
```typescript
import { S3Pilot } from "@d3oxy/s3-pilot";
const uploadsBucket = new S3Pilot({
region: "us-east-1",
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
bucket: "uploads",
});
const downloadsBucket = new S3Pilot({
region: "us-east-1",
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
bucket: "downloads",
});
// Upload to uploads bucket
await uploadsBucket.uploadFile({
filename: "photo.jpg",
file: buffer,
contentType: "image/jpeg",
});
// Download from downloads bucket
const file = await downloadsBucket.getFile({
key: "document.pdf",
});
```
### Benefits of v3.x
1. **Simpler API**: No need to pass client and bucket names to every method
2. **Better Type Safety**: Each instance is strongly typed to its specific bucket
3. **Cleaner Code**: Less boilerplate, more readable
4. **Bulk Operations**: Efficient bulk delete support
5. **Better Performance**: Simpler internal structure reduces overhead
### Common Patterns
#### Pattern 1: Multiple Buckets with Same Credentials
```typescript
const createBucket = (bucketName: string) =>
new S3Pilot({
region: "us-east-1",
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
bucket: bucketName,
});
const uploads = createBucket("uploads");
const downloads = createBucket("downloads");
const temp = createBucket("temp");
```
#### Pattern 2: Factory Function for Reusability
```typescript
const createS3Pilot = (config: { region: string; bucket: string }) =>
new S3Pilot({
...config,
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
});
const bucket1 = createS3Pilot({ region: "us-east-1", bucket: "bucket1" });
const bucket2 = createS3Pilot({ region: "us-west-2", bucket: "bucket2" });
```
---
## Migrating from v2.4.x to v2.5.1+ (File Download Features)
This section covers migration for file download features added in 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);
```
### 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" });
}
});
```
## Version Compatibility
- **v4.0.0+**: S3-compatible services support (R2, MinIO, Spaces), sync `getUrl()`, removed `getKeyFromUrl()`
- **v3.0.0+**: Simplified single-client, single-bucket API
- **v2.5.1+**: File download methods available (with multi-client API)
- **v2.4.x and below**: Only basic signed URLs available
## Support
If you encounter issues during migration:
1. Check the [README](./README.md) for detailed documentation
2. Review the examples in the README
3. Open an issue on GitHub with your specific use case