Skip to content
Development
Command

/r2-multipart-init

Set up multipart upload for large files (>100MB)

From plugin
secondsky-claude-skills
20466 skills46 agents66 commands
Install
$ npx -y skills add secondsky/claude-skills --agent claude-code

How it fires

How this command gets triggered: by you, by Claude, or both.

  • Fires itselfClaude auto-loads it when your prompt matches the work.
  • You can call itInvoke it directly when you want it.
  • Slash command/r2-multipart-init

Context preview

What this command does when you run it.

Set up multipart upload for large files (>100MB)

Command definition

r2-multipart-init.md
name: cloudflare-r2:multipart-init
description: Set up multipart upload for large files (>100MB)

R2 Multipart Upload Setup

Initialize and implement multipart upload workflow for handling large files (>100MB) with chunking, error recovery, and progress tracking.

Required Information

1. **File size** (in MB): `{{file_size}}` 2. **Object key** (destination path): `{{key}}` 3. **Part size** (5-100 MB, recommended 10MB): `{{part_size}}` 4. **Content type**: `{{content_type}}`

What This Command Does

1. **Calculates optimal part size** based on file size 2. **Shows createMultipartUpload code** for initialization 3. **Generates part upload loop** with retry logic 4. **Provides completion/abort handlers** 5. **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)}%`);
Read more
Ships withsecondsky-claude-skills

142 production-ready skills for Claude Code CLI ๐Ÿ”Œ Platform / Harness Support These plugins ship as Claude Code marketplace plugins (.claude-plugin/ manifests) and Codex CLI plugins (.codex-plugin/ manifests).

Get the whole plugin, auto-invoked