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

PHP 遠程文件下載類:支援斷點續傳、大文件、目錄自建、URL 驗證

     編輯:PHP 相關知識

類功能特性

  • 支援 HTTP/HTTPS/FTP 協議
  • 大文件分塊寫入,不佔用過多內存
  • 自動創建多級保存目錄
  • 可選強制重新下載或跳過已存在文件
  • URL 格式驗證、下載進度回調
  • 詳細錯誤信息(連接失敗、寫入失敗、URL 非法等)

完整類代碼

<?php
/**
 * RemoteDownloader - 遠程文件下載器
 */
class RemoteDownloader {
    private string $url = '';
    private string $savePath = './downloads/';
    private string $fileName = '';
    private bool $forceRedownload = false;
    private string $error = '';
    private ?callable $progressCallback = null;

    /** 設置下載 URL */
    public function setUrl(string $url): self {
        $this->url = $url;
        return $this;
    }

    /** 設置保存目錄 */
    public function setSavePath(string $path): self {
        $this->savePath = rtrim($path, '/') . '/';
        return $this;
    }

    /** 設置保存文件名(留空則自動從 URL 推斷) */
    public function setFileName(string $name): self {
        $this->fileName = $name;
        return $this;
    }

    /** 是否強制重新下載 */
    public function setForceRedownload(bool $force): self {
        $this->forceRedownload = $force;
        return $this;
    }

    /** 設置進度回調 function(int $downloaded, int $total): void */
    public function setProgressCallback(callable $cb): self {
        $this->progressCallback = $cb;
        return $this;
    }

    /** 執行下載 */
    public function download(): bool {
        if (!$this->validateUrl()) return false;
        if (!$this->prepareSavePath()) return false;

        $localFile = $this->savePath . $this->getFinalFileName();

        // 檢查文件是否已存在
        if (file_exists($localFile) && !$this->forceRedownload) {
            $this->error = "文件已存在:{$localFile},啟用 forceRedownload 可覆蓋";
            return false;
        }

        // 打開遠程流
        $remote = @fopen($this->url, 'rb');
        if (!$remote) {
            $this->error = "無法打開遠程文件:{$this->url}";
            return false;
        }

        // 獲取文件總大小(用於進度)
        $meta = stream_get_meta_data($remote);
        $totalSize = 0;
        foreach ($meta['wrapper_data'] as $header) {
            if (stripos($header, 'Content-Length:') === 0) {
                $totalSize = (int)trim(substr($header, 15));
                break;
            }
        }

        // 打開本地文件
        $local = @fopen($localFile, 'wb');
        if (!$local) {
            fclose($remote);
            $this->error = "無法創建本地文件:{$localFile}";
            return false;
        }

        // 分塊下載
        $downloaded = 0;
        $bufferSize = 8192;
        while (!feof($remote)) {
            $chunk = fread($remote, $bufferSize);
            if ($chunk === false) break;
            if (fwrite($local, $chunk) === false) {
                $this->error = '寫入本地文件失敗';
                fclose($remote);
                fclose($local);
                @unlink($localFile);
                return false;
            }
            $downloaded += strlen($chunk);
            if ($this->progressCallback) {
                $this->progressCallback($downloaded, $totalSize);
            }
        }

        fclose($remote);
        fclose($local);

        if ($downloaded === 0) {
            @unlink($localFile);
            $this->error = '下載內容為空';
            return false;
        }

        return true;
    }

    /** 獲取最後錯誤 */
    public function getError(): string {
        return $this->error;
    }

    /** 獲取最終文件名 */
    public function getFinalFileName(): string {
        if ($this->fileName) return $this->fileName;
        $parsed = parse_url($this->url);
        $basename = basename($parsed['path'] ?? '');
        return $basename ?: 'download_' . time() . '.bin';
    }

    /** URL 合法性檢查 */
    private function validateUrl(): bool {
        if ($this->url === '') {
            $this->error = 'URL 不能為空';
            return false;
        }
        if (!filter_var($this->url, FILTER_VALIDATE_URL)) {
            $this->error = 'URL 格式非法:' . $this->url;
            return false;
        }
        $scheme = parse_url($this->url, PHP_URL_SCHEME);
        if (!in_array($scheme, ['http', 'https', 'ftp'])) {
            $this->error = '不支持的協議:' . $scheme;
            return false;
        }
        return true;
    }

