將外部圖片下載至本地伺服器是常見需求(內容遷移、防盜鏈、CDN 回源)。本文提供健壯、安全、高效的實作模式。
<?php
/**
* 下載遠程圖片並儲存至本地
*
* @param string $url 遠程圖片 URL
* @param string $saveDir 本地儲存目錄(結尾需帶 /)
* @param int $timeout 連線/讀取逾時秒數
* @return string|false 成功回傳相對儲存路徑,失敗回傳 false
*/
function downloadRemoteImage(string $url, string $saveDir = 'uploads/', int $timeout = 30) {
// 1. 基礎驗證
if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
trigger_error("無效 URL: $url", E_USER_WARNING);
return false;
}
// 2. 確保儲存目錄存在
if (!is_dir($saveDir)) {
if (!mkdir($saveDir, 0755, true) && !is_dir($saveDir)) {
trigger_error("無法建立目錄: $saveDir", E_USER_ERROR);
return false;
}
}
// 3. 產生唯一檔名(保留副檔名)
$ext = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
$ext = $ext ? ".$ext" : '.jpg'; // 預設副檔名
$filename = date('Y-m') . '/' . uniqid('', true) . $ext;
$filepath = $saveDir . $filename;
// 4. 建立子目錄(按年月分目錄)
$subDir = dirname($filepath);
if (!is_dir($subDir)) {
mkdir($subDir, 0755, true);
}
// 5. 使用串流上下文設定逾時、User-Agent、SSL 驗證
$context = stream_context_create([
'http' => [
'timeout' => $timeout,
'user_agent' => 'Mozilla/5.0 (compatible; PHP Image Downloader)',
'follow_location' => 1,
'max_redirects' => 5,
],
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
]
]);
// 6. 串流下載(記憶體友善,避免大檔案 OOM)
$readStream = @fopen($url, 'rb', false, $context);
if (!$readStream) {
trigger_error("無法開啟遠程資源: $url", E_USER_WARNING);
return false;
}
$writeStream = @fopen($filepath, 'wb');
if (!$writeStream) {
fclose($readStream);
trigger_error("無法開啟本地檔案寫入: $filepath", E_USER_WARNING);
return false;
}
// 7. 分塊複製(8KB 緩衝)
$copied = stream_copy_to_stream($readStream, $writeStream, 8192);
fclose($readStream);
fclose($writeStream);
if ($copied === false || $copied === 0) {
@unlink($filepath); // 清理空檔
trigger_error("下載失敗或檔案為空: $url", E_USER_WARNING);
return false;
}
// 8. 可選:驗證圖片有效性
$info = @getimagesize($filepath);
if (!$info) {
@unlink($filepath);
trigger_error("下載內容非有效圖片: $url", E_USER_WARNING);
return false;
}
return $filename; // 回傳相對路徑,便於資料庫儲存
}
?>
<?php
$urls = [
'https://example.com/photo.jpg',
'https://cdn.site.com/avatar.png',
];
foreach ($urls as $url) {
$path = downloadRemoteImage($url, 'storage/app/public/images/');
if ($path) {
echo "✅ 成功: $path\n";
// 入庫: INSERT INTO images (path) VALUES ('$path')
} else {
echo "❌ 失敗: $url\n";
}
}
?>
| 維度 | 建議 |
|---|---|
| 併發效能 | 採用 Guzzle + Promise 或 Swoole/Workerman 非阻塞批量下載 |
| 去重 | 以 URL MD5 或感知雜湊 判斷是否已下載,避免重複佔用儲存 |
| 防盜鏈繞過 | 設定 Referer、Cookie 模擬瀏覽器;注意法律與道德風險 |
| 儲存架構 | 對接 Flysystem 統一介面,支援本地、S3、OSS、FTP 等多種儲存後端 |
| 圖片處理 | 下載後管線接入 Intervention Image 或 ImageMagick 壓縮、裁剪、水印、WebP 轉換 |
| 安全掃描 | 整合 ClamAV 或雲端內容審核 API 檢測惡意內容 |
| 監控告警 | 記錄下載成功率、耗時、檔案大小分佈,異常時告警 |
file_get_contents 下載大檔案:會將整個檔案載入記憶體,導致 OOM。必須使用串流 fopen + stream_copy_to_stream。verify_peer => false 會遭受中間人攻擊,生產環境嚴禁。getimagesize() 或 finfo 驗證實際檔案類型。uniqid()、hash() 或 UUID 確保唯一性。<?php
// composer require league/flysystem league/flysystem-aws-s3-v3 guzzlehttp/guzzle
use League\Flysystem\Filesystem;
use League\Flysystem\Adapter\Local;
use GuzzleHttp\Client;
$adapter = new Local('storage/app/public');
$fs = new Filesystem($adapter);
$http = new Client(['timeout' => 30]);
function downloadToFlysystem(string $url, Filesystem $fs, Client $http, string $prefix = 'images/') {
$response = $http->get($url, ['stream' => true]);
if ($response->getStatusCode() !== 200) return false;
$ext = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION) ?: 'jpg';
$filename = $prefix . date('Y-m/') . uniqid() . ".$ext";
$stream = fopen('php://temp', 'r+');
foreach ($response->getBody() as $chunk) {
fwrite($stream, $chunk);
}
rewind($stream);
return $fs->writeStream($filename, $stream) ? $filename : false;
}
?>
遠程圖片下載看似簡單,生產環境需兼顧記憶體效率、錯誤重試、安全驗證、儲存抽象、後續處理管線。建議封裝為可複用的 Service 類,對接 Flysystem 與非阻塞 HTTP 客戶端,構建可擴展的媒體資產管理體系。