[Rust] Cloudflare WorkersでR2にアクセスしてみる [TypeScript]

2023.01.24

Introduction

Cloudflare R2はAmazon S3互換のオブジェクトストレージです。
エグレス料金がかからず、非常にリーズナブルです。

今回はCloudflare WorkersからR2にアクセスしてみます。

Environment

今回試した環境は以下のとおりです。

  • MacBook Pro (13-inch, M1, 2020)
  • OS : MacOS 12.4
  • Node : v18.2.0
  • wrangler : 2.8.1

Cloudflareのアカウントと
wranglerのインストールは設定済みとします。

Setup

まずはR2のセットアップです。
CloudflareのCLIツールであるwranglerを使います。
もしなければnpm or yarnでインストール。

% npm install -g wrangler

% wrangler --version
 ⛅️ wrangler 2.8.1
-------------------

このあとCLIからいろいろ操作するので、
ログインします。

% wrangler login

wranglerでR2にバケットを作成します。

% wrangler r2 bucket create <YOUR BUCKET NAME>
 ⛅️ wrangler 2.8.1
-------------------
Creating bucket <YOUR BUCKET NAME>.

ちなみに、バケット名に「_」使おうとしたらエラーになったので注意。
これでCloudflareのダッシュボードでもバケットが確認できるし、
↓のコマンドで一覧を取得することもできます。

% wrangler r2 bucket list
[
・・・
  {
    "name": <YOUR BUCKET NAME>,
    "creation_date": "2023-01-24T04:12:06.019Z"
  },
・・・
]

Create Workers with TypeScript

ではTypeScriptでCloudflare WorkersからR2にアクセスしてみます。
wrangler initでWokersの雛形を作成します。

% mkdir ts-r2 && cd ts-r2
% wrangler init --yes

wrangler.tomlにR2のバケット定義を記述します。

[[r2_buckets]]
binding = 'MY_BUCKET'
bucket_name = '<YOUR BUCKET NAME>'
preview_bucket_name = '<YOUR BUCKET NAME>' #開発用

index.tsを、ここを参考に実装します。

//index.ts

interface Env {
	MY_BUCKET: R2Bucket
}
  
export default {
	async fetch(request:Request, env:Env) {
	  const url = new URL(request.url);
	  const key = url.pathname.slice(1);
  
	  switch (request.method) {
		case 'PUT':
		  await env.MY_BUCKET.put(key, request.body);
		  return new Response(`Put ${key} successfully!`);
		case 'GET':
		  const object = await env.MY_BUCKET.get(key);
  
		  if (object === null) {
			return new Response('Object Not Found', { status: 404 });
		  }
  
		  const headers = new Headers();
		  object.writeHttpMetadata(headers);
		  headers.set('etag', object.httpEtag);
  
		  return new Response(object.body, {
			headers,
		  });
		default:
		  return new Response('Method Not Allowed', {
			status: 405,
			headers: {
			  Allow: 'PUT, GET, DELETE',
			},
		  });
	  }
	},
  };

npm startで起動します。

% npm start

> ts-r2@0.0.0 start
> wrangler dev

 ⛅️ wrangler 2.8.1
-------------------
Your worker has access to the following bindings:
- R2 Buckets:
  - MY_BUCKET: <YOUR BUCKET NAME>
⬣ Listening at http://0.0.0.0:8787
- http://127.0.0.1:8787
- http://192.168.11.4:8787
- http://192.168.105.1:8787
Total Upload: 1.02 KiB / gzip: 0.46 KiB
╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ [b] open a browser, [d] open Devtools, [l] turn on local mode,  clear console, [x] to exit          │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯

curlで適当なpngファイルを指定してアップロードします。

% curl -XPUT  --data-binary "@/path/your/image/something.png" http://127.0.0.1:8787/mykey

この時点でダッシュボードをみると、バケットにファイルが登録されてます。
アップロードが成功したら、ブラウザで
http://127.0.0.1:8787/mykey
にアクセスしてみます。
ブラウザでR2からファイルを取得して表示できれば成功です。

Create Workers with Rust

では次に、Rustを使ってWorkersを実装してみましょう。
npm initでRust用Workersの雛形を生成します。

% npm init cloudflare <Project Name> worker-rust 
% cd <Project Name>

さきほどと同じく、wrangler.tomlにR2のバケット定義を記述し、
ほかの部分も下記のように少し書き換えます。

[vars]
WORKERS_RS_VERSION = "*"

[build]
command = "cargo install -q worker-build && worker-build --release"

現在では、workersのcrateはGithubから直接もってこないと
R2が使えません。
なので、↓のようにGithubにあるworkers-rsのmainブランチを指定します。

・・・
[dependencies]
worker = { git = "https://github.com/cloudflare/workers-rs.git", branch = "main"}
・・・

lib.rsを下記のように修正。
Rust版ではR2からオブジェクトを取得する処理だけ実装します。
※修正した部分だけ抜粋

//lib.rs

mod r2;

pub struct SharedData {
}

#[event(fetch)]
pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {

・・・
    let data = SharedData {};
    let router = Router::with_data(data); 
    router
        .get_async("/r2/get/:key", r2::get)
        .run(req, env)
        .await
}

そしてr2.rsの実装です。
/r2/get/:keyにGETでアクセスすると、パスにはいっているキー名で
R2からオブジェクト(ここではpng決め打ち)を取得してレスポンスとして返します。

use worker::*;
use crate::SharedData;

pub async fn get(_req: Request, ctx: RouteContext<SharedData>) -> Result<Response> {
    //bindしたバケット名を指定
    let bucket = ctx.bucket("MY_BUCKET")?;

    //パスからキー名を取得
    let key = ctx.param("key").unwrap();
    let item = bucket.get(key).execute().await?.unwrap();
    let item_body = item.body().unwrap();
    let bytes = item_body.bytes().await.unwrap();
    let response = Response::from_bytes(bytes)?;
    let mut headers = Headers::new();
    headers.set("content-type","image/png")?;
    Ok(response.with_headers(headers))
}

wrangler devコマンドで起動して、動作確認してみます。

% wrangler dev
 ⛅️ wrangler 2.8.1
-------------------
Running custom build: cargo install -q worker-build && worker-build --release
[INFO]: 🎯  Checking for the Wasm target...
[INFO]: 🌀  Compiling to Wasm...

[INFO]: ⬇️  Installing wasm-bindgen...
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended
[INFO]: ✨   Done in 1.15s
⚡ Done in 10ms

・・・

╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ [b] open a browser, [d] open Devtools, [l] turn on local mode,  clear console, [x] to exit          │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯

ブラウザで下記URLにアクセスしてみます。
http://127.0.0.1:8787/r2/get/<R2のファイル名>
R2に登録した画像が表示されれば、Rust版でも動作確認OKです。

References