本文書ではフロントエンドの React から POST リクエストで送信したファイルを バックエンドの Node.js(Express) が受け取り、受け取ったファイルを Cloudflare R2 にアップロードするまでの一連の流れを説明しています。ファイルをアップロードするだけではなく sharp パッケージを利用してアップロードを行う画像のリサイズやフォーマットの変換方法についても動作確認を行っています。

Cloudflare R2の基本的な操作方法については公開済みです。Cloudflare のアカウントの作成やR2の Activateの方法については公開済みの記事を参考にしてください。

プロジェクトの作成

動作確認を行うためのフォルダ”cloudflare_r2_node”を作成します。このフォルダの下にバックエンドの Node.js のプロジェクト、フロントエンドの React のプロジェクトを作成します。


 % mkdir cloudflare_r2_node

バックエンドの設定

“cloudflare_r2_node”フォルダの下に “backend” フォルダを作成します。


 % mkdir backend

作成したbackendディレクトリに移動して”npm init -y”コマンドを実行してpackage.jsonファイルを作成します。


% npm init -y
Wrote to /Users/mac/Desktop/cloudflare_r2_node/backend/package.json:

{
  "name": "backend",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
}

Express サーバの初期設定

Express を利用するため最初に express, nodemon のインストールを行います。nodemon はファイルの更新を検知しファイルの再読み込みを自動で行ってくれるので開発を効率的に行うことができます。


% npm install express
% npm install nodemon --save-dev

必須ではありませんがimport文を利用できるようにpackage.jsonファイルに”type”:”module”を追加します。

import 文を利用しない場合は require を利用してモジュールを読み込みます。
fukidashi

{
  "name": "backend",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "express": "^4.19.2"
  },
  "devDependencies": {
    "nodemon": "^3.1.4"
  },
  "type": "module"
}

backend フォルダの直下に index.js ファイルを作成して Express の動作確認のため以下のコードを記述します。


import express from 'express';

const app = express();
const port = 3000;

app.get('/', (req, res) => res.send('Hello World!'));

app.listen(port, function () {
  console.log(`Example app listening on port ${port}!`);
});

ファイルを作成したら”npx nodemon index.js”コマンドを実行して Express を起動します。


