React Router v7でCloudflare R2にファイルアップロード

2025-02-15 React Router · Cloudflare · 趣味開発

ふと思い立って趣味開発がしたくなりできるだけ安くでできないかを模索したところ、Cloudflare の Workers, R2, D1 を使えば Free Plan で RDB を使いつつそれなりのことが出来そうな雰囲気を感じた。

まずは R2 にファイルをアップロードを試すため、以下を実現するサンプルを実装した。

  1. フォームからファイル送信する
  2. ファイルを受け取って R2 に保存する
  3. フォームからではなく Fetchers を使ってファイルを送信する

Cloudflare R2 とは?

Cloudflare R2 は、AWS S3 のようなオブジェクトストレージサービス。

  • Cloudflare R2 | エグレス料金ゼロのオブジェクトストレージ
  • S3 互換オブジェクトストレージソリューション

プロジェクトのセットアップ

テンプレートを利用して React Router プロジェクトを作成する。

pnpm dlx create-react-router@latest --template remix-run/react-router-templates/cloudflare

GitHub のリポジトリと連携する場合は wrangler.toml の name をリポジトリ名と一致するよう修正する(ように警告が出る)。

R2 の Bucket を作成

以下のコマンドで R2 のバケットを作成する。

pnpm exec wrangler r2 bucket create <bucket-name>

次に、バケットへのアクセス設定を wrangler.toml に記述する。

# wrangler.toml

[[r2_buckets]]
binding = "BUCKET" # context.cloudflare.env に bind される
bucket_name = "<bucket-name>"

# ついでにログを有効にする
[observability.logs]
enabled = true

フォームからのファイルアップロード

import { parseFormData, type FileUpload } from "@mjackson/form-data-parser";
import type { ActionFunctionArgs } from "react-router";

type AppEnv = {
  BUCKET: R2Bucket; // wrangler.toml の binding に設定した名前になる
};

export async function action({ request, context }: ActionFunctionArgs) {
  const env = context.cloudflare.env as AppEnv;
  const uploadHandler = async (fileUpload: FileUpload) => {
    if (fileUpload.fieldName === "file") {
      const { name } = fileUpload;
      await env.BUCKET.put(name, await fileUpload.arrayBuffer());
    }
  };

  await parseFormData(request, uploadHandler);
  return Response.json({ message: "File uploaded" });
}

export default function Form() {
  return (
    <div className="w-full p-4">
      <div className="md:w-1/2 mx-auto">
        <form method="post" encType="multipart/form-data">
          <label
            htmlFor="file-input"
            className="block mb-2 text-sm font-medium"
          >
            Upload a file
          </label>
          <input
            className="block w-full p-2 text-sm border border-gray-300 rounded-lg cursor-pointer focus:outline-none"
            id="file-input"
            type="file"
            name="file"
          />
          <div className="h-4" />
          <button className="p-2 text-sm border border-gray-300 rounded-lg focus:ring-4 focus:ring-blue-300">
            Upload
          </button>
        </form>
      </div>
    </div>
  );
}

Fetch API を使ったファイルアップロード

import { parseFormData, type FileUpload } from "@mjackson/form-data-parser";
import { useFetcher, type ActionFunctionArgs } from "react-router";

type AppEnv = {
  BUCKET: R2Bucket;
};

export async function action({ request, context }: ActionFunctionArgs) {
  const env = context.cloudflare.env as AppEnv;
  const uploadHandler = async (fileUpload: FileUpload) => {
    if (fileUpload.fieldName === "file") {
      const { name } = fileUpload;
      await env.BUCKET.put(name, await fileUpload.arrayBuffer());
    }
  };

  await parseFormData(request, uploadHandler);
  return Response.json({ message: "File uploaded" });
}

export default function Home() {
  const fetcher = useFetcher();
  const upload = async () => {
    const formData = new FormData();
    formData.append(
      "file",
      new Blob(["Hello, world!"], { type: "text/plain" }),
      "hello.txt",
    );

    await fetcher.submit(formData, {
      encType: "multipart/form-data",
      method: "post",
    });
  };

  return (
    <div className="w-full p-4">
      <div className="md:w-1/2 mx-auto">
        <button
          className="p-2 text-sm border border-gray-300 rounded-lg focus:ring-4 focus:ring-blue-300"
          onClick={() => upload()}
        >
          サンプルファイルアップロードボタン
        </button>
      </div>
    </div>
  );
}

まとめ

Cloudflare の workers, R2 を初めて触ったが、かなりスムーズに実装できた。

R2 の実装がこうなら D1 もこんな感じか? という予想がつくので趣味開発環境としてかなり期待できそう。

参考

  • Securely access and upload assets with Cloudflare R2

  • File Uploads | React Router

  • Remix で Cloudflare R2 にアップロードしてみる

Michiaki Mizoguchi

Michiaki Mizoguchi

Software Developer / Engineering Manager

Home Resume GitHub