檔案上傳是 Web 開發核心功能,但細節陷阱眾多。本文系統整理 7 大常見場景:基礎上傳、$_FILES 結構、MIME 驗證、重名處理、日期分目錄、多檔上傳、大檔案配置,提供生產級程式碼與最佳實踐。
1. 基礎上傳表單與處理
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" id="file" required>
<input type="hidden" name="MAX_FILE_SIZE" value="2000000"> <!-- 2MB 客戶端提示 -->
<button type="submit">上傳</button>
</form>
<?php
// 基礎驗證
if ($_FILES['file']['error'] !== UPLOAD_ERR_OK) {
die('上傳錯誤: ' . $_FILES['file']['error']);
}
if ($_FILES['file']['size'] > 2_000_000) {
die('檔案過大');
}
$dest = 'uploads/' . basename($_FILES['file']['name']);
if (move_uploaded_file($_FILES['file']['tmp_name'], $dest)) {
echo "上傳成功: $dest";
}
?>
2. $_FILES 結構與錯誤碼對照
| 鍵名 | 說明 |
| name | 用戶端原始檔名 |
| type | 瀏覽器報告 MIME (不可信) |
| size | 檔案大小 (位元組) |
| tmp_name | 伺服器暫存路徑 |
| error | 錯誤碼 (見下表) |
| 常數 | 值 | 說明 |
| UPLOAD_ERR_OK | 0 | 成功 |
| UPLOAD_ERR_INI_SIZE | 1 | 超過 php.ini upload_max_filesize |
| UPLOAD_ERR_FORM_SIZE | 2 | 超過表單 MAX_FILE_SIZE |
| UPLOAD_ERR_PARTIAL | 3 | 部分上傳 (網路中斷) |
| UPLOAD_ERR_NO_FILE | 4 | 未選擇檔案 |
| UPLOAD_ERR_NO_TMP_DIR | 6 | 缺少暫存目錄 |
| UPLOAD_ERR_CANT_WRITE | 7 | 寫入失敗 (權限/磁碟滿) |
3. MIME 類型驗證 (核心安全)
<?php
// ❌ 錯誤:信任 $_FILES['file']['type']
// ✅ 正確:使用 Fileinfo 擴展檢測實際內容
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['file']['tmp_name']);
$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf'];
if (!in_array($mime, $allowed, true)) {
die('檔案類型不允許: ' . $mime);
}
?>
4. 重名檔案自動重命名
<?php
function getUniqueFilename($dir, $filename) {
$ext = pathinfo($filename, PATHINFO_EXTENSION);
$base = basename($filename, '.' . $ext);
$counter = 1;
$newName = $filename;
while (file_exists($dir . $newName)) {
$newName = $base . '_' . $counter++ . ($ext ? '.' . $ext : '');
}
return $newName;
}
// 使用
$safeName = getUniqueFilename('uploads/', $_FILES['file']['name']);
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $safeName);
?>
5. 依日期自動分目錄儲存
<?php
$dateDir = date('Y/m/d/'); // 2024/01/15/
$fullDir = 'uploads/' . $dateDir;
if (!is_dir($fullDir)) {
mkdir($fullDir, 0755, true); // 遞迴建立
}
$dest = $fullDir . basename($_FILES['file']['name']);
move_uploaded_file($_FILES['file']['tmp_name'], $dest);
?>
6. 多檔案上傳處理
<form method="post" enctype="multipart/form-data">
<input type="file" name="files[]" multiple>
<button type="submit">上傳多檔</button>
</form>
<?php
// $_FILES 結構轉換:從「欄位優先」轉為「檔案優先」
function normalizeFilesArray($files) {
$normalized = [];
foreach ($files['name'] as $index => $name) {
if ($files['error'][$index] === UPLOAD_ERR_OK) {
$normalized[] = [
'name' => $name,
'type' => $files['type'][$index],
'tmp_name' => $files['tmp_name'][$index],
'size' => $files['size'][$index],
'error' => $files['error'][$index],
];
}
}
return $normalized;
}
$files = normalizeFilesArray($_FILES['files']);
foreach ($files as $file) {
// 單檔處理邏輯...
}
?>
7. 大檔案上傳伺服器配置
| 配置項 | 建議值 | 說明 |
| php.ini: upload_max_filesize | 100M | 單檔最大值 |
| php.ini: post_max_size | 120M | POST 總大小 > upload_max_filesize |
| php.ini: max_execution_time | 300 | 上傳處理最長秒數 |
| php.ini: max_input_time | 300 | 接收資料最長秒數 |
| php.ini: memory_limit | 256M | 腳本記憶體上限 |
| Nginx: client_max_body_size | 100M | Nginx 層面限制 |
| Apache: LimitRequestBody | 104857600 | Apache 層面限制 (位元組) |
分塊上傳 (超大檔案 > 100MB)
<?php
// 前端:將檔案切片 (如 5MB/片),並發上傳
// 後端:合併切片
function mergeChunks($chunkDir, $finalPath, $totalChunks) {
$out = fopen($finalPath, 'wb');
for ($i = 0; $i < $totalChunks; $i++) {
$chunkFile = $chunkDir . "/part_$i";
if (!file_exists($chunkFile)) {
fclose($out);
return false; // 缺片
}
$in = fopen($chunkFile, 'rb');
stream_copy_to_stream($in, $out);
fclose($in);
unlink($chunkFile); // 清理切片
}
fclose($out);
return true;
}
?>
安全檢查清單
- ✅ MIME 類型白名單驗證 (Fileinfo)
- ✅ 檔案大小限制 (雙重:客戶端 + 伺服端)
- ✅ 檔名安全化 (移除路徑、特殊字元、生成唯一名)
- ✅ 儲存目錄非 Web 可執行 (或設定 Nginx/Apache 禁用腳本執行)
- ✅ 病毒掃描 (整合 ClamAV `clamscan`)
- ✅ 速率限制 (防止濫用)
- ✅ CSRF Token 保護表單