Skip to main content

FastifyでNode.jsのファイルアップロードを処理する方法

著者

Amarachi Aso

feature fastify js upload

2022年12月8日

0 分で読めます

ファイルアップロードは、Webアプリケーションでよく必要とされる機能です。そのため、多くのWebフレームワークには、サーバーサイドのアプリケーションでファイルを受け取り、扱うための仕組みが用意されています。Node.jsのWebフレームワークであり、Expressの後継と見なされることも多いFastifyも同様です。高速で、強力なプラグインアーキテクチャを備え、オーバーヘッドを最小限に抑えながら、優れた開発者体験の提供に重点を置いています。

このチュートリアルでは、Node.jsアプリケーションでFastifyフレームワークを使ってファイルアップロードを設定します。一般的なサーバーでファイルアップロードがどのように機能するか、Fastifyサーバーアプリケーションでアップロードを処理する方法を学びます。

ファイルアップロードの基礎

アプリケーションサーバーにファイルをアップロードするには、クライアントのコンピューターからサーバーへPOSTリクエストを送信する必要があります。リクエストボディには、通常HTMLフォームから送信されるデータを含めます。HTMLフォームで使用できるデータのエンコード方法は3種類あります。

  • application/x-www-form-urlencoded(デフォルト)は、HTMLフォームで標準的に使われるコンテンツタイプまたはエンコード方式です。ただし、このフォームを使ってファイルを送信する場合は、multipart/form-data方式でエンコードする必要があります。

  • multipart/form-dataは、HTMLフォームに含まれる複数のパーツやフィールドを表すために使われます。各パーツは、リクエストボディ内で名前と値のペアとして送信されます。それぞれの名前と値は、入力フィールドのname属性と値に対応します。

  • text/plainは、書式設定のないテキストデータを送信するために使われ、ファイルの送信には使用できません。

ファイルをアプリケーションサーバーに送信する目的は、通常、サーバーでファイルを処理する、保存する、またはその両方です。サーバーアプリケーションでは、アップロードされたファイルを次のような方法で処理・保存できます。

  • 独自サーバーのファイルシステムに保存する方法です。アプリケーションが実行されているサーバー上の専用ディレクトリや、ファイル保存専用の別サーバーを利用できます。

  • データベースに保存する方法です。

  • Amazon S3やCloudinaryなどのクラウドストレージにファイルをアップロードする方法です。

このチュートリアルでは、最初の方法でファイルアップロードを処理します。

Fastifyでファイルアップロードを実装する

このチュートリアルを始める前に、次のものを用意してください。

  • Node.jsがインストールされていること

  • Visual Studio Code(VS Code)など、お好みのコードエディター

  • Postman。サインインしてWebアプリを使うか、コンピューターにインストールしてください。

以降のセクションでは、Fastifyプロジェクトを作成し、ファイルアップロード用のプラグインを設定します。また、複数ファイルのアップロードを処理できる/uploadルートを作成し、そのルートのテストケースを記述します。

プロジェクトを作成してFastifyを設定する

まず、プロジェクトを作成してFastifyを設定します。プロジェクトを作成するディレクトリに移動し、fastify-file-uploadというフォルダーを作成します。ターミナルで次のコマンドを実行してください。

mkdir fastify-file-upload

次のコマンドで、このフォルダーに移動します。

cd fastify-file-upload

次に、お好みのコードエディターでフォルダーを開きます。VS Codeを使用している場合は、次のコマンドを実行できます。

code .

次のコマンドを実行して、プロジェクトを初期化し、package.jsonファイルを作成します。

npm init -y

次に、このプロジェクトで使用する依存関係をインストールします。

npm install fastify @fastify/multipart form-auto-content tap

そして、app.jsとserver.jsの2つのファイルを作成します。

touch app.js server.js

app.jsにはアプリケーションのコードを記述します。ファイルを開き、次のコードをコピーして貼り付けます。

import Fastify from 'fastify'

export const app = Fastify({
    logger: true
  })

Fastifyをfastifyからインポートする

このコードでは、Fastifyフレームワークをインポートし、そこからアプリのインスタンスを作成します。次に、server.jsファイルに以下のコードを記述します。

import { app } from './app.js'

const start = async () => {
    try {
      await app.listen({ port: 8000 })
      console.log(`server listening on ${app.server.address().port}`)
    } catch (err) {
        app.log.error(err)
      process.exit(1)
    }
  }
  start()

このコードは、アプリケーションの実行時にアプリを提供します。提供中に問題が発生した場合は、エラーを返します。

このプロジェクトではES6モジュール構文を使用するため、JavaScriptファイルをモジュールとして読み込む必要があります。JavaScriptファイルがモジュールとして読み込まれるように、package.jsonにtypeプロパティを追加し、値をmoduleに設定します。

"type": "module"

ファイルアップロード用プラグインを設定する

Fastify-multipartは、Fastifyでmultipartコンテンツタイプをサポートします。依存関係のインストール時にプラグインもすでにインストールしたので、次は設定を行います。multipartを'@fastify/multipart'からインポートして登録してください。

