ファイルのアップロード

ファイルのアップロード

AdonisJSは、multipart/form-dataコンテンツタイプを使用して送信されたユーザーがアップロードしたファイルを処理するための一流のサポートを提供しています。ファイルは、bodyparserミドルウェアを使用して自動的に処理され、オペレーティングシステムのtmpディレクトリに保存されます。

後で、コントローラ内でファイルにアクセスし、バリデーションし、永続的な場所やS3のようなクラウドストレージサービスに移動できます。

ユーザーがアップロードしたファイルにアクセスする

request.fileメソッドを使用して、ユーザーがアップロードしたファイルにアクセスできます。このメソッドはフィールド名を受け入れ、MultipartFileのインスタンスを返します。

import { HttpContext } from '@adonisjs/core/http'
export default class UserAvatarsController {
update({ request }: HttpContext) {
// ハイライト開始
const avatar = request.file('avatar')
console.log(avatar)
// ハイライト終了
}
}

単一の入力フィールドを使用して複数のファイルをアップロードする場合、request.filesメソッドを使用してそれらにアクセスできます。このメソッドはフィールド名を受け入れ、MultipartFileの配列を返します。

import { HttpContext } from '@adonisjs/core/http'
export default class InvoicesController {
update({ request }: HttpContext) {
// ハイライト開始
const invoiceDocuments = request.files('documents')
for (let document of invoiceDocuments) {
console.log(document)
}
// ハイライト終了
}
}

ファイルの手動バリデーション

バリデータを使用してファイルをバリデーションするか、request.fileメソッドを使用してバリデーションルールを定義できます。

次の例では、request.fileメソッドを使用してバリデーションルールをインラインで定義し、file.errorsプロパティを使用してバリデーションエラーにアクセスします。

const avatar = request.file('avatar', {
size: '2mb',
extnames: ['jpg', 'png', 'jpeg']
})
if (!avatar.isValid) {
return response.badRequest({
errors: avatar.errors
})
}

ファイルの配列を扱う場合、ファイルごとにバリデーションが失敗したかどうかを確認するためにファイルを反復処理できます。

request.filesメソッドに提供されるバリデーションオプションはすべてのファイルに適用されます。次の例では、各ファイルのサイズが2mb未満であることを期待し、許可されるファイル拡張子のいずれかを持っている必要があります。

const invoiceDocuments = request.files('documents', {
size: '2mb',
extnames: ['jpg', 'png', 'pdf']
})
/**
* 無効なドキュメントのコレクションを作成する
*/
let invalidDocuments = invoiceDocuments.filter((document) => {
return !document.isValid
})
if (invalidDocuments.length) {
/**
* ファイル名とエラーを横に並べてレスポンスする
*/
return response.badRequest({
errors: invalidDocuments.map((document) => {
name: document.clientName,
errors: document.errors,
})
})
}

バリデータを使用してファイルをバリデーションする

前のセクションで手動でファイルをバリデーションする方法を見てきましたが、バリデータを使用してファイルをバリデーションすることもできます。バリデータを使用する場合、エラーを手動でチェックする必要はありません。バリデーションパイプラインがそれを処理します。

// app/validators/user_validator.ts
import vine from '@vinejs/vine'
export const updateAvatarValidator = vine.compile(
vine.object({
// ハイライト開始
avatar: vine.file({
size: '2mb',
extnames: ['jpg', 'png', 'pdf']
})
// ハイライト終了
})
)
import { HttpContext } from '@adonisjs/core/http'
import { updateAvatarValidator } from '#validators/user_validator'
export default class UserAvatarsController {
async update({ request }: HttpContext) {
// ハイライト開始
const { avatar } = await request.validateUsing(
updateAvatarValidator
)
// ハイライト終了
}
}

vine.arrayタイプを使用してファイルの配列をバリデーションすることもできます。

例:

