Skip to content

File Upload ​

15.4.1. FormData Construction ​

File uploads must use the FormData object and be sent with Content-Type set to multipart/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
}
  • The file and metadata must be included in a single FormData and sent as one request.
  • The Content-Type header must be explicitly set to multipart/form-data.
  • For multiple file uploads, append must be called repeatedly with the same key.

15.4.2. Progress Display ​

For large file uploads, the upload progress must be displayed using the Axios onUploadProgress callback.

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.total may be undefined, so its existence must be checked.
  • The progress must be converted to an integer between 0 and 100 for display.

15.4.3. Drag and Drop ​

File drag and drop must be handled using the @dragover.prevent and @drop.prevent events.

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>Drag a file here or click to select.</p>
    <input type="file" @change="handleFileSelect" />
  </div>
  <p v-if="selectedFile">Selected file: {{ selectedFile.name }}</p>
</template>
  • @dragover.prevent must be applied to prevent the browser's default behavior (opening the file).
  • The drag state (isDragOver) must be tracked to provide visual feedback.
  • An <input type="file"> must be placed alongside to support click-based selection as well.

15.4.4. Client-Side Validation ​

Before uploading a file, the extension, size, and MIME type must be validated on the client side.

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: `File type not allowed. (${ALLOWED_EXTENSIONS.join(', ')})` }
  }

  if (file.size > MAX_FILE_SIZE) {
    return { valid: false, error: 'Maximum file size is 50MB.' }
  }

  if (!ALLOWED_MIME_TYPES.includes(file.type)) {
    return { valid: false, error: 'MIME type is not in the allowed list.' }
  }

  return { valid: true, error: null }
}
Validation ItemCriteriaNotes
ExtensionAllowlist-basedMust be synchronized with the backend allowlist
File sizeMaximum 50MBMay be adjusted per project requirements
MIME typeAllowlist-basedDual validation to prevent extension spoofing
  • The allowed extension list must be kept in sync with the backend standard.
  • Client-side validation is a preliminary check for user convenience and must not replace server-side validation.

TIENIPIA QUALIFIED STANDARD