Skip to content

ファイルアップロード

15.4.1. FormDataの構成

ファイルアップロードはFormDataオブジェクトを使用し、Content-Typemultipart/form-dataで送信します。

typescript
async function uploadFile(file: File, metadata: { category: string }) {
  const formData = new FormData()
  formData.append('file', file)
  formData.append('category', metadata.category)

  const response = await axios.post('/api/files', formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
  })

  return response.data
}
  • ファイルとメタデータを1つのFormDataに含めて単一リクエストで送信します。
  • Content-Typeヘッダーは明示的にmultipart/form-dataを指定します。
  • 複数ファイルアップロード時は同一キーでappendを繰り返し呼び出します。

15.4.2. 進捗率の表示

大容量ファイルアップロード時、AxiosのonUploadProgressコールバックを活用して進捗率を表示します。

vue
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'

const progress = ref(0)
const isUploading = ref(false)

async function upload(file: File) {
  isUploading.value = true
  progress.value = 0

  const formData = new FormData()
  formData.append('file', file)

  try {
    await axios.post('/api/files', formData, {
      headers: { 'Content-Type': 'multipart/form-data' },
      onUploadProgress(event) {
        if (event.total) {
          progress.value = Math.round((event.loaded / event.total) * 100)
        }
      },
    })
  } finally {
    isUploading.value = false
  }
}
</script>

<template>
  <div v-if="isUploading" class="progress-bar">
    <div class="progress-fill" :style="{ width: `${progress}%` }" />
    <span>{{ progress }}%</span>
  </div>
</template>
  • event.totalundefinedになる可能性があるため、必ず存在有無を確認します。
  • 進捗率は0~100の整数に変換して表示します。

15.4.3. ドラッグアンドドロップ

ファイルのドラッグアンドドロップは@dragover.prevent@drop.preventイベントで処理します。

vue
<script setup lang="ts">
import { ref } from 'vue'

const isDragOver = ref(false)
const selectedFile = ref<File | null>(null)

function handleDrop(event: DragEvent) {
  isDragOver.value = false
  const files = event.dataTransfer?.files
  if (files && files.length > 0) {
    selectedFile.value = files[0]
  }
}

function handleFileSelect(event: Event) {
  const target = event.target as HTMLInputElement
  if (target.files && target.files.length > 0) {
    selectedFile.value = target.files[0]
  }
}
</script>

<template>
  <div
    class="drop-zone"
    :class="{ 'drag-over': isDragOver }"
    @dragover.prevent="isDragOver = true"
    @dragleave="isDragOver = false"
    @drop.prevent="handleDrop"
  >
    <p>ファイルをドラッグするか、クリックして選択してください。</p>
    <input type="file" @change="handleFileSelect" />
  </div>
  <p v-if="selectedFile">選択されたファイル: {{ selectedFile.name }}</p>
</template>
  • @dragover.preventはブラウザのデフォルト動作(ファイルを開く)をブロックするために必須で適用します。
  • ドラッグ状態(isDragOver)を追跡して視覚的フィードバックを提供します。
  • <input type="file">を併設してクリック方式もサポートします。

15.4.4. クライアント検証

ファイルアップロード前にクライアントで拡張子、サイズ、MIMEタイプを検証します。

typescript
// src/utils/fileValidation.ts
interface FileValidationResult {
  valid: boolean
  error: string | null
}

const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'docx']
const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB
const ALLOWED_MIME_TYPES = [
  'image/jpeg',
  'image/png',
  'image/gif',
  'application/pdf',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
]

export function validateFile(file: File): FileValidationResult {
  const extension = file.name.split('.').pop()?.toLowerCase() ?? ''
  if (!ALLOWED_EXTENSIONS.includes(extension)) {
    return { valid: false, error: `許可されていないファイル形式です。(${ALLOWED_EXTENSIONS.join(', ')})` }
  }

  if (file.size > MAX_FILE_SIZE) {
    return { valid: false, error: 'ファイルサイズは最大50MBまで許可されます。' }
  }

  if (!ALLOWED_MIME_TYPES.includes(file.type)) {
    return { valid: false, error: 'MIMEタイプが許可リストに含まれていません。' }
  }

  return { valid: true, error: null }
}
検証項目基準備考
拡張子許可リストベースバックエンドの許可リストと同期
ファイルサイズ最大50MBプロジェクト要件に応じて調整可能
MIMEタイプ許可リストベース拡張子偽装防止のための二重検証
  • 許可拡張子リストはバックエンドの標準と同一に維持しなければなりません。
  • クライアント検証はユーザーの利便性のための事前検証であり、サーバー側の検証を代替するものではありません。

TIENIPIA QUALIFIED STANDARD