ANTIGRAVITY LABEN
記事一覧/アプリ開発
アプリ開発/2026-03-28中級

Antigravity × Cloudflare R2 でファイルアップロード&画像最適化パイプラインを構築する

Antigravity の AI エージェントを活用して Cloudflare R2 へのファイルアップロード機能と画像最適化パイプラインを構築する方法を、コード例付きで実践的に解説します。

antigravity436cloudflare6r22file-uploadimage-optimizationworkers3

取り組みの背景 — なぜ Cloudflare R2 なのか

Web アプリケーション開発において、ファイルアップロードとストレージは避けて通れない課題です。ユーザーアバター、商品画像、ドキュメントなど、あらゆるアプリでファイル管理が必要になります。

Cloudflare R2 は、S3 互換のオブジェクトストレージでありながらエグレス料金が無料という大きなメリットを持っています。AWS S3 では帯域幅に応じた課金が発生しますが、R2 ではデータの読み出し(ダウンロード)にかかる帯域コストがゼロです。画像や動画を多く配信するアプリにとって、これは運用コストの大幅な削減につながります。

ここではAntigravity の AI エージェントを使って Cloudflare R2 へのファイルアップロード機能と画像最適化パイプラインを効率的に構築する方法を、ステップバイステップで解説します。

この記事で学べること

  • Cloudflare R2 バケットのセットアップと Workers との連携方法
  • Antigravity エージェントを活用した API ルート&型定義の自動生成
  • アップロード時の画像リサイズ・WebP 変換パイプラインの構築
  • 署名付き URL によるセキュアなアップロードフローの実装
  • 本番運用で役立つエラーハンドリングとバリデーション

対象読者

  • Cloudflare Workers / R2 を使ったことがある方(初歩的な知識がある前提)
  • ファイルアップロード機能を自分のアプリに実装したい方
  • 画像最適化の自動化に興味がある方

Cloudflare R2 の基本セットアップ

まず Antigravity のターミナルで Wrangler CLI を使い、R2 バケットを作成します。Antigravity のエージェントに「R2 バケットを作成して wrangler.toml に追記して」と指示すれば、以下のような手順を自動実行してくれます。

# R2 バケットを作成
npx wrangler r2 bucket create my-app-uploads
 
# 作成されたバケットを確認
npx wrangler r2 bucket list

次に wrangler.toml にバインディングを追加します。

# wrangler.toml
name = "my-upload-worker"
main = "src/index.ts"
compatibility_date = "2026-03-01"
 
[[r2_buckets]]
binding = "UPLOADS_BUCKET"
bucket_name = "my-app-uploads"

この設定により、Worker コード内で env.UPLOADS_BUCKET として R2 バケットにアクセスできるようになります。

Antigravity エージェントでアップロード API を自動生成する

Antigravity のエージェント機能を活用すると、アップロード API の雛形を素早く生成できます。エージェントに以下のように指示してみましょう。

「Cloudflare Workers で R2 にファイルをアップロードする API を TypeScript で作成して。CORS 対応、ファイルサイズ制限(10MB)、許可する MIME タイプの制限を含めてください。」

Antigravity が生成するコードは以下のようになります。

// src/upload.ts
// R2 ファイルアップロードハンドラー
 
interface Env {
  UPLOADS_BUCKET: R2Bucket;
  ALLOWED_ORIGINS: string;
}
 
// 許可するMIMEタイプ
const ALLOWED_TYPES = [
  "image/jpeg",
  "image/png",
  "image/webp",
  "image/gif",
  "application/pdf",
];
 
// 最大ファイルサイズ: 10MB
const MAX_FILE_SIZE = 10 * 1024 * 1024;
 
