🔖

Next.jsからCloudflare R2に画像をアップロードして使用

2024/09/02に公開

はじめに

この記事では、next.jsからCloudflare R2に画像をアップロードしてサイト内で使用する方法を解説します。


https://github.com/s1f10210273/next-cloudflare-sample

手順

1.ライブラリのインストール

Cloudflare R2はAWS S3のAPIと互換性があり、aws-sdkを使用することができます。

npm install @aws-sdk/client-s3

2.クレカ登録とか

以下のページより、R2の利用を開始しましょう
クレジットカードの登録をすると利用可能になります
https://www.cloudflare.com/ja-jp/developer-platform/r2/

料金設定はこのようになっており、個人で開発する程度では無料で使用することができます

無料枠 月額
ストレージ 10 GB / 月 $0.015 / GB ストレージ
クラスA操作:状態を変更する 100万 / 月 $4.50 / 100万
クラスB操作:既存の状態を読み取る 1000万 / 月 $0.36 / 100万

3.バケットの設定

ログインしたら左のサイドバーから、R2のOverviewページに進みましょう

次に、Create Bucketを押して次の画面に進んでください。


そうしたら、Bucket nameに任意の名前を入力し、Create Bucketを押してください

作成したバケットのSettingsに移動して、R2.dev subdomainを有効にしましょう。
Allow Accessを押すと有効にできます
出てきた、Public R2.dev Bucket URLを環境変数に登録しましょう
next.jsのプロジェクトのルートに.envファイルを作成して追加します。

.env
R2_BUCKET_URL=https://pub-XXXX.r2.dev

4.APIトークンの作成

R2のOverview内のManage R2 API Tokensから、APIトークンの作成ができます
PermissionsをObject Read & WriteにしてAPIトークンを作成してください。

作成したトークンを環境変数に追加しましょう。

.env
R2_ACCESS_KEY=XXXX
R2_SECRET_KEY=XXXX
R2_ENDPOINT=https://XXXX.r2.cloudflarestorage.com

5.APIの作成

Next.jsで画像をアップロードするためのAPIを作成しましょう

app/api/upload/route.tsx
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  try {
    const formData = await req.formData();
    const imageFileData = formData.get("imageFileData") as File;
    const imageFileDataArrayBuffer = await imageFileData.arrayBuffer();
    const imageFileDataBuffer = Buffer.from(imageFileDataArrayBuffer);

    const s3 = new S3Client({
      region: "auto",
      endpoint: process.env.R2_ENDPOINT!,
      credentials: {
        accessKeyId: process.env.R2_ACCESS_KEY!,
        secretAccessKey: process.env.R2_SECRET_KEY!,
      },
    });

    const key = `images/${Date.now()}_${imageFileData.name}`;

    await s3.send(
      new PutObjectCommand({
        Bucket: "test",
        Key: key,
        ContentType: imageFileData.type,
        Body: imageFileDataBuffer,
        ACL: "public-read",
      })
    );

    const uploadedUrl = `${process.env.R2_ENDPOINT}/test/${key}`;
    const uploadedUrlUse = `${process.env.R2_ENDPOINT_DEV}/${key}`;

    return NextResponse.json({
      message: "アップロードに成功しました。",
      url: uploadedUrlUse,
    });
  } catch (error) {
    console.error("Error uploading file:", error);
    return NextResponse.json(
      {
        message: "アップロードに失敗しました。",
        error: error.message,
      },
      { status: 500 }
    );
  }
}

5.アップロードフォームの作成

app/page.tsx
"use client";

import { useState } from "react";
import { TbPhotoPlus } from "react-icons/tb";

export default function Home() {
  const [uploading, setUploading] = useState(false);
  const [url, setUrl] = useState("");

  const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    setUploading(true);

    const formData = new FormData();
    formData.append("imageFileData", file);

    try {
      const res = await fetch("/api/upload", {
        method: "POST",
        body: formData,
      });

      if (res.ok) {
        const data = await res.json();
        setUrl(data.url);
      } else {
        console.error("Upload failed.");
      }
    } catch (error) {
      console.error("An error occurred while uploading the image:", error);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div className="flex items-center justify-center h-screen">
      <div className="relative bg-white p-4 border rounded-lg shadow-lg max-w-lg w-full">
        {url ? (
          <>
            <h2 className="text-lg font-semibold mb-4 text-center">
              アップロードされた画像
            </h2>
            <div className="flex justify-center">
              <img
                src={url}
                alt="Uploaded"
                className="object-cover max-h-80 max-w-full"
              />
            </div>
          </>
        ) : (
          <>
            <h2 className="text-lg font-semibold mb-4 text-center">
              画像をアップロード
            </h2>
            <input
              type="file"
              onChange={handleFileChange}
              className="hidden"
              id="file-input"
              accept="image/*"
            />
            <label htmlFor="file-input">
              <div
                className={`relative flex h-80 cursor-pointer flex-col items-center justify-center gap-4 border-2 border-dashed border-neutral-300 transition hover:opacity-70 ${
                  uploading ? "opacity-50" : ""
                }`}
              >
                <TbPhotoPlus size={50} />
              </div>
            </label>
          </>
        )}
      </div>
    </div>
  );
}

おわりに

サブドメインを介したパブリック アクセスはr2.devレート制限されており、開発目的でのみ使用する必要があります。
アクセス管理、キャッシュ、ボット管理機能を有効にするには、バケットへのパブリック アクセスを有効にするときにカスタム ドメインを設定する必要があります。

今回、アップロードした画像を表示するのに使用しているr2.devは上記の通り、レート制限されています。
実際に公開するアプリなどで使用する場合は、別にドメインを設定しましょう

参考文献

https://developers.cloudflare.com/r2/examples/aws/aws-sdk-js-v3/
https://developers.cloudflare.com/r2/buckets/public-buckets/#enable-managed-public-access

Discussion