Cloudflare R2 にブラウザからダイレクトアップロードする
結論
-
R2 の署名付き URL は、CORS ポリシーで弾かれるためブラウザからのダイレクトアップロードには使用できない。-
かつ、S3 のように、ダッシュボード上でアクセスポリシー系の設定は行えない。This logic lives within your Worker’s code, as it is your application’s job to determine user privileges.
https://developers.cloudflare.com/r2/get-started/#6-bucket-access-and-privacy
すいません()
-
→ 2023.12.03 時点での情報を見ると、ダイレクトアップロードは可能なようです。
- R2 バケットを Worker にバインドさせれば、Worker からバケットにアクセス可能になるため、自前の認証処理を含む Worker スクリプトを記述し、デプロイすることで、R2 にアクセスするためのエンドポイントを立てることができる。これは普通の Worker なので、CORS 処理の記述が可能。
- この場合、秘匿情報をブラウザに持たせないための、サーバーサイド (または別の worker?) での署名、Worker での署名検証などを自分で実装する必要がある。
以上です。間違ってる部分があれば教えてもらえると嬉しいです🙏
バインドした Worker でダイレクトアップロードを実現するための署名・検証の実装のところは、今からやるのでできたら追記します。
これ結局、署名付き URL で直接アップロードできないなら、Next.js の API Routes とかで普通の R2 API を使ってやるのと何も変わらないことに気付いたので、やっぱり API Routes でやります😕
違うとすれば、アップロード容量上限とかくらいですかね。要調査。
R2 は S3 互換の API を提供していて、公式の Examples も AWS SDK を使用しているため、これを使用します。
クライアントを生成
面倒だったので違いは詳しく調べていないが、@aws-sdk/client-s3 から export されているものとはインターフェイスがすこし違っていて、今回使用したい client.getSignedUrlPromise
が後者には生えていないので、公式の Example 通りに aws-sdk/clients/s3 を使用します。
import S3Client from 'aws-sdk/clients/s3';
export const r2Client = new S3Client({
endpoint: R2_ENDPOINT,
region: 'auto',
accessKeyId: R2_ACCESS_KEY_ID,
secretAccessKey: R2_SECRET_ACCESS_KEY,
signatureVersion: 'v4',
});
署名付き URL (presigned links) の取得自体はシンプルで、以下コードで取得できます。
const purpose = 'putObject' // or 'getObject'
const url = await r2Client.getSignedUrlPromise(purpose, {
Bucket: '<bucket-name>',
Key: '<key>', // ex) dog.png
Expires: 60 * 60,
});
// https://my-bucket-name.<accountid>.r2.cloudflarestorage.com/dog.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=<credential>&X-Amz-Date=<timestamp>&X-Amz-Expires=3600&X-Amz-Signature=<signature>&X-Amz-SignedHeaders=host
ただし、public access とは言えどアクセスポリシーがあるので、ブラウザからこの URL に PUT リクエストを送っても CORS で弾かれます。
R2 では、各アクセスに対しての認証処理を記述した Worker スクリプトを、R2 バケットに bind してデプロイすることで解決するようです。
これ↑を今からやってみる
違いました (多分) 2個下のコメントに書いてます。
試しに、PUT リクエストが来たら認証プロセス無しで R2 に put する公式の Worker スクリプトを書いてデプロイしてみる。
この内容は8割方↑に貼った公式ドキュメントのままで、メモ程度に書いているだけなので、公式を参照することをお勧めします。
スクリプト
export interface Env {
BUCKET: R2Bucket;
}
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
if (request.method === 'PUT') {
await env.BUCKET.put(key, request.body);
return new Response(`Put ${key} successfully!`);
}
return new Response('Method Not Allowed', { status: 405, headers: { Allow: 'PUT' } });
},
};
wrangler.toml
name = "r2-worker"
main = "src/index.ts"
compatibility_date = "2022-07-10"
account_id = "<my-account-id>"
workers_dev = true
[[r2_buckets]]
binding = 'BUCKET' # 任意の js で有効な変数名 (この変数名でランタイムからアクセスできる)
bucket_name = '<my-bucket-name>'
デプロイ
❯ wrangler publish
⛅️ wrangler 2.0.16
--------------------
Your worker has access to the following bindings:
- R2 Buckets:
- BUCKET: my-bucket
Total Upload: 1.03 KiB / gzip: 0.47 KiB
Uploaded r2-worker (1.10 sec)
Published r2-worker (3.60 sec)
r2-worker.<my-domain-name>.workers.dev
ちゃんとバケットに bind されているのが確認できる。
R2 バケットに bind した Worker スクリプトというのは、R2 への PUT リクエストのミドルウェア的に動くのかと思っていたらそういうことではないらしく、単純に bind されたリソースにスクリプトからアクセス可能になるということらしいです。
つまり、Worker スクリプトを間に挟んで認証するのではなく、自前の認証処理を含む R2 アップロード用のエンドポイントを新しく立てるイメージですね。大変失礼しました。