Open9

中継サーバーでローカルからCloudflare R2を操作できるようにする

MaronMaron

Cloudflareが提供しているRemixのテンプレ構成が気に入っているが、ローカル起動だとR2やD1はリモートにアクセスするのではなく、ローカルでエミュレートされてしまう。リモートにアクセスしてリソースを引っ張ってくるようにしたい。
https://developers.cloudflare.com/pages/framework-guides/deploy-a-remix-site/

下記記事のposgreSQL(Supabase)にアクセスするみたく、R2やD1もリモートにアクセスさせたい。
https://zenn.dev/mizchi/articles/remix-cloudflare-pages-supabase

MaronMaron

ローカルでの起動は二つ方法がある。

  • remix vite:dev
    • viteでサーバー起動
  • wrangler pages dev ./build/client
    • wranglerでサーバー起動

個人的に、viteでサーバー起動させて開発したい。

MaronMaron

R2を利用したい場合、wrangler.tomlで定義する。
context.cloudflare.env.<bindingの名前>contextからR2にアクセスできるようになる。

...

[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "your-bucket-name"

...
MaronMaron

ここで、問題が....
remix vite:devでサーバー起動し、R2から画像一覧や画像ファイルを取得しようとすると、空で何も返ってこない。

普通に、リモートサーバーから画像一覧や画像ファイルを所得できるようにしてほしい...

MaronMaron

中継サーバーをホスティングして、それ経由でR2を操作する

手順

  • Remixに中継サーバー用のエンドポイントを生やす。
  • R2をラップして操作する。
  • 環境変数(.dev.vars)で中継サーバーを利用するか設定できるようにする。
MaronMaron

Remixに中継サーバー用のエンドポイントを生やす。

※これだとセキュリティーガバガバなので、APIキーとかで制御するように改造したりしてもいい。

app/routes/r2.relay.tsx

import { ActionFunction, json } from "@remix-run/cloudflare";

type R2Event = "list" | "get";

export const action: ActionFunction = async ({ request, context }) => {
  if (request.method !== "POST") {
    throw new Error("Method not allowed");
  }

  const { event, ...rest } = await request.json<{
    event?: R2Event;
    [key: string]: unknown;
  }>();
  if (!event) {
    throw new Error("not found event");
  }

  const R2 = context.cloudflare.env.MY_BUCKET;

  switch (event) {
    case "list": {
      const { options } = rest;
      const res = await R2.list(options as R2ListOptions);
      return json(res);
    }
    case "get": {
      const { key, options } = rest;
      const res = await R2.get(key as string, options as R2GetOptions);
      return json(res);
    }
    default: {
      throw new Error(`not supported event ${event}`);
    }
  }
};
MaronMaron

R2をラップして操作する。

R2Relayを作成する。

export class R2Relay {
  r2: R2Bucket;
  RELAY_SERVER_URL?: string;

  constructor(r2: R2Bucket, opts?: { RELAY_SERVER_URL?: string }) {
    const { RELAY_SERVER_URL } = opts || {};
    this.r2 = r2;
    this.RELAY_SERVER_URL = RELAY_SERVER_URL!;
  }

  async list(options?: R2ListOptions): Promise<R2Objects> {
    if (this.RELAY_SERVER_URL) {
      this.log(`Relaying list operation to ${this.RELAY_SERVER_URL}`);
      const res = await fetch(this.RELAY_SERVER_URL, {
        method: "POST",
        body: JSON.stringify({ options }),
      });
      return res.json();
    }

    this.log(`Relaying list operation to R2`);
    const res = await this.r2.list(options);
    return res;
  }

  async get(key: string, options?: R2GetOptions): Promise<R2ObjectBody | null> {
    if (this.RELAY_SERVER_URL) {
      this.log(`Relaying get operation to ${this.RELAY_SERVER_URL}`);
      const res = await fetch(this.RELAY_SERVER_URL, {
        method: "POST",
        body: JSON.stringify({ key, options }),
      });
      return res.json();
    }

    this.log(`Relaying get operation to R2`);
    const res = await this.r2.get(key, options);
    return res;
  }

  log(message: string) {
    console.log(`[R2Relay]`, message);
  }
}

load-context.ts

import { type PlatformProxy } from "wrangler";
import { type AppLoadContext } from "@remix-run/cloudflare";
import { R2Relay } from "./relay-server";

type Cloudflare = Omit<PlatformProxy<Env>, "dispose">;

declare module "@remix-run/cloudflare" {
  interface AppLoadContext {
    cloudflare: Cloudflare;
    R2: R2Relay;
  }
}

type GetLoadContext = (args: {
  request: Request;
  context: {
    cloudflare: Cloudflare;
  };
}) => Promise<AppLoadContext>;

export const getLoadContext: GetLoadContext = async ({ context }) => {
  const r2Relay = new R2Relay(context.cloudflare.env.MY_BUCKET, {
    RELAY_SERVER_URL: context.cloudflare.env.R2_RELAY_SERVER_URL,
  });

  return {
    cloudflare: context.cloudflare,
    R2: r2Relay,
  };
};
MaronMaron

環境変数(.dev.vars)で中継サーバーを利用するか設定できるようにする。

Remixのアプリをホスティング(中継サーバーをホスティング)してから、エンドポイントを.dev.varsに設定して、ローカル起動するとR2にアクセスできるようになる。

.dev.vars

R2_RELAY_SERVER_URL="https://<remix-app-domain>/r2/relay"
MaronMaron

問題点

この方法だと、R2にアクセスできるようになるが、返り値のメソッド(R2ObjectBody.blob()など)は使えないので、不完全....

結局、AWSのS3のSDK使うのが安牌かも。