import vine from '@vinejs/vine'
export const createInvoiceValidator = vine.compile(
vine.object({
// ハイライト開始
documents: vine.array(
vine.file({
size: '2mb',
extnames: ['jpg', 'png', 'pdf']
})
)
// ハイライト終了
})
)

ファイルを永続的な場所に移動する

デフォルトでは、ユーザーがアップロードしたファイルはオペレーティングシステムのtmpディレクトリに保存され、コンピュータがtmpディレクトリをクリーンアップすると削除される可能性があります。

そのため、ファイルを永続的な場所に保存することをオススメします。file.moveメソッドを使用して、同じファイルシステム内でファイルを移動できます。このメソッドはファイルを移動するための絶対パスを受け入れます。

import app from '@adonisjs/core/services/app'
const avatar = request.file('avatar', {
size: '2mb',
extnames: ['jpg', 'png', 'jpeg']
})
// ハイライト開始
/**
* アバターを「storage/uploads」ディレクトリに移動する
*/
await avatar.move(app.makePath('storage/uploads'))
// ハイライト終了

移動されたファイルに一意のランダムな名前を提供することをオススメします。そのために、cuidヘルパーを使用できます。

// ハイライト開始
import { cuid } from '@adonisjs/core/helpers'
// ハイライト終了
import app from '@adonisjs/core/services/app'
await avatar.move(app.makePath('storage/uploads'), {
// ハイライト開始
name: `${cuid()}.${avatar.extname}`
// ハイライト終了
})

ファイルが移動された後、その名前をデータベースに保存してあとで参照できます。

await avatar.move(app.makePath('uploads'))
/**
* ファイル名をアバターとして保存し、ユーザーモデルに永続化するダミーコード
*/
auth.user!.avatar = avatar.fileName!
await auth.user.save()

ファイルのプロパティ

以下は、MultipartFileインスタンスでアクセスできるプロパティのリストです。

プロパティ説明
fieldNameHTMLの入力フィールドの名前。
clientNameユーザーのコンピュータ上のファイル名。
sizeファイルのサイズ(バイト単位)。
extnameファイルの拡張子
errors特定のファイルに関連するエラーの配列。
typeファイルのMIMEタイプ
subtypeファイルのMIMEサブタイプ
filePathmove操作後のファイルへの絶対パス。
fileNamemove操作後のファイル名。
tmpPathtmpディレクトリ内のファイルへの絶対パス。
metaファイルに関連するメタデータ(キーと値のペア)です。デフォルトではオブジェクトは空です。
validatedファイルがバリデーションされたかどうかを示すブール値です。
isValidファイルがバリデーションルールをパスしたかどうかを示すブール値です。
hasErrors1つ以上のエラーが特定のファイルに関連付けられているかどうかを示すブール値です。

ファイルの提供

アプリケーションコードと同じファイルシステムにユーザーがアップロードしたファイルを永続化した場合、ルートを作成し、response.downloadメソッドを使用してファイルを提供できます。

import { sep, normalize } from 'node:path'
import app from '@adonisjs/core/services/app'
import router from '@adonisjs/core/services/router'
const PATH_TRAVERSAL_REGEX = /(?:^|[\\/])\.\.(?:[\\/]|$)/
router.get('/uploads/*', ({ request, response }) => {
const filePath = request.param('*').join(sep)
const normalizedPath = normalize(filePath)
if (PATH_TRAVERSAL_REGEX.test(normalizedPath)) {
return response.badRequest('Malformed path')
}
const absolutePath = app.makePath('uploads', normalizedPath)
return response.download(absolutePath)
})
  • ワイルドカードルートパラメータを使用してファイルパスを取得し、配列を文字列に変換します。

  • 次に、Node.jsのpathモジュールを使用してパスを正規化します。

  • PATH_TRAVERSAL_REGEXを使用して、このルートをパストラバーサル攻撃から保護します。

  • 最後に、normalizedPathuploadsディレクトリ内の絶対パスに変換し、response.downloadメソッドを使用してファイルを提供します。

Driveを使用してファイルをアップロードおよび提供する

