<?php
/**
* 依數字 ID 產生固定深度路徑
* 例:id=122333 → /000/000/122/122333.jpg
* @param int $id
* @param string $ext
* @param int $depth 目錄層數
* @param int $width 每層位數
* @return string 相對路徑
*/
function pathById(int $id, string $ext = 'jpg', int $depth = 4, int $width = 3): string {
$padded = str_pad((string)$id, $depth * $width, '0', STR_PAD_LEFT);
$parts = str_split($padded, $width);
$parts[] = $id . '.' . $ext; // 檔名用原始 ID
return '/' . implode('/', $parts);
}
// 測試
echo pathById(122333); // /000/000/122/122333.jpg
echo pathById(1); // /000/000/000/001.jpg
echo pathById(1234567890); // /123/456/789/1234567890.jpg
?>
<?php
/**
* 依 MD5 雜湊前綴分桶
* 例:id=122333 → md5=9c7cc2cd... → /9c/7c/c2/cd/122333.jpg
* @param string|int $seed 種子 (ID、檔名、UUID)
* @param string $ext
* @param int $depth
* @param int $width
* @return string
*/
function pathByHash($seed, string $ext = 'jpg', int $depth = 4, int $width = 2): string {
$hash = md5((string)$seed);
$parts = [];
for ($i = 0; $i < $depth; $i++) {
$parts[] = substr($hash, $i * $width, $width);
}
$parts[] = $seed . '.' . $ext;
return '/' . implode('/', $parts);
}
// 測試
echo pathByHash(122333); // /9c/7c/c2/cd/122333.jpg
echo pathByHash('user_avatar_1'); // /3f/8e/.../user_avatar_1.jpg
echo pathByHash('550e8400-e29b-41d4-a716-446655440000'); // UUID
?>
<?php
/**
* 依上傳時間建立日期目錄
* 例:2024-12-25 14:30 → /2024/12/25/14/abc123.jpg
* @param DateTimeInterface|null $dt
* @param string $ext
* @param bool $withHour 是否包含小時目錄
* @return string
*/
function pathByDate(?DateTimeInterface $dt = null, string $ext = 'jpg', bool $withHour = false): string {
$dt = $dt ?? new DateTime();
$parts = [
$dt->format('Y'),
$dt->format('m'),
$dt->format('d'),
];
if ($withHour) $parts[] = $dt->format('H');
// 檔名:時間戳 + 隨機避免衝突
$filename = $dt->format('His') . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$parts[] = $filename;
return '/' . implode('/', $parts);
}
// 測試
echo pathByDate(); // /2024/12/25/143052_a1b2c3d4.jpg
echo pathByDate(null, 'log', true); // /2024/12/25/14/143052_a1b2c3d4.log
?>
<?php
class UploadPathResolver {
public function resolve(string $originalName, ?int $userId = null): string {
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
$ext = $ext ?: 'bin';
$safeExt = in_array($ext, ['jpg','jpeg','png','gif','webp','pdf','doc','docx','xls','xlsx','zip','mp4']) ? $ext : 'bin';
// 核心路徑:雜湊分桶 (均勻、不可預測)
$seed = $userId ? "user_{$userId}_" . microtime(true) : microtime(true) . '_' . random_bytes(8);
$hashPath = $this->hashPath($seed, 3, 2); // 3層 x 2字元 = 256x256x256 = 16M 桶
// 可選:前綴日期 (便於按月清理)
$datePrefix = (new DateTime())->format('Y/m');
// 最終檔名:UUID v7 (時間有序) 或 nanoid
$filename = $this->generateFilename($safeExt);
return "/uploads/{$datePrefix}{$hashPath}/{$filename}";
}
private function hashPath(string $seed, int $depth, int $width): string {
$hash = md5($seed);
$parts = [];
for ($i = 0; $i < $depth; $i++) {
$parts[] = substr($hash, $i * $width, $width);
}
return '/' . implode('/', $parts);
}
private function generateFilename(string $ext): string {
// 使用 UUID v7 (需 ramsey/uuid ^4.7) 或簡易 nanoid
if (class_exists('Ramsey\Uuid\Uuid')) {
return \Ramsey\Uuid\Uuid::uuid7()->toString() . '.' . $ext;
}
// 簡易替代:時間戳(毫秒) + 6字元隨機
return (string)(microtime(true) * 1000) . '_' . bin2hex(random_bytes(3)) . '.' . $ext;
}
}
// 使用
$resolver = new UploadPathResolver();
$path = $resolver->resolve('photo.jpg', 12345);
// /uploads/2024/12/a1/b2/c3/1703500000123_a1b2c3.jpg
?>
<?php
function ensureDir(string $fullPath, int $mode = 0755): bool {
$dir = dirname($fullPath);
if (!is_dir($dir)) {
return @mkdir($dir, $mode, true) && @chmod($dir, $mode);
}
return true;
}
// 上傳處理流程
$relativePath = $resolver->resolve($_FILES['file']['name'], $currentUserId);
$absolutePath = $_SERVER['DOCUMENT_ROOT'] . $relativePath;
if (ensureDir($absolutePath)) {
if (move_uploaded_file($_FILES['file']['tmp_name'], $absolutePath)) {
// 記錄資料庫
$stmt = $pdo->prepare('INSERT INTO attachments (user_id, path, original_name, size, mime) VALUES (?,?,?,?,?)');
$stmt->execute([$currentUserId, $relativePath, $_FILES['file']['name'], $_FILES['file']['size'], $_FILES['file']['type']]);
echo json_encode(['success' => true, 'path' => $relativePath]);
} else {
http_response_code(500);
echo json_encode(['error' => '移動檔案失敗']);
}
} else {
http_response_code(500);
echo json_encode(['error' => '建立目錄失敗']);
}
?>
| 策略 | 均勻度 | 可預測性 | 適合場景 | 缺點 |
|---|---|---|---|---|
| 數字 ID 分級 | 低 (低 ID 集中) | 高 | 純數字 ID、簡單專案 | 熱點目錄 (低層目錄檔案多) |
| 雜湊分桶 | 極高 | 低 | 大量檔案、安全性要求高 | 路徑較長、難以按時間清理 |
| 日期目錄 | 中 (每日波動) | 高 | 日誌、備份、時間序列 | 熱點目錄 (當天目錄大) |
| 混合 (日期+雜湊) | 高 | 低 | 通用生產環境 | 路徑最長 |
選對路徑策略、落實安全措施、自動化目錄管理,讓檔案上傳系統穩健支撐業務增長。