本文書ではフロントエンドの 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

Express サーバの初期設定

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


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

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

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

{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.2",
  },
  "devDependencies": {
    "nodemon": "^2.0.22"
  },
  "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] 2.0.22
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,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 のインストール

To perform R2 operations using @aws-sdk/client-s3, you need to configure the Cloudflare API Token and store it as an environment variable in the .env file. To do this, you will need to install dotenv.


 % npm install dotenv

CORS のインストール

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


 % npm install cors

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


{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@aws-sdk/client-s3": "^3.363.0",
    "cors": "^2.8.5",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "multer": "^1.4.5-lts.1"
  },
  "devDependencies": {
    "nodemon": "^2.0.22"
  },
  "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 を利用してフォームを作成し、フォームで選択したファイルを POST リクエストで Express サーバに対して送信します。

React プロジェクトの作成

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 v4.3.9  ready in 637 ms

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

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

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

ファイル送信の動作確認

backend フォルダで”npx nodemon index.js”が実行されていることを確認してファイルを選択して”Upload”ボタンをクリックします。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 リクエストでファイルを送信して受信できることが確認できました。

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

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

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

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

Backet の作成

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

Overview画面
Overview画面

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

動作確認のために利用する Bucket の名前にここでは”reffect”という名前をつけています。任意の名前をつけることができるので好きな名前をつけてください。

Bucketの名前設定
Bucketの名前設定

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

Bucketの内容の確認画面
Bucketの内容の確認画面

API Token の作成

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

ダッシュボードの R2 の Overview 画面の右上にある”Manage R2 API Tokens”をクリックします。

Cloudflare Dashboard
Cloudflare Dashboard

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

API Token画面
API Token画面

Bucket のオブジェクトの作成などが行えるように”Edit: Allow edit access of all objects and List, Write, and Delete operations of all buckets”を選択して”Create API Token”ボタンをクリックします。

API Token作成画面
API Token作成画面

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

API Token の設定

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

.env


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}!`);
});

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

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

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

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

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


% npm install sharp

画像のリサイズ

sharp の resize メソッドを利用することで受け取ったファイルをリサイズして R2 にアップロードを行います。ここでは画像の width の大きさを 400 に設定します。height も設定を行うこともできます。詳細は 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 にアップロードするだけではなくファイルのリサイズなどを実施後にアップロードすることができるようになりました。