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

PHP 大檔案上傳完整實作:分片上傳、斷點續傳、秒傳與安全防護

     編輯:PHP 相關知識

大檔案上傳需解決 PHP 限制、記憶體、逾時、網路中斷等問題。本文提供生產級分片上傳方案:前端切片、後端合併、秒傳檢查、進度追蹤。

核心挑戰與對策

挑戰對策
post_max_size / upload_max_filesize分片上傳,單片 < 10MB
max_execution_time / max_input_time分片請求獨立,後端合併異步處理
記憶體耗盡串流寫入,不載入完整檔案
網路中斷重傳斷點續傳:記錄已上傳分片
重複上傳浪費秒傳:檔案雜湊比對

後端核心實作

<?php
// config.php
define('UPLOAD_CHUNK_DIR', __DIR__ . '/storage/chunks/');
define('UPLOAD_FINAL_DIR', __DIR__ . '/storage/files/');
define('CHUNK_SIZE', 5 * 1024 * 1024); // 5MB
define('MAX_FILE_SIZE', 5 * 1024 * 1024 * 1024); // 5GB

// 確保目錄存在
foreach ([UPLOAD_CHUNK_DIR, UPLOAD_FINAL_DIR] as $dir) {
    if (!is_dir($dir)) mkdir($dir, 0755, true);
}

// upload_chunk.php - 接收單一切片
require 'config.php';

$fileId = $_POST['file_id'] ?? '';           // 前端生成的檔案唯一 ID (雜湊)
$chunkIndex = (int)($_POST['chunk_index'] ?? -1);
$totalChunks = (int)($_POST['total_chunks'] ?? 0);
$fileName = $_POST['file_name'] ?? '';
$fileHash = $_POST['file_hash'] ?? '';       // SHA256,用於秒傳

if (!$fileId || $chunkIndex < 0 || !$totalChunks) {
    http_response_code(400);
    exit(json_encode(['error' => '參數錯誤']));
}

// 秒傳檢查:合併前先查最終檔案是否存在
if ($chunkIndex === 0 && $fileHash) {
    $finalPath = UPLOAD_FINAL_DIR . $fileHash . '_' . basename($fileName);
    if (file_exists($finalPath)) {
        exit(json_encode(['skip' => true, 'path' => $finalPath, 'message' => '秒傳成功']));
    }
}

$chunkDir = UPLOAD_CHUNK_DIR . $fileId . '/';
if (!is_dir($chunkDir)) mkdir($chunkDir, 0755, true);

$chunkFile = $chunkDir . $chunkIndex;
$input = fopen('php://input', 'rb');
$output = fopen($chunkFile, 'wb');
stream_copy_to_stream($input, $output);
fclose($input);
fclose($output);

// 記錄進度 (Redis/資料庫/檔案)
$progressFile = $chunkDir . '.progress';
$progress = file_exists($progressFile) ? json_decode(file_get_contents($progressFile), true) : [];
$progress[$chunkIndex] = true;
file_put_contents($progressFile, json_encode($progress));

echo json_encode(['success' => true, 'chunk' => $chunkIndex]);
?>
<?php
// merge_chunks.php - 合併分片
require 'config.php';

$fileId = $_POST['file_id'] ?? '';
$fileName = $_POST['file_name'] ?? '';
$fileHash = $_POST['file_hash'] ?? '';
$totalChunks = (int)($_POST['total_chunks'] ?? 0);

if (!$fileId || !$totalChunks) {
    http_response_code(400);
    exit(json_encode(['error' => '參數錯誤']));
}

$chunkDir = UPLOAD_CHUNK_DIR . $fileId . '/';
$finalName = ($fileHash ? $fileHash . '_' : '') . basename($fileName);
$finalPath = UPLOAD_FINAL_DIR . $finalName;

// 驗證所有分片存在
for ($i = 0; $i < $totalChunks; $i++) {
    if (!file_exists($chunkDir . $i)) {
        http_response_code(400);
        exit(json_encode(['error' => "分片 $i 缺失"]));
    }
}