% npx nodemon index.js
[nodemon] 3.1.4
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node index.js`
Example app listening on port 3000!

Express が正常に動作しているか確認するために localhost:3000 にアクセスします。”/“にアクセスすると”Hello World”の文字列を返すように設定しているので画面上には”Hello World”が表示されます。

Hello World
Hello World

各種パッケージのインストール

multer のインストール

フロントエンドから multipart/form-data で送信されていたファイルを効率よく処理するために middleware の multer のインストールを行います。


 % npm install multer

Express での multer についての基本設定については下記の記事で公開済みなので初めての人は参考にしてみてください。

@aws-sdk/client-s3 のインストール

Cloudflare R2 へのアップロードは AWS の S3 の SDK である@aws-sdk/client-s3 を利用して行います。Cloudflare R2 は AWS の S3 のコマンドラインやライブラリを利用して操作することができます。


 % npm install @aws-sdk/client-s3

dotenv のインストール

@aws-sdk/client-s3を利用してR2を操作するためにCloudflareのAPIのTokenを利用する必要がありまます。API Tokenを環境変数として.envファイルに保存するためdotenvパッケージのインストールを行います。


 % npm install dotenv

CORS のインストール

フロントエンドとバックエンドでは開発サーバの起動するポート番号が異なるためリクエストを送信すると CORS に関連するエラーが発生します。エラーを抑えるために事前に CORS のインストールを行います。


 % npm install cors

今回の動作確認でバックエンド側で利用するパッケージのインストールが完了したので package.json ファイルでパッケージのバージョンを確認します。


{
  "name": "backend",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@aws-sdk/client-s3": "^3.620.0",
    "cors": "^2.8.5",
    "dotenv": "^16.4.5",
    "express": "^4.19.2",
    "multer": "^1.4.5-lts.1"
  },
  "devDependencies": {
    "nodemon": "^3.1.4"
  },
  "type": "module"
}

POST リクエストで送信されてきたファイルの情報が取得できるか確認するために新たにルーティング upload を追加して middleware に multer を設定します。ファイルはローカルには保存しないので multer の設定では memoryStorage を利用します。送信されたファイルを正常に受け取ることができたら req.file によりファイルの情報がコンソールに表示されます。


import express from 'express';
import multer from 'multer';
import cors from 'cors';

const app = express();
const port = 3000;
app.use(cors());

const storage = multer.memoryStorage();
const upload = multer({
  storage,
});

app.post('/upload', upload.single('file'), (req, res) => {
  console.log(req.file);
  res.send('File Upload');
});

app.listen(port, function () {
  console.log(`Example app listening on port ${port}!`);
});

動作確認はフロントエンド側でReactをインストールしてから行いますが、VSCodeを利用している場合はExtensionsのThunder ClientなどをインストールしてPOSTリクエストを利用してファイルを送信すると”npx nodemon index.js”を実行しているターミナルに送信ファイルの情報が表示されます。ファイルを送信する時のフィールド名にはfileを指定します。

フロントエンドの設定

React を利用してフォームを作成しフォームで選択したファイルを POST リクエストで Express サーバに対して送信します。

React プロジェクトの作成

Reactプロジェクトの作成はcludflare_r2_nodeの直下のディレクトリで行います。

React プロジェクトは Vite を利用して作成します。“npm create vite”コマンドを実行するといくつか質問がありますが、フレームワークでは React、variant では JavaScript を選択しています。プロジェクト名は frontend としてコマンドの引数に設定しています。


 % npm create vite@latest frontend
✔ Select a framework: › React
✔ Select a variant: › JavaScript

Scaffolding project in /Users/mac/Desktop/cloudflare_r2_node/frontend...

Done. Now run:

  cd frontend
  npm install
  npm run dev

実行後に作成される frontend フォルダに移動して”npm install”コマンドを実行して JavaScript のパッケージのインストールを行います。


 % cd frontend
 % npm install

入力フォームの作成

src フォルダにある App.jsx ファイルに入力フォームを追加するため更新を行います。input 要素には type 属性に file を設定します。ファイルを選択すると onChange イベントにより handleChange 関数が実行され選択したファイルが useState Hook で定義した file に保存されます。Upload ボタンをクリックすると submit イベントにより handleSubmit 関数が実行されファイルの情報を formData に保存して fetch 関数の POST リクエストでバックエンドに送信します。


import { useState } from 'react';

function App() {
  const [file, setFile] = useState('');

  const handleChange = (e) => {
    setFile(e.target.files[0]);
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    const formData = new FormData();
    formData.append('file', file);
    const response = await fetch('http://localhost:3000/upload', {
      method: 'POST',
      body: formData,
    });

    const message = await response.text();
    console.log(message);
  };

  return (
    <>
      <h1>File Upload</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="file">Select file:</label>
          <input type="file" name="file" onChange={handleChange} />
        </div>
        <div>
          <button type="submit">Upload</button>
        </div>
      </form>
    </>
  );
}

export default App;

デフォルトのスタイルが設定されているので main.jsx ファイルで import している index.css の行をコメントします。


import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
// import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

ファイルの更新が完了したら React の開発サーバを起動するため”npm run dev”コマンドを実行します。


 % npm run dev

> frontend@0.0.0 dev
> vite

  VITE v5.3.5  ready in 1662 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

ブラウザから http://localhost:5173/にアクセスするとファイルのアップロード画面が表示されます。

ファイルアップロード画面
ファイルアップロード画面

ファイル送信の動作確認

backend フォルダで”npx nodemon index.js”が実行されていることを確認してブラウザのファイルアップロード画面でファイルを選択して”Upload”ボタンをクリックします。”npx nodemon index.js”コマンドでExpress を起動しているターミナルには送信されていたファイルの情報が表示されます。


{
  fieldname: 'file',
  originalname: 'test.png',
  encoding: '7bit',
  mimetype: 'image/png',
  buffer: <Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 0a b2 00 00 07 40 08 06 00 00 00 99 d8 4b 70 00 00 0a e3 69 43 43 50 49 43 43 20 50 72 6f 66 69 ... 729878 more bytes>,
  size: 729928
}

フロントエンドの React から Express に POST リクエストでファイルを送信して受信できることが確認できました。

R2 へのファイルアップロード

フロントエンドの React から送信したファイルをバックエンドの Express で受信することができたので次は Express から Cloudflare R2 へのファイルのアップロードを行います。ここからの作業は backend フォルダと Cloudflare のダッシュボード上で行います。

Cloudflare のアカウントは作成済みで R2 の Activate が完了しているという前提です。

[commment]Cloudflare R2 には無料枠がありますが利用するためには支払い情報を入力する必要があります。[/comment]

Backet の作成

R2 が利用可能になると Overview 画面が表示されます。R2 では Bucket を作成してファイルをオブジェクトとして Bucket の中に保存していきます。ダッシュボードから Bucket を作成することができるので”Create bucket”ボタンをクリックします。

R2の概要画面の表示
R2の概要画面の表示
Bucket はオブジェクト(ファイル)を保存する入れ物です。ファイルシステムのようにディレクトリ(フォルダ)構造を持っておらずオブジェクト毎に割り当てるキーによってオブジェクトを識別します。キーに”/“を入れること(test/test.csv)で test ディレクトリの下に test.csv が保存されているように管理することができます。しかし実際にはディレクトリは存在しません。
fukidashi

動作確認のために利用するBucketの名前にここでは”reffect”という名前をつけています。任意の名前をつけることができるので好きな名前をつけてください。作成する際に位置情報、ストレージクラスを変更することができます。位置情報については”管理を指定する”を選択すると欧州EUが表示されます。設定はデフォルトのまま進めます。”バケットを作成する”ボタンをクリックします。

R2のBucketの作成(名前設定)
R2のBucketの作成(名前設定)

作成が完了すると以下の画面が表示されます。

Bucketを作成直後の画面
Bucketを作成直後の画面

この画面から Bucket に保存されているオブジェクトを確認することができます。作成したばかりなので画像は存在しないため Bucket の中身は空です。

API Token の作成

R2 へのアップロードには@aws-sdk/client-s3 を利用します。@aws-sdk/client-s3 を利用するためには R2 の API Token の設定が必要になります。API Token は Cloudflare のダッシュボードから作成することができます。

ダッシュボードのR2の概要画面の右上にある”R2 API トークンの管理”をクリックします。

R2 APIトークンの管理
R2 APIトークンの管理

API Tokens の画面が表示されるので”API トークンを作成する”ボタンをクリックします。

APIトークン一覧画面
APIトークン一覧画面

Bucket のオブジェクトの作成などが行えるようにオブジェクト読み取りと書き込みボタンをクリックします。バケットの指定では作成した”reffect”のみ指定しておきます。

トークンの作成画面
トークンの作成画面

作成が完了すると表示される”Access Key ID”と”Secret Access Key”が必要となります。この情報は他の人に見せないように大切に保管してください。また後でキーを確認することができないので Key の情報は確実に控えておいてくだい。

R2 Tokenの作成後の画面
R2 Tokenの作成後の画面

API Token の設定

backend フォルダに.env ファイルを作成します。作成した.env ファイルには先ほど取得した API Token の情報を環境変数として設定します。.env ファイルで設定した環境変数は dotenv を利用しているのでコード内で呼び出すことができます。


R2_ACCESS_KEY_ID=2627242a9c1bf30695b2c1241cab8ac9
R2_SECRET_ACCESS_KEY=8d893715f1410b681ce31e5fe0eab2159c018d45f355924ca932a73c861c5796
ENDPOINT=https://a51248e16eb1cfe6a9a262e7XXXXX.r2.cloudflarestorage.com

index.js ファイルに R2 へのファイルのアップロードに関するコードを追加します。追加したコードでは、@aws-sdk/client-s3 から import した S3Client を利用して S3 インスタンスを作成します。インスタンスを作成する際に.env ファイルに設定した API Token の環境変数を設定します。

PutObjectCommand の引数のオブジェクトには React から送信されたきたファイルの情報を下記のように設定します。Bucket は R2 に作成している Bucket 名です。


import express from 'express';
import multer from 'multer';
import cors from 'cors';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import dotenv from 'dotenv';

const app = express();
const port = 3000;
app.use(cors());
dotenv.config();

const storage = multer.memoryStorage();
const upload = multer({
  storage,
});

app.post('/upload', upload.single('file'), async (req, res) => {
  const S3 = new S3Client({
    region: 'auto',
    endpoint: process.env.ENDPOINT,
    credentials: {
      accessKeyId: process.env.R2_ACCESS_KEY_ID,
      secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
    },
  });

  await S3.send(
    new PutObjectCommand({
      Body: req.file.buffer,
      Bucket: 'reffect',
      Key: req.file.originalname,
      ContentType: req.file.mimetype,
    })
  );
  res.send('File Upload');
});

app.listen(port, function () {
  console.log(`Example app listening on port ${port}!`);
});

ブラウザ上からファイルを選択してUploadボタンをクリックすると画面上には何も変化がありませんがブラウザのデベロッパーツールのコンソールを見るとバックエンドサーバから戻された”File Upload”の文字列を確認することができます。

R2 へのファイルアップロードが完了するとダッシュボードからアップロードしたファイルを確認することができます。Objects 列にはファイル名が設定され、Type 列には mime の値である image/png が設定されています。

アップロードしたファイルの確認
アップロードしたファイルの確認

フロントエンドの React から送信したファイルをバックエンドの Express が受け取り、R2 にアップロードするまでの流れを理解することができました。

ファイルのリサイズやフォーマット変換

ファイルをアップロードして保存する場合、そのままの状態ではなくファイルのリサイズや圧縮率の高いファイルにに変換して保存する場合があります。リサイズやファイルの変換に sharp を利用するため sharp パッケージのインストールを行います。バックエンド側で利用するためbackendフォルダで実行します。


% npm install sharp

画像のリサイズ

sharp の resize メソッドを利用することで受け取ったファイルをリサイズして R2 にアップロードを行います。ここでは画像の width の大きさを 400 に設定します。height も設定を行うこともできます。詳細は sharp のドキュメントを確認してください。


import sharp from 'sharp';
//略
const resizeFileBuffer = await sharp(req.file.buffer)
  .resize({ width: 400 })
  .toBuffer();

await S3.send(
  new PutObjectCommand({
    // Body: req.file.buffer,
    Body: resizeFileBuffer,
    Bucket: 'reffect',
    Key: req.file.originalname,
    ContentType: req.file.mimetype,
  })
);

コードを変更後にファイルのアップロードを行います。同じファイルをアップロードしているのでファイル名の変化はありませんがファイルサイズが小さくなっていることが確認できます。

サイズを変更したファイルのアップロード
サイズを変更したファイルのアップロード

実際のファイルのサイズを確認したい場合はファイルをダッシュボード上からダウンロードして確認することができます。

webp フォーマットへの変換

sharp を利用することで webp フォーマットへの変換を行うことができます。webp フォーマットは圧縮率が高いのでファイルサイズを小さくすることができます。またオプションで quality を設定することもできます。quality を低くすることで画質は下がりますがファイルサイズも小さくなります。

webp メソッドの引数に quality を 75 を設定しています。


const webpFileBuffer = await sharp(req.file.buffer)
  .webp({ quality: 75 })
  .toBuffer();

これで webp に変換することができますが拡張子は webp, mime-type はアップロードしたファイルとは異なる値になります。変換後に戻される fileBuffer から拡張子と mime を取得するために”file-type”パッケージを利用します。“file-type”パッケージを利用するためにはインストールが必要です。


% npm install file-type

インストールした file-type の fileTypeFromBuffer を利用します。実行すると ext, mime を持つオブジェクトが戻されます。


import { fileTypeFromBuffer } from 'file-type';

const fileInfo = await fileTypeFromBuffer(webpFileBuffer);

//fileInfoの中身
{ ext: 'webp', mime: 'image/webp' }

ファイル名については”req.file.originalname”から拡張子を除いたファイルの名前だけ取得するために path モジュールを利用します。


import path from 'path';

const { name } = path.parse(req.file.originalname);

最終的なコードは以下となります。


import express from 'express';
import multer from 'multer';
import cors from 'cors';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import dotenv from 'dotenv';
import sharp from 'sharp';
import { fileTypeFromBuffer } from 'file-type';
import path from 'path';

const app = express();
const port = 3001;
app.use(cors());
dotenv.config();

const storage = multer.memoryStorage();
const upload = multer({
  storage,
});

app.post('/upload', upload.single('file'), async (req, res) => {
  const S3 = new S3Client({
    region: 'auto',
    endpoint: process.env.ENDPOINT,
    credentials: {
      accessKeyId: process.env.R2_ACCESS_KEY_ID,
      secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
    },
  });

  const webpFileBuffer = await sharp(req.file.buffer)
    .webp({ quality: 75 })
    .toBuffer();

  const fileInfo = await fileTypeFromBuffer(webpFileBuffer);

  const { name } = path.parse(req.file.originalname);

  await S3.send(
    new PutObjectCommand({
      Body: webpFileBuffer,
      Bucket: 'reffect',
      Key: `${name}.${fileInfo.ext}`,
      ContentType: fileInfo.mime,
    })
  );

  res.send('File Upload');
});

app.listen(port, function () {
  console.log(`Example app listening on port ${port}!`);
});

これまでと同様にファイルをアップロードしてダッシュボードから確認すると webp のファイルがアップロードされていることが確認できます。

webp ファイルの確認
webp ファイルの確認

ファイルを R2 にアップロードするだけではなくファイルのリサイズなどを実施後にアップロードすることができるようになりました。