後端 PHP (upload.php)
<?php
header('Content-Type: application/json');
if (!isset($_FILES['myfile'])) {
echo json_encode(['code'=>1, 'msg'=>'No file']);
exit;
}
$file = $_FILES['myfile'];
$allowTypes = ['image/jpeg','image/png','image/gif','image/webp'];
$maxSize = 5 * 1024 * 1024; // 5MB
if (!in_array($file['type'], $allowTypes)) {
echo json_encode(['code'=>2, 'msg'=>'Type not allowed']);
exit;
}
if ($file['size'] > $maxSize) {
echo json_encode(['code'=>3, 'msg'=>'File too large']);
exit;
}
if ($file['error'] !== UPLOAD_ERR_OK) {
echo json_encode(['code'=>4, 'msg'=>'Upload error: '.$file['error']]);
exit;
}
// 目錄:uploads/2024/01/15/
$dateDir = date('Y/m/d');
$uploadDir = __DIR__ . "/uploads/$dateDir";
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
// 重新命名避免衝突
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = bin2hex(random_bytes(8)) . '.' . $ext;
$dest = "$uploadDir/$newName";
if (move_uploaded_file($file['tmp_name'], $dest)) {
// 回傳相對路徑供前端顯示
$url = "/uploads/$dateDir/$newName";
echo json_encode(['code'=>0, 'url'=>$url, 'name'=>$file['name']]);
} else {
echo json_encode(['code'=>5, 'msg'=>'Move failed']);
}
?>
前端:原生 Fetch API (現代瀏覽器)
<input type="file" id="fileInput" accept="image/*" />
<button id="uploadBtn">上傳</button>
<div id="preview"></div>
<div id="progress"></div>
<script>
document.getElementById('uploadBtn').addEventListener('click', async () => {
const file = document.getElementById('fileInput').files[0];
if (!file) return alert('請選擇檔案');
const formData = new FormData();
formData.append('myfile', file);
const progressEl = document.getElementById('progress');
progressEl.textContent = '上傳中...';
try {
const res = await fetch('upload.php', {
method: 'POST',
body: formData
// 不需手動設定 Content-Type,瀏覽器會自動加上 boundary
});
const data = await res.json();
if (data.code === 0) {
progressEl.textContent = '成功!';
document.getElementById('preview').innerHTML =
`
`;
} else {
progressEl.textContent = '失敗: ' + data.msg;
}
} catch (e) {
progressEl.textContent = '網路錯誤: ' + e.message;
}
});
</script>
前端:XMLHttpRequest (支援 IE10+、進度條)
function uploadWithXHR(file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', 'upload.php');
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
onProgress(Math.round(e.loaded / e.total * 100));
}
};
xhr.onload = () => {
if (xhr.status === 200) resolve(JSON.parse(xhr.responseText));
else reject(new Error('HTTP ' + xhr.status));
};
xhr.onerror = () => reject(new Error('Network error'));
const fd = new FormData();
fd.append('myfile', file);
xhr.send(fd);
});
}
// 使用
uploadWithXHR(file, pct => progressEl.textContent = pct + '%')
.then(data => { /* ... */ })
.catch(err => { /* ... */ });
多檔案上傳
<input type="file" id="files" multiple accept="image/*" />
const files = [...document.getElementById('files').files];
for (const file of files) {
// 依序或平行上傳
await uploadWithXHR(file, pct => console.log(file.name, pct + '%'));
}
安全性檢查清單
- ✅ 檔案型態雙重驗證 (MIME +
getimagesize()/finfo)
- ✅ 檔案大小限制 (PHP + 前端雙重)
- ✅ 重新命名 (隨機雜湊 + 保留副檔名)
- ✅ 儲存目錄不可執行 PHP (Nginx:
location ~ \.php$ { deny all; })
- ✅ HTTPS 傳輸
- ✅ CSRF Token 驗證 (AJAX 請求攜帶 Header)
- ✅ 登入狀態驗證 (Session/JWT)