    /** 創建保存目錄 */
    private function prepareSavePath(): bool {
        if (!is_dir($this->savePath)) {
            if (!mkdir($this->savePath, 0755, true)) {
                $this->error = '無法創建保存目錄:' . $this->savePath;
                return false;
            }
        }
        return true;
    }
}
?>

使用示例

基本下載

$downloader = new RemoteDownloader();
$downloader->setUrl('https://example.com/large-file.zip')
           ->setSavePath('/var/www/files/')
           ->setFileName('my-file.zip');

if ($downloader->download()) {
    echo "下載成功:{$downloader->getFinalFileName()}";
} else {
    echo "失敗:{$downloader->getError()}";
}

帶進度條(CLI 環境)

$downloader = new RemoteDownloader();
$downloader->setUrl('https://speed.hetzner.de/100MB.bin')
           ->setSavePath('/tmp/')
           ->setProgressCallback(function($done, $total) {
               static $lastPct = -1;
               if ($total > 0) {
                   $pct = (int)($done / $total * 100);
                   if ($pct !== $lastPct) {
                       echo "\r進度:{$pct}% (" . formatBytes($done) . " / " . formatBytes($total) . ")";
                       $lastPct = $pct;
                   }
               } else {
                   echo "\r已下載:" . formatBytes($done);
               }
           });

if ($downloader->download()) {
    echo "\n完成!\n";
} else {
    echo "\n失敗:{$downloader->getError()}\n";
}

function formatBytes($bytes) {
    $units = ['B','KB','MB','GB'];
    $i = 0;
    while ($bytes >= 1024 && $i < 3) { $bytes /= 1024; $i++; }
    return round($bytes, 2) . ' ' . $units[$i];
}

Web 環境異步下載(AJAX 輪詢進度)

<!-- 前端 -->
<button id="startBtn">開始下載</button>
<div id="progress" style="display:none">
    <progress id="bar" max="100" value="0"></progress>
    <span id="pct">0%</span>
</div>

<script>
document.getElementById('startBtn').onclick = async () => {
    const btn = document.getElementById('startBtn');
    btn.disabled = true;
    const res = await fetch('download_async.php', {
        method: 'POST',
        headers: {'Content-Type':'application/json'},
        body: JSON.stringify({url: 'https://example.com/big.zip'})
    });
    const {task_id} = await res.json();
    document.getElementById('progress').style.display = 'block';
    pollProgress(task_id);
};

function pollProgress(taskId) {
    fetch('download_status.php?task_id=' + taskId)
        .then(r => r.json())
        .then(data => {
            document.getElementById('bar').value = data.pct;
            document.getElementById('pct').textContent = data.pct + '%';
            if (data.status === 'running') setTimeout(() => pollProgress(taskId), 1000);
            else if (data.status === 'done') alert('下載完成:' + data.file);
            else alert('失敗:' + data.error);
        });
}
</script>

斷點續傳擴展思路

HTTP 範圍請求 (Range: bytes=START-) 實現續傳:

// 在 download() 中,若本地文件已存在部分內容
$localSize = file_exists($localFile) ? filesize($localFile) : 0;
if ($localSize > 0 && !$this->forceRedownload) {
    // 請求剩餘部分
    $context = stream_context_create([
        'http' => ['header' => "Range: bytes={$localSize}-\r\n"]
    ]);
    $remote = fopen($this->url, 'rb', false, $context);
    // 打開本地文件追加模式 'ab'
    $local = fopen($localFile, 'ab');
    // ... 後續邏輯同樣
}

安全與性能建議

  1. 超時控制:設置 stream_context_create(['http'=>['timeout'=>30]]) 避免掛起。
  2. 大小限制:下載前檢查 Content-Length,超過閾值拒絕或需確認。
  3. 校驗完整性:下載後驗證 MD5/SHA256(若源提供)。
  4. 臨時文件:下載到 .tmp 文件,成功後 rename() 覆蓋目標,防止部分文件被誤用。
  5. 並發限制:同一時間限制下載任務數,防止帶寬耗盡。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved