首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 檔案上傳自動分配儲存路徑:數字分級、雜湊分桶、日期目錄三大策略

     編輯:PHP 綜合知識

為什麼需要路徑分級?

  • 單目錄檔案過多導致 inode 耗盡、ls/find 變慢、檔案系統效能下降(ext4 建議單目錄 < 10 萬)。
  • 便於備份、遷移、CDN 同步、權限控制。
  • 避免檔名衝突、提升安全性(不可預測路徑)。

策略一:數字 ID 分級(適合自增 ID 為主鍵)

<?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
?>

策略二:雜湊分桶(適合 UUID、雜湊值、需均勻分布)

<?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、簡單專案熱點目錄 (低層目錄檔案多)
雜湊分桶極高低大量檔案、安全性要求高路徑較長、難以按時間清理
日期目錄中 (每日波動)高日誌、備份、時間序列熱點目錄 (當天目錄大)
混合 (日期+雜湊)高低通用生產環境路徑最長

安全補充

  • **副檔名白名單**:嚴格限制允許上傳的副檔名與 MIME。
  • **檔名去識別化**:永遠不使用用戶原始檔名,改用 UUID/雜湊/時間戳。
  • **儲存於 Web Root 外**:`/var/www/uploads` 而非 `/var/www/html/uploads`,防止直接執行腳本。
  • **Nginx 禁用腳本執行**:`location /uploads { location ~ \.php$ { deny all; } }`
  • **病毒掃描**:整合 ClamAV 掃描上傳檔案。

選對路徑策略、落實安全措施、自動化目錄管理,讓檔案上傳系統穩健支撐業務增長。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved