檔案上傳是 Web 開發高頻需求,涉及客戶端協議、伺服器配置、安全驗證、多檔處理等多個面向。本文從 HTTP 原理出發,層層遞進至物件導向封裝與下載實作。
method="post" enctype="multipart/form-data"<input type="file" name="myFile"> 選擇檔案upload_tmp_dir($_FILES['tmp_name'])move_uploaded_file() 移至永久目錄enctype:必須設為 multipart/form-dataaccept:客戶端檔案類型過濾(僅 UI 提示,不可信)MAX_FILE_SIZE:隱藏欄位限制大小(客戶端可繞過,不可信)Array(
[myFile] => Array(
[name] => 原始檔名
[type] => MIME 類型(瀏覽器提供,不可信)
[tmp_name] => 暫存路徑(關鍵)
[error] => 錯誤代碼(0=成功)
[size] => 檔案大小(位元組)
)
)
function validateUpload($fileInfo, $config) {
// 1. 錯誤代碼檢查
if ($fileInfo['error'] !== UPLOAD_ERR_OK) {
throw new Exception(getUploadErrorMessage($fileInfo['error']));
}
// 2. 檔案大小限制(php.ini + 程式雙重保障)
if ($fileInfo['size'] > $config['maxSize']) {
throw new Exception('檔案超過大小限制');
}
// 3. 副檔名白名單(而非黑名單)
$ext = strtolower(pathinfo($fileInfo['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $config['allowExt'])) {
throw new Exception('不允許的檔案類型');
}
// 4. MIME 類型二次驗證
if (!in_array($fileInfo['type'], $config['allowMime'])) {
throw new Exception('MIME 類型不符');
}
// 5. 真實內容驗證(防偽裝圖片)
if ($config['checkImage'] && !@getimagesize($fileInfo['tmp_name'])) {
throw new Exception('非真實圖片檔案');
}
// 6. 上傳方式驗證
if (!is_uploaded_file($fileInfo['tmp_name'])) {
throw new Exception('非 HTTP POST 上傳');
}
}
// 唯一檔名:防覆蓋、防猜測、防注入
$uniName = md5(uniqid(microtime(true), true)) . '.' . $ext;
$destination = $uploadPath . '/' . $uniName;
// 目錄自動建立
if (!file_exists($uploadPath)) {
mkdir($uploadPath, 0777, true);
}
// 移動檔案(相當於剪貼,暫存檔消失)
if (!move_uploaded_file($fileInfo['tmp_name'], $destination)) {
throw new Exception('檔案移動失敗');
}
foreach ($_FILES as $fileInfo) {
uploadFile($fileInfo); // 復用單檔邏輯
}
name="files[]"(推薦)// $_FILES 結構變為三維陣列,需正規化
function normalizeFiles() {
$files = [];
foreach ($_FILES as $field) {
if (is_string($field['name'])) {
$files[] = $field; // 單檔
} else {
// 多檔:重組為單檔結構陣列
foreach ($field['name'] as $i => $name) {
$files[] = [
'name' => $name,
'type' => $field['type'][$i],
'tmp_name' => $field['tmp_name'][$i],
'error' => $field['error'][$i],
'size' => $field['size'][$i],
];
}
}
}
return $files;
}
class FileUploader {
protected $config = [
'maxSize' => 5 * 1024 * 1024,
'allowExt' => ['jpg', 'jpeg', 'png', 'gif'],
'allowMime' => ['image/jpeg', 'image/png', 'image/gif'],
'uploadPath' => './uploads',
'checkImage' => true,
];
protected $fileInfo;
protected $error;
public function __construct($fieldName, array $customConfig = []) {
$this->config = array_merge($this->config, $customConfig);
$this->fileInfo = $_FILES[$fieldName];
}
public function upload() {
$this->validate();
$this->ensureDirectory();
$dest = $this->generateDestination();
if (!move_uploaded_file($this->fileInfo['tmp_name'], $dest)) {
throw new Exception('移動失敗');
}
return $dest;
}
// 驗證、目錄、命名等私有方法...
}
// download.php
$filename = basename($_GET['file']); // 防止目錄遍歷
$filepath = '/var/www/files/' . $filename;
if (!file_exists($filepath)) {
header('HTTP/1.1 404 Not Found');
exit;
}
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: ' . filesize($filepath));
header('Cache-Control: no-cache, must-revalidate');
readfile($filepath); // 直接輸出,不佔用 PHP 記憶體
| php.ini 設定 | 建議值 | 說明 |
|---|---|---|
| file_uploads | On | 啟用上傳 |
| upload_max_filesize | 10M | 單檔上限 |
| post_max_size | 20M | POST 總上限 > upload_max_filesize |
| max_file_uploads | 20 | 單次最大檔案數 |
| upload_tmp_dir | /tmp/uploads | 暫存目錄(需寫入權限) |
| max_execution_time | 300 | 大檔上傳需較長時間 |
| memory_limit | 128M | 下載用 readfile 不佔記憶體 |
掌握這套完整體系,從原理到封裝,讓檔案上傳下載功能安全、高效、可維護。