Open3

Remix のアプリからユーザーが送信した画像を Cloudflare R2 にアップロードして Cloudflare D1 に保存する

ピン留めされたアイテム
ktgwShotaktgwShota

前提

目的

  • input の file フィールドから送信された画像を R2 にアップロードし、その URL を DB` に保存する
export async function action({ context, request }: ActionFunctionArgs) {
  try {
    const formData = await request.formData();
    const { title, text, imageFile, author, errors } =
      validateFormData(formData);

    if (Object.keys(errors).length > 0) {
      return json({ title, text, imageFile, author, errors });
    }

    // TODO: ここの関数を作成する
    const imageUrl = await uploadImageFile(imageFile);

    const database = context.cloudflare.env.DB;
    const query = `INSERT INTO Recipes (title, text, imageUrl, author) VALUES ('${title}', '${text}', '${imageUrl}', '${author}')`;
    await database.prepare(query).run();
  } catch (e) {
    return redirect("/error");
  }

  return redirect("/recipes");
}
ktgwShotaktgwShota

Cloudflare R2 に画像をアップロードするための準備

1. バケットを作成

  • cloudflare のダッシュボードの R2 → 概要 → バケット作成

2. APIトークンを作成

  • cloudflare のダッシュボードの R2 → 概要 → R2 API トークンの管理 → APIトークンを作成

  • 以下のリクエストで取得したトークンがアクティブ状態になっているか確認できる

curl -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
     -H "Authorization: Bearer 取得したトークンをコピペ" 

3. アプリとバケットを連携

  • wrangler.toml に以下のコードを追加
[[r2_buckets]]
binding = "R2" # アプリ内で使用する変数名なので何でもOK
bucket_name = "image" # 作成したバケット名

4. エンドポイントを作成

  • worker(server.ts)に以下のコードを追加
interface Env {
  DB: D1Database;
  R2: R2Bucket;
}

export default {
  async fetch(request: Request, env: Env) {
    const url = new URL(request.url);
    const key = url.pathname.slice(1); // URLからファイル名を取得

    if (request.method === "PUT") {
      await env.R2.put(key, request.body); // R2バケットにファイルを保存
      return new Response(`Put ${key} successfully!`, { status: 200 });
    }

    return new Response("Method not allowed", { status: 405 }); // PUT以外のリクエストを拒否
  },
};
  • worker をデプロイ
npx wrangler deploy
  • 以下のリクエストが成功すれば R2 のダッシュボードからアップロードされた画像を確認できる
curl -X PUT "https://{xxx}.dev/images/IMG_0275.png" \
     --upload-file /Users/ktgwshota/Downloads/IMG_0275.png
ktgwShotaktgwShota

目的の関数を作る

async function uploadImageFile(imageFile: File): Promise<string> {
  const uuid = v4();
  const createPath = `images/${uuid}.${imageFile.name}`;

  const response = await fetch(
    `https://{xxx}.dev/${createPath}`,
    {
      method: "PUT",
      body: imageFile,
    }
  );

  if (!response.ok) {
    throw new Error("画像のアップロードに失敗しました");
  }

  // FIXME: ローカル用のURLなので、本番で運用する場合は考える必要がある
  // ref: https://developers.cloudflare.com/r2/buckets/public-buckets/#enable-managed-public-access
  const imageUrl = `https://{xxx}.r2.dev/${createPath}`;

  return imageUrl;
}