<?php
session_start();
// 權限檢查略
// 配置
$config = [
'allowTypes' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
'maxSize' => 5 * 1024 * 1024, // 5MB
'uploadRoot' => __DIR__ . '/uploads',
'thumb' => true,
'thumbSizes' => [
'm' => [400, 400], // 中圖
's' => [150, 150], // 小圖
],
'watermark' => __DIR__ . '/assets/watermark.png', // 可選
];
// 1. 按日期創建目錄
$dateDir = date('Ymd');
$targetDir = $config['uploadRoot'] . '/' . $dateDir;
if (!is_dir($targetDir) && !mkdir($targetDir, 0755, true)) {
exit(json_encode(['code' => 500, 'msg' => '目錄創建失敗']));
}
// 2. 檢查請求
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($_FILES['file'])) {
exit(json_encode(['code' => 400, 'msg' => '無效請求']));
}
$file = $_FILES['file'];
// 3. 錯誤碼檢查
if ($file['error'] !== UPLOAD_ERR_OK) {
$errMsg = [
UPLOAD_ERR_INI_SIZE => '超過 php.ini 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 => '擴展阻止上傳',
];
exit(json_encode(['code' => 400, 'msg' => $errMsg[$file['error']] ?? '上傳錯誤']));
}
// 4. 文件大小
if ($file['size'] > $config['maxSize']) {
exit(json_encode(['code' => 400, 'msg' => '文件過大']));
}
// 5. MIME 類型二次驗證(防偽造 Content-Type)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, $config['allowTypes'], true)) {
exit(json_encode(['code' => 400, 'msg' => '禁止的文件類型']));
}
// 6. 構建目標文件名
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = bin2hex(random_bytes(8)) . '.' . strtolower($ext); // 隨機名防衝突
$destPath = $targetDir . '/' . $filename;
// 7. 移動文件
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
exit(json_encode(['code' => 500, 'msg' => '文件移動失敗']));
}
// 8. 生成縮略圖(需 GD/Imagick)
$thumbs = [];
if ($config['thumb'] && function_exists('imagecreatefromstring')) {
$srcImg = imagecreatefromstring(file_get_contents($destPath));
if ($srcImg) {
foreach ($config['thumbSizes'] as $prefix => [$w, $h]) {
$thumbPath = $targetDir . '/' . $prefix . '_' . $filename;
$thumb = imagescale($srcImg, $w, $h, IMG_BICUBIC);
// 加水印
if ($config['watermark'] && is_file($config['watermark'])) {
$wm = imagecreatefrompng($config['watermark']);
$wmW = imagesx($wm); $wmH = imagesy($wm);
$dstX = imagesx($thumb) - $wmW - 10;
$dstY = imagesy($thumb) - $wmH - 10;
imagecopy($thumb, $wm, $dstX, $dstY, 0, 0, $wmW, $wmH);
}
imagejpeg($thumb, $thumbPath, 85);
$thumbs[$prefix] = $dateDir . '/' . $prefix . '_' . $filename;
imagedestroy($thumb);
}
imagedestroy($srcImg);
}
}
// 9. 入庫(略)
// $db->insert('attachments', ['path' => $dateDir.'/'.$filename, 'thumbs' => json_encode($thumbs), ...]);
// 10. 返回結果
echo json_encode([
'code' => 0,
'data' => [
'url' => '/uploads/' . $dateDir . '/' . $filename,
'thumbs' => $thumbs,
]
]);
finfo 讀取文件頭,防止偽造 $_FILES['type']。bin2hex(random_bytes(8)) 避免覆蓋、遍歷、執行腳本。location /uploads { php_flag engine off; }。upload_max_filesize + 代碼邏輯 maxSize。超過 100MB 文件建議採用前端分片(如 Uploader.js、WebUploader),後端合併:
// 合併邏輯
$chunkDir = $targetDir . '/chunks_' . $fileHash;
$finalPath = $targetDir . '/' . $filename;
$fp = fopen($finalPath, 'wb');
for ($i = 0; $i < $totalChunks; $i++) {
fwrite($fp, file_get_contents("{$chunkDir}/{$i}"));
}
fclose($fp);
// 校驗 MD5/SHA1
if (hash_file('sha1', $finalPath) !== $fileHash) unlink($finalPath);