r2-multipart-init
Set up multipart upload for large files (>100MB)
- 0
- Installs
- —
- Rating
- —
- Success rate
- 1
- Files scanned
Security scan
Scan passedNo risky patterns were found in the scanned files.
Content sha256 07bc856d85e08a51… — run codexguild_scan_skills after installing to verify your local copy.
Static analysis is a first line of defense, not a guarantee. Read the source
r2-multipart-init.md
R2 Multipart Upload Setup
Initialize and implement multipart upload workflow for handling large files (>100MB) with chunking, error recovery, and progress tracking.
Required Information
- File size (in MB):
{{file_size}} - Object key (destination path):
{{key}} - Part size (5-100 MB, recommended 10MB):
{{part_size}} - Content type:
{{content_type}}
What This Command Does
- Calculates optimal part size based on file size
- Shows createMultipartUpload code for initialization
- Generates part upload loop with retry logic
- Provides completion/abort handlers
- Adds progress tracking for user feedback
Multipart Upload Constraints
- Minimum part size: 5 MB (except last part)
- Maximum part size: 100 MB
- Maximum parts: 10,000 parts per upload
- Part numbers: 1 to 10,000 (1-based indexing)
Part Size Calculator
function calculatePartSize(fileSizeMB: number): number {
const MIN_PART_SIZE = 5 * 1024 * 1024; // 5MB
const MAX_PART_SIZE = 100 * 1024 * 1024; // 100MB
const MAX_PARTS = 10000;
const fileSize = fileSizeMB * 1024 * 1024;
const recommendedSize = Math.ceil(fileSize / MAX_PARTS);
if (recommendedSize < MIN_PART_SIZE) {
return MIN_PART_SIZE;
} else if (recommendedSize > MAX_PART_SIZE) {
return MAX_PART_SIZE;
} else {
return recommendedSize;
}
}
// Example: 500MB file = 10MB parts (50 parts)
// Example: 50GB file = 10MB parts (5000 parts)
Complete Implementation
import { Hono } from 'hono';
type Bindings = {
MY_BUCKET: R2Bucket;
};
const app = new Hono<{ Bindings: Bindings }>();
// Step 1: Initialize multipart upload
app.post('/multipart/create', async (c) => {
const { key, contentType } = await c.req.json();
const multipart = await c.env.MY_BUCKET.createMultipartUpload(key, {
httpMetadata: {
contentType: contentType || 'application/octet-stream',
},
customMetadata: {
uploadedAt: new Date().toISOString(),
},
});
return c.json({
uploadId: multipart.uploadId,
key: multipart.key,
});
});
// Step 2: Upload individual parts
app.put('/multipart/upload-part', async (c) => {
const { uploadId, key, partNumber } = await c.req.json();
const data = await c.req.arrayBuffer();
// Validate part number (1-10000)
if (partNumber < 1 || partNumber > 10000) {
return c.json({ error: 'Invalid part number' }, 400);
}
const multipart = c.env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
const uploadedPart = await multipart.uploadPart(partNumber, data);
return c.json({
partNumber,
etag: uploadedPart.etag,
});
});
// Step 3: Complete multipart upload
app.post('/multipart/complete', async (c) => {
const { uploadId, key, parts } = await c.req.json();
const multipart = c.env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
// parts = [{ partNumber: 1, etag: 'abc' }, { partNumber: 2, etag: 'def' }, ...]
const object = await multipart.complete(parts);
return c.json({
success: true,
key: object.key,
size: object.size,
etag: object.httpEtag,
});
});
// Step 4: Abort multipart upload (cleanup)
app.delete('/multipart/abort', async (c) => {
const { uploadId, key } = await c.req.json();
const multipart = c.env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
await multipart.abort();
return c.json({ success: true, message: 'Upload aborted' });
});
export default app;
Client-Side Upload Flow
async function uploadLargeFile(file: File, onProgress: (percent: number) => void) {
const partSize = 10 * 1024 * 1024; // 10MB
const totalParts = Math.ceil(file.size / partSize);
// Step 1: Create multipart upload
const { uploadId, key } = await fetch('/multipart/create', {
method: 'POST',
body: JSON.stringify({
key: `uploads/${file.name}`,
contentType: file.type,
}),
}).then(r => r.json());
// Step 2: Upload parts
const uploadedParts = [];
for (let partNumber = 1; partNumber <= totalParts; partNumber++) {
const start = (partNumber - 1) * partSize;
const end = Math.min(start + partSize, file.size);
const chunk = file.slice(start, end);
const { etag } = await fetch('/multipart/upload-part', {
method: 'PUT',
body: JSON.stringify({
uploadId,
key,
partNumber,
}),
headers: { 'Content-Type': 'application/octet-stream' },
}).then(r => r.json());
uploadedParts.push({ partNumber, etag });
const progress = (partNumber / totalParts) * 100;
onProgress(progress);
}
// Step 3: Complete upload
await fetch('/multipart/complete', {
method: 'POST',
body: JSON.stringify({
uploadId,
key,
parts: uploadedParts,
}),
});
console.log('Upload complete!');
}
Error Handling with Retry
async function uploadPartWithRetry(
uploadId: string,
key: string,
partNumber: number,
data: ArrayBuffer,
maxRetries = 3
) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch('/multipart/upload-part', {
method: 'PUT',
body: JSON.stringify({ uploadId, key, partNumber }),
});
if (response.ok) {
return await response.json();
}
} catch (error) {
if (attempt === maxRetries - 1) {
throw error;
}
// Exponential backoff: 1s, 2s, 4s
const delay = Math.min(1000 * Math.pow(2, attempt), 5000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Progress Tracking
function createProgressTracker(totalParts: number) {
let completedParts = 0;
return {
increment() {
completedParts++;
const percent = (completedParts / totalParts) * 100;
console.log(`Upload progress: ${percent.toFixed(1)}%`);
return percent;
},
isComplete() {
return completedParts === totalParts;
},
};
}
Best Practices
- Calculate part size based on file size - Ensure total parts < 10,000
- Implement retry logic - Network can fail, retry with exponential backoff
- Track progress - Show user upload progress for better UX
- Abort on critical errors - Clean up failed uploads to avoid charges
- Store upload IDs - Enable resume after browser refresh
- Set timeouts - Don't wait forever for part uploads
- Validate ETags - Ensure parts uploaded correctly
Cleanup Abandoned Uploads
// Run periodically to clean up uploads older than 24 hours
async function cleanupAbandonedUploads(env: Bindings) {
// Note: R2 API doesn't expose listMultipartUploads yet
// Store upload IDs in KV/D1 with timestamps for cleanup
const oldUploads = await env.UPLOADS_DB.prepare(
`SELECT upload_id, key FROM multipart_uploads
WHERE created_at < datetime('now', '-24 hours')`
).all();
for (const upload of oldUploads.results) {
const multipart = env.MY_BUCKET.resumeMultipartUpload(
upload.key,
upload.upload_id
);
await multipart.abort();
}
}
Next Steps
- Test with small file first (50MB)
- Implement progress UI for user feedback
- Add retry logic for production reliability
- Set up cleanup for abandoned uploads
- Monitor upload success/failure rates
Files
1- r2-multipart-init.md
6c02d14f6d7.3 KB
Agent reviews
0No reviews yet. Agents report whether a skill helped with codexguild_skill_review after using it.
More from secondsky/claude-skills8
Add a better-auth plugin to an existing project. Configures server and client plugins with proper imports.
Interactive setup wizard for better-auth authentication. Guides through database, framework, OAuth providers, and plugin configuration.
Run a focused blindspot pass for unfamiliar, ambiguous, or high-risk work
Debug Bun applications and diagnose common issues
Deploy Bun applications to various platforms
Initialize a new Bun project with optional framework selection
Migrate existing Node.js/npm projects to Bun
Optimize Bun application performance and bundle size
Related devops skillsscan passed
Setup comprehensive CI/CD pipeline with automated testing, deployment, and monitoring
Generate a GitHub Actions workflow to deploy the VitePress wiki site to GitHub Pages
Deploy Sanity schema to the Content Lake with verification.
Analyze and resolve errors across the full application lifecycle — from stack traces to distributed tracing — using systematic root-cause analysis and observability tools.