export async function handleUpload(
  request: Request,
  env: Env
): Promise<Response> {
  // CORS プリフライト対応
  if (request.method === "OPTIONS") {
    return new Response(null, {
      headers: corsHeaders(env.ALLOWED_ORIGINS),
    });
  }
 
  if (request.method !== "POST") {
    return jsonResponse({ error: "Method not allowed" }, 405);
  }
 
  try {
    const formData = await request.formData();
    const file = formData.get("file") as File | null;
 
    if (!file) {
      return jsonResponse({ error: "No file provided" }, 400);
    }
 
    // ファイルサイズチェック
    if (file.size > MAX_FILE_SIZE) {
      return jsonResponse(
        { error: `File size exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit` },
        413
      );
    }
 
    // MIMEタイプチェック
    if (!ALLOWED_TYPES.includes(file.type)) {
      return jsonResponse(
        { error: `File type ${file.type} is not allowed` },
        415
      );
    }
 
    // ユニークなキーを生成
    const key = generateKey(file.name);
 
    // R2 にアップロード
    await env.UPLOADS_BUCKET.put(key, file.stream(), {
      httpMetadata: {
        contentType: file.type,
      },
      customMetadata: {
        originalName: file.name,
        uploadedAt: new Date().toISOString(),
      },
    });
 
    return jsonResponse(
      {
        success: true,
        key,
        url: `/files/${key}`,
        size: file.size,
        type: file.type,
      },
      201,
      corsHeaders(env.ALLOWED_ORIGINS)
    );
  } catch (err) {
    console.error("Upload error:", err);
    return jsonResponse({ error: "Upload failed" }, 500);
  }
}
 
// ユニークなファイルキーを生成
function generateKey(originalName: string): string {
  const timestamp = Date.now();
  const random = Math.random().toString(36).substring(2, 8);
  const ext = originalName.split(".").pop() || "bin";
  return `uploads/${timestamp}-${random}.${ext}`;
}
 
// CORS ヘッダー
function corsHeaders(origin: string): HeadersInit {
  return {
    "Access-Control-Allow-Origin": origin,
    "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
  };
}
 
// JSON レスポンスヘルパー
function jsonResponse(
  data: Record<string, unknown>,
  status: number,
  headers?: HeadersInit
): Response {
  return new Response(JSON.stringify(data), {
    status,
    headers: {
      "Content-Type": "application/json",
      ...headers,
    },
  });
}

エージェントが生成したコードには CORS 対応、ファイルサイズ制限、MIME タイプ検証がすべて含まれています。この出力をベースに、自分のプロジェクトに合わせてカスタマイズしていく流れが効率的です。

画像最適化パイプラインの構築

ファイルアップロードだけでなく、アップロードされた画像を自動的にリサイズ・WebP 変換する処理を追加しましょう。Cloudflare Workers では cf.image プロパティを使ったリサイズが可能ですが、ここでは R2 への保存時に複数サイズのバリアントを生成するアプローチを紹介します。

// src/image-optimizer.ts
// 画像最適化パイプライン
 
interface ImageVariant {
  suffix: string;
  width: number;
  quality: number;
}
 
// 生成するバリアント定義
const VARIANTS: ImageVariant[] = [
  { suffix: "thumb", width: 150, quality: 75 },
  { suffix: "medium", width: 600, quality: 80 },
  { suffix: "large", width: 1200, quality: 85 },
];
 
export async function optimizeAndStore(
  file: File,
  key: string,
  bucket: R2Bucket
): Promise<{ original: string; variants: Record<string, string> }> {
  // オリジナルを保存
  const originalKey = `originals/${key}`;
  await bucket.put(originalKey, file.stream(), {
    httpMetadata: { contentType: file.type },
  });
 
  const variantKeys: Record<string, string> = {};
 
  // Cloudflare Image Resizing を利用してバリアント生成
  for (const variant of VARIANTS) {
    const variantKey = `optimized/${variant.suffix}/${key.replace(
      /\.[^.]+$/,
      ".webp"
    )}`;
 
    // Image Resizing API を使ったリサイズリクエスト
    const resizeUrl = new URL(`https://your-domain.com/originals/${key}`);
    const resizedResponse = await fetch(resizeUrl.toString(), {
      cf: {
        image: {
          width: variant.width,
          quality: variant.quality,
          format: "webp",
          fit: "cover",
        },
      },
    });
 
    if (resizedResponse.ok) {
      await bucket.put(variantKey, resizedResponse.body, {
        httpMetadata: { contentType: "image/webp" },
      });
      variantKeys[variant.suffix] = variantKey;
    }
  }
 
  return { original: originalKey, variants: variantKeys };
}

