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
FormDataand sent as one request. - The
Content-Typeheader must be explicitly set tomultipart/form-data. - For multiple file uploads,
appendmust 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.totalmay beundefined, 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.preventmust 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 Item | Criteria | Notes |
|---|---|---|
| Extension | Allowlist-based | Must be synchronized with the backend allowlist |
| File size | Maximum 50MB | May be adjusted per project requirements |
| MIME type | Allowlist-based | Dual 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.