ふと思い立って趣味開発がしたくなりできるだけ安くでできないかを模索したところ、Cloudflare の Workers, R2, D1 を使えば Free Plan で RDB を使いつつそれなりのことが出来そうな雰囲気を感じた。
まずは R2 にファイルをアップロードを試すため、以下を実現するサンプルを実装した。
- フォームからファイル送信する
- ファイルを受け取って R2 に保存する
- フォームからではなく Fetchers を使ってファイルを送信する
Cloudflare R2 とは?
Cloudflare R2 は、AWS 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 もこんな感じか? という予想がつくので趣味開発環境としてかなり期待できそう。