錯誤代碼常量對照表
| 常量 | 值 | 含義 | 典型原因 |
| 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 | 找不到臨時目錄 | php.ini upload_tmp_dir 未配置或無權限 |
| UPLOAD_ERR_CANT_WRITE | 7 | 文件寫入失敗 | 磁盤滿、權限不足、SELinux |
| UPLOAD_ERR_EXTENSION | 8 | PHP 擴展阻止上傳 | 如 suhosin、自定義擴展攔截 |
注意:值 5 保留未使用,歷史上曾表示「臨時目錄丟失」但已合併到 6。
標準處理代碼模板
<?php
function handleUpload(array $file, array $allowedTypes = [], int $maxSize = 2 * 1024 * 1024): array {
// 1. 檢查錯誤代碼
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'code' => $file['error'], 'message' => uploadErrorMessage($file['error'])];
}
// 2. 檢查文件大小(雙重保險)
if ($file['size'] > $maxSize) {
return ['success' => false, 'code' => UPLOAD_ERR_INI_SIZE, 'message' => '文件超過大小限制'];
}
// 3. 檢查 MIME 類型(不信任瀏覽器提供的 type)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!empty($allowedTypes) && !in_array($mime, $allowedTypes, true)) {
return ['success' => false, 'code' => 400, 'message' => '文件類型不允許: ' . $mime];
}
// 4. 檢查副檔名
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$allowedExt = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
if (!in_array($ext, $allowedExt, true)) {
return ['success' => false, 'code' => 400, 'message' => '副檔名不允許'];
}
// 5. 生成安全文件名並移動
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$destDir = __DIR__ . '/uploads/' . date('Y/m/d');
if (!is_dir($destDir)) mkdir($destDir, 0755, true);
$destPath = $destDir . '/' . $newName;
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
return ['success' => false, 'code' => UPLOAD_ERR_CANT_WRITE, 'message' => '文件移動失敗'];
}
return [
'success' => true,
'path' => $destPath,
'url' => '/uploads/' . date('Y/m/d') . '/' . $newName,
'size' => $file['size'],
'mime' => $mime,
];
}
function uploadErrorMessage(int $code): string {
return match ($code) {
UPLOAD_ERR_INI_SIZE => '文件超過服務器限制 (upload_max_filesize)',
UPLOAD_ERR_FORM_SIZE => '文件超過表單限制 (MAX_FILE_SIZE)',
UPLOAD_ERR_PARTIAL => '文件上傳不完整',
UPLOAD_ERR_NO_FILE => '未選擇文件',
UPLOAD_ERR_NO_TMP_DIR => '服務器臨時目錄配置錯誤',
UPLOAD_ERR_CANT_WRITE => '文件寫入失敗',
UPLOAD_ERR_EXTENSION => '被 PHP 擴展攔截',
default => '未知上傳錯誤',
};
}
// 使用
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) {
$result = handleUpload($_FILES['avatar'], ['image/jpeg', 'image/png', 'image/gif']);
header('Content-Type: application/json');
echo json_encode($result, JSON_UNESCAPED_UNICODE);
}
?>
關鍵配置檢查清單
| 配置項 | 推薦值 | 說明 |
| file_uploads | On | 啟用文件上傳 |
| upload_max_filesize | 10M | 單文件最大 |
| post_max_size | 12M | POST 總大小,必須 ≥ upload_max_filesize |
| max_file_uploads | 20 | 單次請求最大文件數 |
| upload_tmp_dir | /tmp/php_uploads | 專用臨時目錄,確保可寫 |
| max_execution_time | 60 | 大文件上傳需足夠時間 |
| memory_limit | 128M | 處理圖片縮略圖等需內存 |
進階:分片上傳大文件(斷點續傳)
<?php
// 前端將大文件切片,每片發送:chunk_index, total_chunks, file_hash, filename
$chunkIndex = (int)$_POST['chunk_index'];
$totalChunks = (int)$_POST['total_chunks'];
$fileHash = $_POST['file_hash']; // 全文件 SHA256
$filename = basename($_POST['filename']);
$chunkDir = sys_get_temp_dir() . "/chunks/$fileHash";
if (!is_dir($chunkDir)) mkdir($chunkDir, 0755, true);
$chunkPath = "$chunkDir/$chunkIndex";
// 接收分片
move_uploaded_file($_FILES['chunk']['tmp_name'], $chunkPath);
// 判斷是否為最後一片
if ($chunkIndex === $totalChunks - 1) {
// 合併
$finalPath = "/uploads/$fileHash-" . $filename;
$fp = fopen($finalPath, 'wb');
for ($i = 0; $i < $totalChunks; $i++) {
fwrite($fp, file_get_contents("$chunkDir/$i"));
}
fclose($fp);
// 清理分片
array_map('unlink', glob("$chunkDir/*"));
rmdir($chunkDir);
echo json_encode(['success' => true, 'path' => $finalPath]);
} else {
echo json_encode(['success' => true, 'message' => "分片 $chunkIndex 接收完成"]);
}
?>
安全加固要點
- ✅ 使用
finfo 驗證真實 MIME,不信任 $_FILES['type']
- ✅ 文件名用隨機哈希重命名,防止路徑遍歷、覆蓋、腳本執行
- ✅ 上傳目錄設置
php_flag engine off / Nginx location ~ \.php$ { deny all; } 禁用腳本執行
- ✅ 設置
Content-Security-Policy 限制上傳域
- ✅ 病毒掃描集成(ClamAV 等)
- ✅ 限制上傳頻率、大小、類型