// 串流合併 (不載入記憶體)
$finalFp = fopen($finalPath, 'wb');
for ($i = 0; $i < $totalChunks; $i++) {
    $chunkFp = fopen($chunkDir . $i, 'rb');
    stream_copy_to_stream($chunkFp, $finalFp);
    fclose($chunkFp);
}
fclose($finalFp);

// 驗證雜湊 (秒傳依賴)
if ($fileHash) {
    $actualHash = hash_file('sha256', $finalPath);
    if ($actualHash !== $fileHash) {
        unlink($finalPath);
        http_response_code(400);
        exit(json_encode(['error' => '檔案雜湊不符,可能損壞']));
    }
}

// 清理分片
array_map('unlink', glob($chunkDir . '*'));
rmdir($chunkDir);

echo json_encode(['success' => true, 'path' => $finalPath, 'size' => filesize($finalPath)]);
?>

前端核心邏輯

<script>
// 簡化版分片上傳邏輯
class ChunkUploader {
    constructor(file, options = {}) {
        this.file = file;
        this.chunkSize = options.chunkSize || 5 * 1024 * 1024;
        this.onProgress = options.onProgress || (() => {});
        this.onComplete = options.onComplete || (() => {});
        this.onError = options.onError || (() => {});
    }

    async upload() {
        // 1. 計算檔案 SHA256 (Web Worker 避免阻塞)
        this.fileHash = await this.calculateHash();
        
        // 2. 檢查秒傳
        const instant = await this.checkInstant();
        if (instant) return this.onComplete(instant);

        // 3. 分片上傳
        this.totalChunks = Math.ceil(this.file.size / this.chunkSize);
        this.fileId = this.generateFileId();
        
        for (let i = 0; i < this.totalChunks; i++) {
            await this.uploadChunk(i);
            this.onProgress((i + 1) / this.totalChunks);
        }

        // 4. 通知後端合併
        return this.merge();
    }

    async uploadChunk(index) {
        const start = index * this.chunkSize;
        const end = Math.min(start + this.chunkSize, this.file.size);
        const chunk = this.file.slice(start, end);
        
        const formData = new FormData();
        formData.append('file_id', this.fileId);
        formData.append('chunk_index', index);
        formData.append('total_chunks', this.totalChunks);
        formData.append('file_name', this.file.name);
        formData.append('file_hash', this.fileHash);
        formData.append('chunk', chunk);

        const response = await fetch('/upload_chunk.php', {
            method: 'POST',
            body: formData
        });
        return response.json();
    }

    async merge() {
        const response = await fetch('/merge_chunks.php', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                file_id: this.fileId,
                file_name: this.file.name,
                file_hash: this.fileHash,
                total_chunks: this.totalChunks
            })
        });
        return this.onComplete(await response.json());
    }

    async calculateHash() {
        // 使用 SparkMD5 或 Web Crypto API
        const buffer = await this.file.arrayBuffer();
        const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
        return Array.from(new Uint8Array(hashBuffer))
            .map(b => b.toString(16).padStart(2, '0')).join('');
    }
}

// 使用
const uploader = new ChunkUploader(fileInput.files[0], {
    onProgress: (p) => progressBar.style.width = (p * 100) + '%',
    onComplete: (res) => console.log('上傳完成:', res),
});
uploader.upload();

安全防護清單

  • 檔案類型驗證:finfo_file() 檢查 MIME,非信任副檔名
  • 檔名安全:最終儲存用雜湊命名,原始檔名存資料庫
  • 儲存路徑隔離:上傳目錄不可直接 Web 存取,需 PHP 輸出
  • 大小限制:前後端雙重驗證
  • 速率限制:Token Bucket 限制單用戶上傳頻率
  • 掃毒整合:ClamAV 掃描合併後檔案

進階優化

  • 併發上傳:Promise.all() 並行上傳 3-5 片
  • 異步合併:合併任務推入隊列,立即回應前端
  • 分散式儲存:分片直傳 S3/OSS,後端僅協調
  • WebSocket 進度:即時推送合併進度

分片上傳是大檔案上傳的標準解法,核心在於「前端切片 + 後端串流合併 + 雜湊驗證」,配合秒傳、斷點續傳、安全掃毒可構建企業級上傳系統。

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