Closed8

Cloudflare R2 にブラウザからダイレクトアップロードする

ピン留めされたアイテム
Yo IwamotoYo Iwamoto

結論

  • R2 の署名付き URL は、CORS ポリシーで弾かれるためブラウザからのダイレクトアップロードには使用できない。

→ 2023.12.03 時点での情報を見ると、ダイレクトアップロードは可能なようです。

  • R2 バケットを Worker にバインドさせれば、Worker からバケットにアクセス可能になるため、自前の認証処理を含む Worker スクリプトを記述し、デプロイすることで、R2 にアクセスするためのエンドポイントを立てることができる。これは普通の Worker なので、CORS 処理の記述が可能
    • この場合、秘匿情報をブラウザに持たせないための、サーバーサイド (または別の worker?) での署名、Worker での署名検証などを自分で実装する必要がある。

以上です。間違ってる部分があれば教えてもらえると嬉しいです🙏

Yo IwamotoYo Iwamoto

バインドした Worker でダイレクトアップロードを実現するための署名・検証の実装のところは、今からやるのでできたら追記します。

Yo IwamotoYo Iwamoto

これ結局、署名付き URL で直接アップロードできないなら、Next.js の API Routes とかで普通の R2 API を使ってやるのと何も変わらないことに気付いたので、やっぱり API Routes でやります😕

違うとすれば、アップロード容量上限とかくらいですかね。要調査。

Yo IwamotoYo Iwamoto

クライアントを生成
面倒だったので違いは詳しく調べていないが、@aws-sdk/client-s3 から export されているものとはインターフェイスがすこし違っていて、今回使用したい client.getSignedUrlPromise が後者には生えていないので、公式の Example 通りに aws-sdk/clients/s3 を使用します。
https://developers.cloudflare.com/r2/examples/aws-sdk-js/

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',
});
Yo IwamotoYo Iwamoto

署名付き 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 してデプロイすることで解決するようです。
https://developers.cloudflare.com/r2/get-started/#6-bucket-access-and-privacy

これ↑を今からやってみる

違いました (多分) 2個下のコメントに書いてます。
https://zenn.dev/link/comments/1d640538075231

Yo IwamotoYo Iwamoto

試しに、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 されているのが確認できる。

Yo IwamotoYo Iwamoto

R2 バケットに bind した Worker スクリプトというのは、R2 への PUT リクエストのミドルウェア的に動くのかと思っていたらそういうことではないらしく、単純に bind されたリソースにスクリプトからアクセス可能になるということらしいです。
つまり、Worker スクリプトを間に挟んで認証するのではなく、自前の認証処理を含む R2 アップロード用のエンドポイントを新しく立てるイメージですね。大変失礼しました。

このスクラップは2022/07/10にクローズされました