從遠端 URL 下載檔案儲存至本地伺服器是常見需求(圖片搬遷、資料備份、爬蟲存檔)。PHP 提供三種主要方式:cURL(功能最全)、Stream Context(無擴展依賴)、fopen/fread(最基礎)。本文完整比較三種寫法、錯誤處理、大檔案支援與安全防護。
<?php
/**
* 遠程下載檔案到本地
* @param string $url 遠端檔案 URL
* @param string $savePath 本地儲存路徑(含檔名),空則自動取 URL 基名
* @param int $timeout 連線/總超時秒數
* @param array $options 額外 cURL 選項
* @return string|false 成功回傳儲存路徑,失敗回傳 false
*/
function downloadFile($url, $savePath = '', $timeout = 60, $options = []) {
// 1. 決定儲存路徑
if (!$savePath) {
$basename = basename(parse_url($url, PHP_URL_PATH));
$savePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $basename;
}
// 2. 確保目錄存在
$dir = dirname($savePath);
if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
error_log("建立目錄失敗: $dir");
return false;
}
// 3. URL 編碼空格等特殊字元
$url = str_replace(' ', '%20', $url);
// 4. 嘗試 cURL(功能最完整)
if (function_exists('curl_init')) {
$ch = curl_init();
$defaultOpts = [
CURLOPT_URL => $url,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => min(10, $timeout),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_USERAGENT => 'PHP-Downloader/1.0',
CURLOPT_SSL_VERIFYPEER => true, // 生產環境建議 true
CURLOPT_SSL_VERIFYHOST => 2,
];
curl_setopt_array($ch, array_merge($defaultOpts, $options));
$content = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($errno === 0 && $content !== false && $httpCode === 200) {
if (file_put_contents($savePath, $content) !== false) {
return $savePath;
}
error_log("寫入檔案失敗: $savePath");
} else {
error_log("cURL 下載失敗 [$errno]: $error, HTTP: $httpCode, URL: $url");
}
}
// 5. 兜底:Stream Context(不需 cURL 擴展)
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => $timeout,
'header' => 'User-Agent: PHP-Downloader/1.0',
'follow_location' => 1,
'max_redirects' => 5,
],
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
]
]);
if (@copy($url, $savePath, $ctx)) {
return $savePath;
}
error_log("Stream 下載失敗: $url");
return false;
}
// 使用範例
$path = downloadFile('https://example.com/image.jpg', '/var/www/uploads/img.jpg');
if ($path) echo "下載成功: $path";
else echo "下載失敗";
?>
<?php
function downloadLargeFile($url, $savePath, $chunkSize = 8192, $timeout = 300) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_TIMEOUT => $timeout,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_USERAGENT => 'PHP-Downloader/1.0',
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_WRITEFUNCTION => function($ch, $data) use ($savePath, $chunkSize) {
$fp = fopen($savePath, 'ab'); // 追加模式
$written = fwrite($fp, $data);
fclose($fp);
return $written; // 必須回傳寫入位元組數
},
]);
$result = curl_exec($ch);
$errno = curl_errno($ch);
curl_close($ch);
return $errno === 0 && $result !== false;
}
// 使用
downloadLargeFile('https://speedtest.tele2.net/100MB.zip', '/data/100MB.zip');
?>
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>遠程下載工具</title></head>
<body>
<h2>遠程檔案下載到伺服器</h2>
<form method="post">
<input type="url" name="url" size="80" placeholder="https://example.com/file.zip" required>
<input type="text" name="save_as" size="40" placeholder="儲存檔名 (選填)">
<button type="submit">開始下載</button>
</form>
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
set_time_limit(3600); // 1小時超時
$url = $_POST['url'] ?? '';
$saveAs = $_POST['save_as'] ?? '';
$destDir = __DIR__ . '/downloads/';
if (!is_dir($destDir)) mkdir($destDir, 0755, true);
$savePath = $destDir . ($saveAs ?: basename(parse_url($url, PHP_URL_PATH)));
echo "<p>開始下載: $url</p>";
$start = microtime(true);
$ok = downloadFile($url, $savePath, 3600);
$elapsed = microtime(true) - $start;
if ($ok) {
$size = filesize($savePath);
echo "<p style='color:green'>成功!儲存至: $savePath (<strong>$size</strong> bytes, 耗時: <strong>{$elapsed}s</strong>)</p>";
} else {
echo "<p style='color:red'>下載失敗,請檢查日誌</p>";
}
}
?>
</body></html>
| 需求 | 實作方式 |
|---|---|
| 斷點續傳 | cURL CURLOPT_RANGE + 檢查本地檔案大小 + CURLOPT_RESUME_FROM |
| 並發下載多檔 | curl_multi_* 非阻塞多路複用 |
| 驗證檔案完整性 | 下載後 hash_file('sha256', $path) 比對預期雜湊值 |
| 限速下載 | CURLOPT_MAX_RECV_SPEED_LARGE (cURL 7.15.5+) |
| 代理下載 | CURLOPT_PROXY, CURLOPT_PROXYUSERPWD |
| Basic Auth | CURLOPT_USERPWD => 'user:pass' |
http/https,封鎖 file://, ftp://, gopher://, dict://。finfo_file() 檢測 MIME,拒絕執行檔。../ 目錄遍歷,檔名隨機化 bin2hex(random_bytes(16))。CURLOPT_MAXFILESIZE 或 Stream max_file_size 參數。CURLOPT_SSL_VERIFYPEER => true,必要時指定 CURLOPT_CAINFO。<?php
function isPrivateIp($ip) {
$privateRanges = [
['10.0.0.0', '10.255.255.255'],
['172.16.0.0', '172.31.255.255'],
['192.168.0.0', '192.168.255.255'],
['127.0.0.0', '127.255.255.255'],
['169.254.0.0', '169.254.255.255'],
];
$long = ip2long($ip);
foreach ($privateRanges as [$start, $end]) {
if ($long >= ip2long($start) && $long <= ip2long($end)) return true;
}
return false;
}
function safeDownload($url, $savePath) {
$host = parse_url($url, PHP_URL_HOST);
if (!$host) return false;
$ips = gethostbynamel($host);
if (!$ips) return false;
foreach ($ips as $ip) {
if (isPrivateIp($ip)) {
error_log("SSRF 阻擋: $url -> $ip");
return false;
}
}
return downloadFile($url, $savePath);
}
?>
PHP 遠程下載檔案:首選 cURL(功能完整、效能佳),兜底 Stream Context(無擴展依賴)。核心函式 downloadFile() 處理路徑建立、超時、重導向、錯誤記錄。大檔案用 CURLOPT_WRITEFUNCTION 分塊寫入避免 OOM。生產環境必須實作 SSRF 防護、檔案型別驗證、大小限制、HTTPS 驗證,避免成為攻擊跳板。