類別功能特色
- 支援 GIF/JPG/PNG 等圖片格式
- MIME 型別 + 檔頭魔術位元組雙重驗證 (防偽裝上傳)
- 單檔/總大小限制
- 隨機 MD5 檔名避免衝突
- 上傳目錄可配置
- 詳細錯誤碼與訊息回報
完整類別程式碼
'474946', // GIF87a/89a
'jpg' => 'FFD8FF', // JPEG
'png' => '89504E47', // PNG
];
private $files = [];
private $totalSize = 0;
private $errors = [];
public function __construct(array $files = [])
{
$this->files = $files ?: $_FILES;
}
public function upload(): array
{
$results = [];
foreach ($this->files['name'] as $idx => $name) {
$error = $this->files['error'][$idx];
$tmp = $this->files['tmp_name'][$idx];
$size = $this->files['size'][$idx];
$mime = $this->files['type'][$idx];
if ($error !== UPLOAD_ERR_OK) {
$results[] = $this->result($name, $mime, $size, $this->errorMsg($error), false);
continue;
}
if ($size > self::MAX_FILE_SIZE) {
$results[] = $this->result($name, $mime, $size, '單檔超過大小限制', false);
continue;
}
if ($this->totalSize + $size > self::MAX_TOTAL_SIZE) {
$results[] = $this->result($name, $mime, $size, '總大小超過限制', false);
continue;
}
if (!$this->validateType($tmp, $mime)) {
$results[] = $this->result($name, $mime, $size, '檔案類型不符', false);
continue;
}
$destName = $this->randomName(pathinfo($name, PATHINFO_EXTENSION));
$destPath = $_SERVER['DOCUMENT_ROOT'] . self::UPLOAD_DIR . $destName;
if (!is_dir(dirname($destPath))) mkdir(dirname($destPath), 0755, true);
if (move_uploaded_file($tmp, $destPath)) {
$this->totalSize += $size;
$results[] = $this->result($name, $mime, $size, '上傳成功', true, $destName);
} else {
$results[] = $this->result($name, $mime, $size, '移動檔案失敗', false);
}
}
return $results;
}
private function validateType(string $tmp, string $mime): bool
{
if (!in_array($mime, $this->allowedMimes, true)) return false;
$bytes = bin2hex(file_get_contents($tmp, false, null, 0, 8));
foreach ($this->magicBytes as $ext => $magic) {
if (stripos($bytes, $magic) === 0) return true;
}
return false;
}
private function randomName(string $ext): string
{
return md5(uniqid(microtime(true), true)) . '.' . strtolower($ext);
}
private function errorMsg(int $code): string
{
return match ($code) {
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => '檔案超過大小限制',
UPLOAD_ERR_PARTIAL => '檔案上傳不完整',
UPLOAD_ERR_NO_FILE => '未選擇檔案',
UPLOAD_ERR_NO_TMP_DIR => '缺少暫存目錄',
UPLOAD_ERR_CANT_WRITE => '寫入失敗',
UPLOAD_ERR_EXTENSION => '擴展阻止上傳',
default => '未知錯誤',
};
}
private function result(string $orig, string $mime, int $size, string $msg, bool $ok, string $saved = ''): array
{
return [
'original_name' => $orig,
'mime' => $mime,
'size' => $this->formatSize($size),
'message' => $msg,
'success' => $ok,
'saved_name' => $saved,
];
}
private function formatSize(int $bytes): string
{
if ($bytes < 1024) return $bytes . ' B';
if ($bytes < 1024 * 1024) return round($bytes / 1024, 1) . ' KB';
return round($bytes / (1024 * 1024), 1) . ' MB';
}
}
使用範例
require 'MultiFileUpload.php';
$uploader = new MultiFileUpload();
$results = $uploader->upload();
foreach ($results as $r) {
echo "{$r['original_name']} | {$r['size']} | {$r['message']}";
if ($r['success']) echo " → saved as {$r['saved_name']}";
echo "
\n";
}
安全性補充
- 上傳目錄**禁止執行腳本** (Nginx:
location /uploadfile { php_flag engine off; })
- 檔案副檔名**白名單驗證**,不要信任用戶提供的副檔名
- 資料庫儲存路徑使用**隨機名稱**,原始檔名另存欄位
- 考慮整合病毒掃描 (ClamAV) 或圖片重繪 (
imagecreatefromstring → imagejpeg) 清除潛在惡意代碼