Driveは、AdonisJSコアチームによって作成されたファイルシステムの抽象化です。Driveを使用してユーザーがアップロードしたファイルを管理し、それらをローカルファイルシステムに保存するか、S3やGCSのようなクラウドストレージサービスに移動できます。

手動でファイルをアップロードおよび提供する代わりに、Driveを使用することをオススメします。Driveはパストラバーサルなどの多くのセキュリティ上の懸念事項を処理し、複数のストレージプロバイダにわたる統一されたAPIを提供します。

Driveについて詳しく学ぶ

高度な - 自己処理マルチパートストリーム

マルチパートリクエストの自動処理をオフにし、ストリームを自己処理することで、高度なユースケースに対応できます。config/bodyparser.tsファイルを開き、次のオプションのいずれかを変更して自動処理を無効にします。

{
multipart: {
/**
* すべてのHTTPリクエストに対してマルチパートストリームを手動で自己処理する場合は、falseに設定します。
*/
autoProcess: false
}
}
{
multipart: {
/**
* マルチパートストリームを自己処理するルートパターンの配列を定義します。
*/
processManually: ['/assets']
}
}

自動処理を無効にした後、request.multipartオブジェクトを使用して個々のファイルを処理できます。

次の例では、Node.jsのstream.pipelineメソッドを使用してマルチパートの読み込み可能なストリームを処理し、ディスク上のファイルに書き込みます。ただし、このファイルをs3のような外部サービスにストリームすることもできます。

import { createWriteStream } from 'node:fs'
import app from '@adonisjs/core/services/app'
import { pipeline } from 'node:stream/promises'
import { HttpContext } from '@adonisjs/core/http'
export default class AssetsController {
async store({ request }: HttpContext) {
/**
* ステップ1: ファイルリスナーを定義する
*/
request.multipart.onFile('*', {}, async (part, reporter) => {
part.pause()
part.on('data', reporter)
const filePath = app.makePath(part.file.clientName)
await pipeline(part, createWriteStream(filePath))
return { filePath }
})
/**
* ステップ2: ストリームを処理する
*/
await request.multipart.process()
/**
* ステップ3: 処理されたファイルにアクセスする
*/
return request.allFiles()
}
}
  • multipart.onFileメソッドは、ファイルを処理するために使用する入力フィールド名を第一パラメータとして受け入れます。すべてのファイルを処理するためにワイルドカード*を使用することもできます。

  • onFileリスナーは、最初のパラメータとしてpart(読み込み可能なストリーム)を受け取り、2番目のパラメータとしてreporter関数を受け取ります。

  • reporter関数は、ストリームの進行状況を追跡するために使用されます。これにより、AdonisJSはストリームが処理された後に処理されたバイト、ファイルの拡張子、およびその他のメタデータへのアクセスを提供できます。

  • 最後に、onFileリスナーから返されるプロパティのオブジェクトは、request.fileまたはrequest.allFiles()メソッドを使用してアクセスするファイルオブジェクトとマージされます。

エラーハンドリング

partオブジェクト上のerrorイベントをリッスンし、エラーを手動で処理する必要があります。通常、ストリームリーダー(書き込み可能なストリーム)は内部的にこのイベントをリッスンし、書き込み操作を中止します。

ストリームパートのバリデーション

AdonisJSでは、マルチパートストリームを手動で処理する場合でも、ストリームパート(ファイル)をバリデーションできます。エラーが発生した場合、partオブジェクトでerrorイベントが発生します。

multipart.onFileメソッドは、2番目のパラメータとしてバリデーションオプションを受け入れます。また、dataイベントをリッスンし、reporterメソッドをバインドする必要があります。そうしないと、バリデーションは行われません。

request.multipart.onFile('*', {
size: '2mb',
extnames: ['jpg', 'png', 'jpeg']
}, async (part, reporter) => {
/**
* ストリームのバリデーションを実行するためには、以下の2行が必要です
*/
part.pause()
part.on('data', reporter)
})