在無 Shell 訪問權限的虛擬主機環境中,使用 PHP 內建 ZipArchive 或舊版 zip_* 函數可實現壓縮包上傳後自動解壓,適用於模板安裝、插件更新、批量圖片部署等場景。本文提供兩種版本的完整代碼並說明權限與安全注意事項。
<?php
/**
* 解壓 ZIP 到指定目錄
*
* @param string $zipPath 壓縮包路徑
* @param string $destDir 目標目錄(會自動建立)
* @param array $options 可選:['overwrite' => true, 'password' => 'secret']
* @return array ['success' => bool, 'message' => string, 'files' => array]
*/
function unzip_archive($zipPath, $destDir, $options = []) {
if (!file_exists($zipPath)) {
return ['success' => false, 'message' => '壓縮包不存在'];
}
if (!is_dir($destDir)) {
mkdir($destDir, 0755, true);
}
$zip = new ZipArchive();
$res = $zip->open($zipPath);
if ($res !== true) {
return ['success' => false, 'message' => "無法打開壓縮包,錯誤碼: $res"];
}
// 密碼保護支援(PHP 7.2+)
if (!empty($options['password'])) {
if (!$zip->setPassword($options['password'])) {
$zip->close();
return ['success' => false, 'message' => '密碼錯誤或不支援加密格式'];
}
}
$overwrite = $options['overwrite'] ?? true;
$extracted = [];
for ($i = 0; $i < $zip->numFiles; $i++) {
$stat = $zip->statIndex($i);
$filename = $stat['name'];
// 安全過濾:防止目錄遍歷攻擊 (../../etc/passwd)
$filename = ltrim($filename, '/\\');
if (strpos($filename, '..') !== false) {
continue; // 跳過危險路徑
}
$targetPath = $destDir . DIRECTORY_SEPARATOR . $filename;
if ($stat['name'][-1] === '/') { // 目錄項
if (!is_dir($targetPath)) {
mkdir($targetPath, 0755, true);
}
continue;
}
// 確保父目錄存在
$parentDir = dirname($targetPath);
if (!is_dir($parentDir)) {
mkdir($parentDir, 0755, true);
}
// 覆蓋策略
if (file_exists($targetPath) && !$overwrite) {
continue;
}
// 解壓單個文件
$content = $zip->getFromIndex($i);
if ($content === false) {
continue;
}
file_put_contents($targetPath, $content);
$extracted[] = $filename;
}
$zip->close();
return ['success' => true, 'message' => '解壓完成', 'files' => $extracted];
}
// 使用範例
$result = unzip_archive('uploads/package.zip', 'public/themes/new-theme', [
'overwrite' => true,
// 'password' => 'secret123' // 若有加密
]);
var_dump($result);
<?php
// 僅供維護舊專案參考,新專案請用 ZipArchive
$zip = zip_open("moooredale.zip");
if ($zip) {
while ($zip_entry = zip_read($zip)) {
$name = zip_entry_name($zip_entry);
// 安全過濾同前
$fp = fopen($name, "w");
if (zip_entry_open($zip, $zip_entry, "r")) {
$buf = zip_entry_read($zip_entry, zip_entry_filesize($zip_entry));
fwrite($fp, $buf);
zip_entry_close($zip_entry);
}
fclose($fp);
}
zip_close($zip);
}
..、絕對路徑、符號鏈接,否則攻擊者可寫入任意文件(如 ../../../var/www/html/shell.php)。mime_content_type() 或 finfo 確認為 application/zip,防止偽裝的惡意腳本。upload_max_filesize、post_max_size、memory_limit,避免大包導致 OOM。max_execution_time,可用 set_time_limit(0) 或分塊異步處理。<?php
// 控制器:接收上傳 -> 存入臨時目錄 -> 推入隊列 -> 立即返回
// 隊列任務:調用 unzip_archive() -> 記錄日誌 -> 通知前端(WebSocket/輪詢)
// 適用於 >100MB 或文件數 >10k 的場景
推薦使用 Laravel Queue、Symfony Messenger 或原生 Redis 列表實現異步解壓,避免阻塞 HTTP 請求導致超時或用戶體驗差。