Open10

cloudflareのR2というストレージサービスがあるみたいなので調査と使ってみる

gontagonta

色々とまずは見てみる

https://developers.cloudflare.com/r2/get-started/

gontagonta

とりあえずこれを見て進める
https://developers.cloudflare.com/r2/get-started/

gontagonta
  1. アカウントを作る
  2. クレジットを登録
  3. バケットを登録する

で確認ができた。
画像もアップロードをしたがデフォルトでは非公開になるっぽい

gontagonta

バケットをデフォルトで公開にするにはカスタムドメインをcloudflareのDNSに登録をする必要があるっぽい。
とりあえずcloudflareで適当なドメインを買って、バケットに紐づけて購入をしたドメインを登録をしてみた。

まずはバケットを登録する

パブリックアクセスのカスタムドメインを登録する。

バケットごとに別のドメインを登録しておかないとエラーが出るのでサブドメインを適当に指定をして別のドメインで登録をする。

数分経つとアクティブになる。

カスタムドメインが表示をされてこれでアクセスができるようになる。

gontagonta

ファイルをアップロードできるようにする(サーバーでアップロード)

やることは

api tokenを発行する。

必要に応じて厳しくする等の対応が必要かと思うのですが、特段問題がなければ下記のような設定で良いと思います。

fileのデータをサーバー側に送り、S3Clientでs3のインスタンスを作り、PutObjectCommandで画像をアップロードする。
詳細は下記をご確認ください。

https://zenn.dev/shuei/articles/3f1f274cb3a983

gontagonta

署名付きURLを発行してブラウザでアップロードできるようにする

こちらのcreatePresignedUrlWithClientを使ってアップロードをする。
https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/example_s3_Scenario_PresignedUrl_section.html

かなり雑に書いています。zod等を使ってparseして型付けをするなり、エラー処理をちゃんとやる等が必要そうです。

src/app/sample/page.tsx
'use client'

import { format } from 'date-fns'
import { useState } from 'react'

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 fileName = `${format(new Date(), 'yyyyMMddHHmm')}_${file.name}`

    const res = await fetch('/api/presigned-url', {
      method: 'POST',
      body: JSON.stringify({ fileName }),
      headers: {
        'Content-Type': 'application/json',
      },
    })
    if (res.ok) {
      const { data, message } = await res.json()
      const result = await fetch(data.presignedUrl, {
        method: 'PUT',
        headers: {
          'Content-Type': file.type,
        },
        body: file,
      })
      if (result.ok) {
        alert(message)
        // https://avatar.storage-url.com の部分はcloudflareで登録をしたカスタムドメインの部分
        setUrl(`https://avatar.storage-url.com/${fileName}`)
      }
    } else {
      setUrl('')
      alert('アップロードに失敗しました。')
    }
    setUploading(false)
  }

  return (
    <div>
      <div>
        urlは{url}
        {url && !uploading ? (
          <>
            <h2>アップロードされた画像</h2>
            <div>
              <img src={url} alt="upload" width={200} />
            </div>
          </>
        ) : (
          <>
            <h2>画像をアップロード</h2>
            <input type="file" onChange={handleFileChange} className="hidden" id="upload" />
            <label htmlFor="upload">upload</label>
          </>
        )}
      </div>
    </div>
  )
}

src/app/api/presigned-url/route.tsx
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { type NextRequest, NextResponse } from 'next/server'

export async function POST(req: NextRequest) {
  const reqData = await req.json()
  const fileName = reqData.fileName
  const presignedUrl = await createPresignedUrlWithClient({
    bucket: 'avatar', // cloudflareで事前に登録をしているバケット名
    key: fileName,
  })
  return NextResponse.json({
    message: 'アップロードに成功しました。',
    data: { presignedUrl },
  })
}

const createPresignedUrlWithClient = ({ bucket, key }: { bucket: string; key: string }) => {
  const client = new S3Client({
    region: 'auto',
    endpoint: process.env.R2_ENDPOINT as string,
    credentials: {
      accessKeyId: process.env.R2_ACCESS_KEY as string,
      secretAccessKey: process.env.R2_SECRET_KEY as string,
    },
  })
  const command = new PutObjectCommand({ Bucket: bucket, Key: key })
  return getSignedUrl(client, command, { expiresIn: 3600 })
}

gontagonta

料金的に安いみたい

ここをみると、どのくらいの金額になるのかを計算できる。
https://r2-calculator.cloudflare.com/?_gl=1eoavyw_gcl_auMTE0Njg3NDY2OS4xNzI1MzY1NDYz_gaYTk0YTIxNWItNmY1ZC00ZmExLWFhY2UtNjhiMDE2Y2U4OGFj_ga_SQCRB0TXZW*MTcyNzU2NzA4MS4xMC4wLjE3Mjc1NjcwODIuNTkuMC4w