app.jsの内容に、+で始まる行を追加します。

  import Fastify from 'fastify'
+ import multipart from '@fastify/multipart'

export const app = Fastify({
    logger: true
  })

+ app.register(multipart)

ここでは、先ほど作成したFastifyアプリにmultipartを登録しました。これで、アプリケーションのインスタンスがmultipartコンテンツタイプを簡単に解析できるようになります。

ファイルをアップロードする

POSTリクエストからファイルにアクセスする方法はいくつかあります。たとえば、リクエストに含まれるファイルが1つだけだと分かっている場合は、次のコードでアクセスできます。

const data = await request.file()

このコードのrequestは、アップロードルートのハンドラー関数に渡されるリクエストパラメーターです。ハンドラー関数は次のような形になります。

…
app.post('/upload', async function (request, reply) {

  const data = await request.file()

  // The rest of your code
}

複数のファイルを想定している場合は、file()の代わりにfiles()メソッドを呼び出します。次のようになります。

const data = await request.files()

app.jsファイルに、複数ファイルのアップロードを完全に実装するコードを記述します。その前に、アプリケーションにアップロードされたファイルを保存するディレクトリを作成します。プロジェクトのルートにuploadsという名前で作成してください。

mkdir uploads

その後、app.jsファイルを次のように変更します。

import Fastify from 'fastify'
import multipart from '@fastify/multipart'
+ import fs from 'fs'
+ import util from 'util'
+ import { pipeline } from 'stream'

+ const pump = util.promisify(pipeline)

export const app = Fastify({
    logger: true
  })

  app.register(multipart)

+   app.post('/upload', async function (req, reply) {

+ const parts = req.files()
+ 
+ for await (const part of parts) {

+     // upload and save the file
+     await pump(part.file, fs.createWriteStream(`./uploads/${part.filename}`))

+ }

+     return {message : 'files uploaded' }
+ })

変更後のコードでは、Node.jsのコアモジュールfs、util、streamから関数をインポートしています。続いて、/uploadルートとそのハンドラー関数を作成しています。

ハンドラー関数内ではreq.files()を使って、リクエスト内のすべてのファイルにアクセスしています。ファイルを1つずつループし、各ファイルについてawait pump(part.file, fs.createWriteStream(`./uploads/${part.filename}`))でアップロードし、先ほど作成したuploadsディレクトリに保存します。

アップロードするファイルは、保存前にメモリへ蓄積されるわけではない点に注意してください。代わりに、受信側でfs.createWriteStream関数を使ってストリームを作成し、part.fileから届くファイルストリームを新しく作成したストリームにパイプします。これにより、ファイルの内容をまずメモリにバッファリングするのではなく、データをチャンク単位で直接ファイルに書き込み、ストリームから届くたびに保存できます。大きなファイルをアップロードする場合に特に便利です。イベントループをブロックせず、アプリケーションでファイルを効率よくメモリ処理できます。

次はサーバーを起動し、Postmanでファイルを送信します。package.jsonファイルのstartスクリプトを次のように置き換えてください。

"start": "node server.js"

次のコマンドでサーバーを起動します。

npm start

Postmanでhttp://localhost:8000/uploadへのPOSTリクエストを作成します。リクエストのbodyでform-dataを選択し、お好みのファイルを2つアップロードします。

記事のテキストファイルとカバー画像用のフォームデータフィールドを含む、/uploadへのPOSTリクエストを表示したPostmanのインターフェース。
ファイルをアップロードするためのPostmanインターフェース。

ここでは、article.txtという名前のファイルとcover.jpgという名前のファイルをアップロードしています(それぞれのキーは「article」と「coverImage」です)。アップロード後、ステータスコード200と、戻り値オブジェクト{message : 'files uploaded' }が返されます。

article.txtとcover.jpgのファイルを表示したPostmanのform-dataアップロードリクエスト。ファイルのアップロードが完了したことを示す200 OKレスポンス。
Postmanから2つのファイルがアップロードされました。

先ほどアップロード用に作成した「upload」ディレクトリにも、ファイルが追加されていることを確認できます。

これで、fastify/multipartプラグインを使った複数ファイルのアップロード処理ができました。多くのフォームでは、ファイルだけでなく、ファイルとその他のテキストフィールドの両方を指定する必要があります。このようなフォームを処理するには、ファイルストリームとその他のフィールドの両方を扱えるよう、既存のコードを変更します。

そのために、app.jsファイルを次のように変更します。

import Fastify from 'fastify'
import multipart from '@fastify/multipart'
import fs from 'fs'
import util from 'util'
import { pipeline } from 'stream'

const pump = util.promisify(pipeline)

export const app = Fastify({
    logger: true
  })

  app.register(multipart)

  app.post('/upload', async function (req, reply) {

+ const parts = req.parts()

for await (const part of parts) {

+  if (part.file) {

    // upload and save the file
    await pump(part.file, fs.createWriteStream(`./uploads/${part.filename}`))

+  } else {
+    // do something with the non-files parts
+  }

}

    return {message : 'files uploaded' }
})

今回はreq.files()ではなくreq.parts()を使い、ファイルストリームとフィールドの両方にアクセスしています。また、ループ内ではif文で各パーツがファイルかどうかを判定し、フィールドとは異なる方法で処理しています。

次は、もう一度リクエストを送信します。ファイルを保存し、npm startでサーバーを停止してから再起動してください。Postmanに戻り、前のリクエストのキーと値のペアを削除して、新しいものを作成します。最初のフィールドには氏名を入力し、その後、以前と同じようにファイルを2つ指定します。

注:同じファイルを使用する場合は、前回アップロードしたファイルをuploadsディレクトリから削除してください。

リクエストは次のようになります。

1つのフィールドと、article.txtおよびcover.jpgの2つのアップロード済みファイルが表示されたPostmanのform-dataリクエストと、200 OKのレスポンス。
Postmanから1つのフィールドと2つのファイルをアップロードしました。

このように設定できていれば、アプリケーションで複数のファイルストリームとフィールドを処理できます。

テストケースを作成する

ファイルアップロードの実装を保護するには、テストを書く必要があります。では、Fastifyでファイルアップロードのテストケースを書くにはどうすればよいでしょうか。このチュートリアルでは、テストフレームワークのtap、パッケージのform-auto-content、Fastifyの.injectメソッドを使ってテストケースを作成します。

プロジェクトのルートにapp.test.jsファイルを作成します。次のコードをコピーして貼り付けてください。

'use strict'

import { test } from 'tap'
import formAutoContent from 'form-auto-content'
import fs from 'fs'
import { app } from './app.js'

const myForm  = formAutoContent({
    fullName: 'Amarachi Aso',
    article: fs.createReadStream(`./sample_files/file.txt`),
    coverImage: fs.createReadStream(`./sample_files/cover.jpg`)
  })

test('submits form with multiple files in it', async t => {

    const res = await app.inject({
        method: 'post',
        url: '/upload',
        ...myForm
      })
  t.equal(res.statusCode, 200, 'returns a status code of 200')
})

ファイルパス./sample_files/file.txtと./sample_files/cover.jpgを、ファイルシステム上にある任意の2つのファイルへのパスに置き換えます。または、こちらからこれらのファイルをコピーできます。

Fastifyの.injectメソッドを使うと、実際のサーバー上でコードを実行せずに、偽のHTTPリクエストをアプリケーションに送信できます。先ほどのコードでは、formAutoContentを使ってmyFormというフォームを作成し、テストケース内で偽のPOSTリクエストのリクエストボディとして使っています。テストケースは1つだけで、複数のファイルを含むフォームを送信したときに、アプリケーションがstatusCodeとして200を返すことを確認します。

このテストを実行し、成功することを確認します。package.jsonファイルのtestスクリプトを次のように置き換えてください。

"test": "tap --reporter=list --watch",

次に、以下を実行します。

npm test

テストが成功し、次のような出力が表示されるはずです。

Fastifyのファイルアップロードテストが成功し、ステータスコード200とカバレッジ概要が表示されたターミナル
テストに合格しました。

このチュートリアルのコードは、こちらのGitHubリポジトリで入手できます。

まとめ

この記事では、ファイルアップロードの基礎と、ファイルを含むフォームがmultipart/form-data content-typeでのみエンコードされる理由を学びました。また、サーバーアプリケーションでファイルを処理・保存する方法として、同じサーバー上の専用ディレクトリに保存する方法と、Amazon S3などのクラウドストレージにアップロードする方法があることも学びました。さらに、Node.js向けの高速でオーバーヘッドの少ないWebフレームワーク、Fastifyでアップロードを処理する方法を紹介しました。

さらに詳しく知りたい方は、Fastify-multipartプラグインのドキュメントとFastifyのテストガイドをご覧ください。

カテゴリー:

続きを読む

Blog

Stadium Summer:Snyk Connect Fan Zone Tour

SnykのFan Zone Tourでは、8都市と3回のオンラインセッションで、AIセキュリティのワークショップやネットワーキング、楽しい競技を開催しました。参加者はスキルを磨き、アイデアを共有し、共に成長しました。

Blog

Snyk VulnBench JS 1.0:LLMは同じバグを2回見つけられるか?

Snyk VulnBench JS 1.0:300回の反復スキャンで、LLMのセキュリティ検出結果は実行ごとに異なる一方、SASTとモデルはそれぞれ異なる脆弱性の見落としを検出することが明らかに。

feature insights context
Blog

お客様と築くAIセキュリティ:Evoのデザインパートナープログラムから得た5つの教訓

SnykのEvoデザインパートナープログラムから得た5つの重要な教訓をご紹介します。AIの検出、リスクインテリジェンス、ポリシーの自動化によって、チームが生成AIを保護し、拡大するAI利用を大規模に統制する方法をご覧ください。