Cloudflare R2を使ってみました。今までfirebase storageを使っていたため、それと比較しています。
画像のアップロードについては、こちらにまとめなおしました。
Contents
Cloudflare R2の無料枠!
Cloudflare R2を使うために、クレジットカードの情報が必須のようです!
(すでにドメインなどで使っている人は必要ないようです。個人的にPayPalでドメインの支払いをした経験があるため何も聞かれませんでした。)
無料枠は次の通りです。
Class A(書き込み処理、アップロード、更新、削除など):100万操作/月
Class B(読み込み処理、ダウンロードなど):1000万操作/月
ストレージ:10GB/月
月ごとにリセットされるようです。ストレージも10GB超える前に消せばOKのようです。
Firebaseと比較してもかなり魅力的です。
[R2 サブスクリプションをアカウントに追加する]をクリック。> [バケットを作成する]
Cloudflare R2の使い方!画像アップロード
パケットの作り方
- バケット名「バケット名」のフィールドに、作成したいバケットの名前を入力します。
- 位置情報 自動を選択します。バケットのデータを物理的に保存する地域を選択することができます。地域はデータのレスポンス速度に影響を与えるため、ユーザーの位置に近い地域を選択すます。アジアを選びました。
- アクセス権限の設定:「公開」を選択すると、バケット内のファイルはインターネット経由で公開されます。「認証を要求する」を選択すると、アクセスには認証が必要になります。とくにいじりませんでした。
[バケットを作成する]をおします。
ドラック&ドロップで画像をアップロードできます。
Cloudflare R2 Storageはオブジェクトストレージで、従来のファイルシステムのような物理的なフォルダ概念は持っていません。しかし、[プレフィックスをディレクトリとして表示]にチェックをいれるとフォルダのような概念をもつことができるようです。
わかりやすいですね。
フォルダこみで画像を1枚アップロード、メトリクスのタブを確認
クラスAが4、クラスBが2になっていました…。少しこのカウント方法は不明瞭…?です…。
APIトクーンを作成します。
APIトクーンの作り方
R2 > R2 API トークンの管理 > APIトクーンを作成する >
- トークン名:R2 Token
- 権限:オブジェクト読み取りと書き込み(アップロードとダウンロードを行うため)
最後の3つはセキュリティ強化用でしょう。ユーザーが無期限にアップロードする場合はパケットの指定だけでしょう。指定したパケットをプログラミングから使うことにより制限するような感じでしょうか。
- バケットの指定:特定のパケット(バケットの作成でつけた名前を選びます)
- TTL:無期限
- クライアント IP アドレスフィルタリング:含むと除くが選べますけど何もせず
[APIトクーンを作成する]をおします。
APIキーをめもって
[終了]をおします。
公式サイトですがこちらの記事が参考になります。
You can generate an API token to serve as the Access Key
https://developers.cloudflare.com/r2/api/s3/tokens/
.env.local
キーを.env.localに記述してサーバーサイドから呼びます。
// トークンの値
CLOUDFLARE_API_TOKEN='xxx'
// アクセスキーID
CLOUDFLARE_ACCESS_KEY_ID='xxx'
// シークレットアクセスキー
CLOUDFLARE_SECRET_ACCESS_KEY='xxx'
// エンドポイント
CLOUDFLARE_R2_ENDPOINT='https://xxx.r2.cloudflarestorage.com'
npm install @aws-sdk/client-s3
npm install aws-sdk
パッケージサイズを小さく保つため実際にはこちらの方法でインストールしました。
@aws-sdk/client-s3
はAWS SDK for JavaScript v3の一部で、Amazon S3サービス専用のクライアントです。AWS SDK v3はモジュール化されており、必要な機能のみをインポートして使用できます。
npm install @aws-sdk/client-s3
Cloudflare R2はAmazon S3(Simple Storage Service)互換のAPIを提供しています。これは、S3と同じ方法でストレージサービスにアクセスできることを意味します。aws-sdk
はAmazon Web Servicesの公式SDKですが、S3互換のAPIを持つ他のサービス(この場合はCloudflare R2)と通信するためにも使えます。
つまり、AWSのサービスを利用していなくても、aws-sdk
をインストールし、それを使用してCloudflare R2との間でファイル操作を行うことができます。S3互換のAPIを持つため、aws-sdk
はR2 Storageと通信するのに適したツールなのです。
設定ファイル作成
import { S3 } from '@aws-sdk/client-s3';
const s3 = new S3({
credentials: {
accessKeyId: process.env.CLOUDFLARE_ACCESS_KEY_ID as string,
secretAccessKey: process.env.CLOUDFLARE_SECRET_ACCESS_KEY as string
},
endpoint: process.env.CLOUDFLARE_R2_ENDPOINT as string,
region: "auto"
});
env.localをルートディレクトリにおいておけば、cloudflare-client.tsが見てくれます。
Rudexを使って画像アップロード
ざっくりとしたイメージ。
import { S3 } from "@aws-sdk/client-s3";
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { createAsyncThunk } from '@reduxjs/toolkit';
// S3クライアントの初期化
const s3 = new S3({
credentials: {
accessKeyId: process.env.CLOUDFLARE_ACCESS_KEY_ID,
secretAccessKey: process.env.CLOUDFLARE_SECRET_ACCESS_KEY
},
endpoint: process.env.CLOUDFLARE_R2_ENDPOINT,
region: "auto"
});
async function uploadImage(dataUrl: string) {
console.log('uploadImage:dataUrl', dataUrl);
const fileName = `${new Date().getTime()}.png`; // タイムスタンプをファイル名として使用
const bucketName = 'your-bucket-name'; // Cloudflare R2のバケット名
// DataURLからBlobを作成
const response = await fetch(dataUrl);
const blob = await response.blob();
console.log('Fetched blob:', blob);
try {
const result = await s3.putObject({
Bucket: bucketName,
Key: `test/${fileName}`,
Body: blob
});
console.log('Uploaded to R2:', result);
const downloadURL = `https://${bucketName}.r2.cloudflarestorage.com/test/${fileName}`;
return downloadURL;
} catch (error) {
console.error('Error during R2 upload:', error);
throw error;
}
}
export const uploadImageAsync = createAsyncThunk<string, string>(
'upload/uploadImage',
async (dataUrl: string, { dispatch }) => {
dispatch(uploadStart());
try {
const downloadURL = await uploadImage(dataUrl);
dispatch(uploadSuccess(downloadURL)); // ダウンロードURLをpayloadとして送る
return downloadURL;
} catch (error: unknown) {
if (error instanceof Error) {
dispatch(uploadFailed(error.message));
throw error;
} else {
dispatch(uploadFailed('An unknown error occurred.'));
throw new Error('An unknown error occurred.');
}
}
}
);
firebaseのアップロードと比較
今回、firebaseで作ったものの乗り換えなんですけど、firebaseと似た感じでしたね。
import { firebaseApp } from '../../lib/firebase-client';
import { getInitializedApp } from '../../lib/firebase-client';
import { getStorage, ref, uploadBytes, getDownloadURL } from 'firebase/storage';
async function uploadImage(dataUrl: string) {
const fileName = `${new Date().getTime()}.png`; // タイムスタンプをファイル名として使用
const firebaseApp = getInitializedApp();
const storage = getStorage(firebaseApp);
const imageRef = ref(storage, `test/${fileName}`);
// DataURLからBlobを作成
const response = await fetch(dataUrl);
const blob = await response.blob();
console.log('Fetched blob:', blob);
try {
await uploadBytes(imageRef, blob);
console.log('Uploaded bytes successfully');
} catch (error) {
console.error('Error during uploadBytes:', error);
}
let downloadURL;
try {
downloadURL = await getDownloadURL(imageRef);
console.log('Obtained download URL:', downloadURL);
} catch (error) {
console.error('Error during getDownloadURL:', error);
}
// BlobデータをCloud Storageにアップロード
await uploadBytes(imageRef, blob);
// // アップロードした画像のダウンロードURLを返す
return getDownloadURL(imageRef);
}
Cloudflare R2のslug一覧取得
async function getSlugsFromR2() {
const bucketName = 'BUCKET_NAME';
try {
const res = await s3.listObjects({ Bucket: bucketName, Prefix: 'test/sample' });
const slugs = res.Contents?.map(item => item.Key?.split('/')[2].split('.')[0]) ?? [];
return slugs;
} catch (error) {
console.error("Error listing items from R2:", error);
return [];
}
}
firebaseのslug一覧取得と比較
import { getStorage, ref, listAll } from 'firebase/storage';
import { getInitializedApp } from '../../../lib/firebase-client';
async function getSlugsFromFirebase() {
const firebaseApp = getInitializedApp();
const storage = getStorage(firebaseApp);
const listRef = ref(storage, 'test/sample');
try {
const res = await listAll(listRef);
// すべてのファイルのスナップショットを取得
const slugs = res.items.map(itemRef => {
return itemRef.name.split('.')[0]; // ファイル名から拡張子を取り除いてslugを取得
});
return slugs;
} catch (error) {
console.error("Error listing items:", error);
return [];
}
}
Cloudflare R2の画像を表示
async function fetchImageUrl(imageId: string): Promise<string> {
// Cloudflare R2のバケットから直接URLを構築
return `https://${BUCKET_NAME}.r2.cloudflarestorage.com/unregistered/profile/${imageId}.png`;
}
firebaseで画像を表示
import { getStorage, ref, getDownloadURL } from 'firebase/storage';
import { getInitializedApp } from '../../../lib/firebase-client';
async function fetchImageUrl(imageId: string): Promise<string> {
const firebaseApp = getInitializedApp();
const storage = getStorage(firebaseApp);
const imageRef = ref(storage, `unregistered/profile/${imageId}.png`);
return getDownloadURL(imageRef);
}
Cloudflare R2の署名付きURL
実際に使わなかったけど、署名付きURLのサンプル。
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import type { NextApiRequest, NextApiResponse } from 'next';
// Cloudflare R2およびAWS SDKの設定
const s3 = new S3Client({
credentials: {
accessKeyId: process.env.CLOUDFLARE_ACCESS_KEY_ID!,
secretAccessKey: process.env.CLOUDFLARE_SECRET_ACCESS_KEY!
},
endpoint: process.env.CLOUDFLARE_R2_ENDPOINT!,
region: 'auto'
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
console.log('S3 Client:', s3);
console.log('Access Key ID:', process.env.CLOUDFLARE_ACCESS_KEY_ID);
console.log('Secret Access Key:', process.env.CLOUDFLARE_SECRET_ACCESS_KEY);
console.log('R2 Endpoint:', process.env.CLOUDFLARE_R2_ENDPOINT);
console.log('Bucket Name:', process.env.BUCKET_NAME);
if (req.method === 'GET') {
try {
const { fileName } = req.query as { fileName: string }; // クエリパラメータからファイル名を取得
console.log('File Name:', fileName);
console.log('Bucket Name:', process.env.BUCKET_NAME);
// PutObjectCommandを作成
const command = new PutObjectCommand({
Bucket: process.env.BUCKET_NAME!,
Key: fileName,
});
// 署名付きURLの有効期限
const signedUrl = await getSignedUrl(s3, command, {
expiresIn: 300, // 5分
});
console.log('Signed URL:', signedUrl);
// 署名付きURLをレスポンスとして返す
res.status(200).json({ url: signedUrl });
} catch (error: unknown) {
if (error instanceof Error) {
console.error('Error creating signed URL:', error);
res.status(500).json({ error: error.message });
} else {
console.error('Unknown error:', error);
res.status(500).json({ error: 'An unknown error occurred.' });
}
}
} else {
res.setHeader('Allow', ['GET']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
import { CUSTOM_DOMAIN, UPLOAD_PATH } from '../../const/constants';
async function uploadImage(dataUrl: string) {
const fileName = `${new Date().getTime()}.png`; // タイムスタンプをファイル名として使用
// DataURLからBlobを作成
const response = await fetch(dataUrl);
const blob = await response.blob();
// サーバーサイドから署名付きURLを取得
try {
const filePath = UPLOAD_PATH + fileName;
const uploadResponse = await fetch(CUSTOM_DOMAIN + fileName, {
method: 'PUT',
body: blob, // 画像のBlobデータ
headers: {
'Content-Type': 'image/png' // 適切なContent-Typeを設定
}
});
if (!uploadResponse.ok) {
throw new Error(`Error: ${uploadResponse.statusText}`);
}
console.log('Uploaded to R2:', uploadResponse.url);
return uploadResponse.url; // アップロード後の画像URLを返す
} catch (error) {
console.error('Upload failed:', error);
throw error;
}
}
ご参考になれば幸いです。
コメント