このパイプラインにより、ユーザーが1枚の画像をアップロードするだけで、サムネイル(150px)・中サイズ(600px)・大サイズ(1200px)の WebP バリアントが自動生成されます。

署名付き URL によるセキュアなアップロード

クライアントから直接 R2 にアップロードさせたい場合は、署名付き URL(Presigned URL)を使います。サーバー側で署名付き URL を発行し、クライアントはその URL に対して直接ファイルを PUT するフローです。

// src/presigned.ts
// 署名付きURL発行エンドポイント
 
import {
  S3Client,
  PutObjectCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
 
interface Env {
  R2_ACCESS_KEY_ID: string;
  R2_SECRET_ACCESS_KEY: string;
  R2_ACCOUNT_ID: string;
  R2_BUCKET_NAME: string;
}
 
export async function generatePresignedUrl(
  env: Env,
  filename: string,
  contentType: string
): Promise<{ uploadUrl: string; key: string }> {
  const s3 = new S3Client({
    region: "auto",
    endpoint: `https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
    credentials: {
      accessKeyId: env.R2_ACCESS_KEY_ID,
      secretAccessKey: env.R2_SECRET_ACCESS_KEY,
    },
  });
 
  const key = `uploads/${Date.now()}-${filename}`;
 
  const command = new PutObjectCommand({
    Bucket: env.R2_BUCKET_NAME,
    Key: key,
    ContentType: contentType,
  });
 
  // 署名付きURLの有効期限: 15分
  const uploadUrl = await getSignedUrl(s3, command, {
    expiresIn: 900,
  });
 
  return { uploadUrl, key };
}

クライアント側の実装は以下のようになります。

// フロントエンド: 署名付きURLを取得してアップロード
async function uploadFile(file: File): Promise<string> {
  // Step 1: サーバーから署名付きURLを取得
  const res = await fetch("/api/presigned-url", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      filename: file.name,
      contentType: file.type,
    }),
  });
  const { uploadUrl, key } = await res.json();
 
  // Step 2: 署名付きURLに直接アップロード
  await fetch(uploadUrl, {
    method: "PUT",
    headers: { "Content-Type": file.type },
    body: file,
  });
 
  return key;
}

この方式では、ファイルデータがアプリケーションサーバーを経由しないため、サーバーの負荷を大幅に軽減できます。大容量ファイルのアップロードに特に有効です。

本番運用のためのベストプラクティス

Antigravity のエージェントに「本番運用を意識したエラーハンドリングとリトライロジックを追加して」と依頼すると、以下のような堅牢な実装を提案してくれます。

// src/upload-with-retry.ts
// リトライ付きアップロード関数
 
async function uploadWithRetry(
  bucket: R2Bucket,
  key: string,
  body: ReadableStream,
  metadata: R2PutOptions,
  maxRetries = 3
): Promise<R2Object> {
  let lastError: Error | undefined;
 
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const result = await bucket.put(key, body, metadata);
      if (result === null) {
        throw new Error("R2 put returned null");
      }
      return result;
    } catch (err) {
      lastError = err as Error;
      console.warn(
        `Upload attempt ${attempt}/${maxRetries} failed: ${lastError.message}`
      );
 
      if (attempt < maxRetries) {
        // 指数バックオフ: 1秒 → 2秒 → 4秒
        await new Promise((r) =>
          setTimeout(r, Math.pow(2, attempt - 1) * 1000)
        );
      }
    }
  }
 
  throw new Error(
    `Upload failed after ${maxRetries} attempts: ${lastError?.message}`
  );
}

運用時に気をつけるポイント

  • ファイル名のサニタイズ: ユーザーが入力するファイル名には危険な文字が含まれる可能性があります。キーを生成する際はタイムスタンプ+ランダム文字列を使い、オリジナルのファイル名はメタデータとして保存しましょう
  • Content-Type の検証: クライアントが送信する Content-Type ヘッダーだけでなく、ファイルのマジックバイト(先頭数バイト)も検証すると、偽装されたファイルを防げます
  • バケットのライフサイクルルール: 一時ファイルや古いバリアントを自動削除するルールを R2 のダッシュボードから設定しておくと、ストレージコストを抑えられます
  • アクセス制御: 公開すべきファイル(プロフィール画像など)と非公開ファイル(請求書PDFなど)は別バケットに分けるか、キープレフィックスで区別します

Cloudflare Workers でのセキュアな API 構築パターンについて、さらに詳しい認証・バリデーション・レート制限の実装例を知りたい方は Antigravity エージェントでセキュアな API バックエンドを構築する で体系的に解説しています。

R2 + Workers AI で画像解析を追加する

Cloudflare の Workers AI を組み合わせると、アップロード画像に対する自動タグ付けや不適切コンテンツの検出も実現できます。

// src/image-analysis.ts
// Workers AI による画像解析
 
interface Env {
  AI: Ai;
  UPLOADS_BUCKET: R2Bucket;
}
 
export async function analyzeImage(
  env: Env,
  imageKey: string
): Promise<{ labels: string[]; safe: boolean }> {
  // R2 から画像を取得
  const object = await env.UPLOADS_BUCKET.get(imageKey);
  if (!object) {
    throw new Error("Image not found");
  }
 
  const imageBytes = await object.arrayBuffer();
 
  // Workers AI で画像分類
  const classificationResult = await env.AI.run(
    "@cf/microsoft/resnet-50",
    {
      image: [...new Uint8Array(imageBytes)],
    }
  );
 
  // 上位5ラベルを取得
  const labels = classificationResult
    .slice(0, 5)
    .map((r: { label: string }) => r.label);
 
  return {
    labels,
    safe: true, // 実際のプロジェクトではNSFW検出モデルを使用
  };
}

Workers AI と R2 を組み合わせたエッジ AI アプリの構築方法については Antigravity エージェント × Cloudflare Workers AI でエッジ AI アプリを構築する でさらに深く掘り下げています。

まとめ

ここではAntigravity の AI エージェントを活用して Cloudflare R2 ベースのファイルアップロード&画像最適化パイプラインを構築する方法を解説しました。R2 のエグレス無料という強みを活かせば、画像配信コストを大幅に抑えながらスケーラブルなファイル管理基盤を作れます。

Antigravity のエージェントに実装方針を伝えるだけで、型安全なアップロード API、バリデーション、リトライロジックまで自動生成してくれるのは、開発効率の観点で非常に大きなメリットです。まずは小さなプロジェクトで R2 バケットを作成し、実際にアップロード機能を動かしてみてください。

シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

アプリ開発2026-04-26
Antigravity × Cloudflare エッジSaaS設計ガイド: D1・Workers・R2・Workers AIを統合した本番アーキテクチャ
Antigravityを使ってCloudflare D1・Workers・R2・Workers AIを統合したエッジSaaSを本番で動かすための設計ガイド。N+1問題・CPU制限・コスト管理など実際の落とし穴と解決策を網羅します。
アプリ開発2026-04-29
Antigravity × Cloudflare Vectorize:エッジで動く本番 RAG パイプラインを構築する
Cloudflare Vectorize を使ったエッジ RAG パイプラインを Antigravity で構築する本番ガイド。チャンク分割・ハイブリッド検索・コスト設計・観測まで個人開発で運用できる粒度で解説します。
アプリ開発2026-06-14
エッジキャッシュが Next.js の空ページを固定化していた原因と、cache-worker のガード設計
ユーザーから「時々読み込みエラーになる」と報告されるのに手元では再現しない。SSR の例外が HTTP 200 で配信され、それをエッジキャッシュが固定化していた原因を Antigravity のエージェントで切り分け、cache-worker にキャッシュ拒否ガードを実装した記録です。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →