刪除非空目錄是常見檔案操作需求。本文提供兩種遞迴刪除實作,分析優缺點,並補充路徑驗證、權限檢查、錯誤處理等生產級安全措施。
<?php
/**
* 遞迴刪除目錄及其所有內容
*
* @param string $dir 目錄絕對路徑
* @return bool 成功返回 true
* @throws InvalidArgumentException 路徑非目錄或不存在
* @throws RuntimeException 刪除失敗
*/
function recursiveDelete(string $dir): bool {
// 1. 規範化路徑
$dir = rtrim(realpath($dir), DIRECTORY_SEPARATOR);
if ($dir === false || !is_dir($dir)) {
throw new InvalidArgumentException("Directory not found: $dir");
}
// 2. 安全檢查:防止刪除系統關鍵目錄
$protected = ["/", "/etc", "/var", "/usr", "/home", "C:\\Windows", "C:\\Program Files"];
foreach ($protected as $p) {
if (strpos($dir, realpath($p)) === 0) {
throw new RuntimeException("Protected directory: $dir");
}
}
// 3. 開啟目錄句柄
$handle = @opendir($dir);
if (!$handle) {
throw new RuntimeException("Cannot open directory: $dir");
}
// 4. 遍歷刪除
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
$path = $dir . DIRECTORY_SEPARATOR . $entry;
if (is_dir($path)) {
recursiveDelete($path); // 遞迴刪除子目錄
} else {
if (!@unlink($path)) {
throw new RuntimeException("Failed to delete file: $path");
}
}
}
closedir($handle);
// 5. 刪除空目錄本身
if (!@rmdir($dir)) {
throw new RuntimeException("Failed to remove directory: $dir");
}
return true;
}
?>
<?php
function recursiveDeleteScandir(string $dir): bool {
$dir = rtrim(realpath($dir), DIRECTORY_SEPARATOR);
if ($dir === false || !is_dir($dir)) {
throw new InvalidArgumentException("Directory not found: $dir");
}
// 同樣加入受保護目錄檢查...
$entries = scandir($dir);
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') continue;
$path = $dir . DIRECTORY_SEPARATOR . $entry;
if (is_dir($path)) {
recursiveDeleteScandir($path);
} else {
if (!@unlink($path)) {
error_log("Warning: unlink failed for $path");
// 視業務決定是否拋出異常或繼續
}
}
}
if (!@rmdir($dir)) {
throw new RuntimeException("Failed to remove directory: $dir");
}
return true;
}
?>
<?php
function recursiveDeleteIterator(string $dir): bool {
$dir = rtrim(realpath($dir), DIRECTORY_SEPARATOR);
if ($dir === false || !is_dir($dir)) {
throw new InvalidArgumentException("Directory not found: $dir");
}
// 受保護目錄檢查...
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST // 先刪子項,再刪父目錄
);
foreach ($iterator as $item) {
$path = $item->getPathname();
if ($item->isDir()) {
if (!@rmdir($path)) {
error_log("Warning: rmdir failed for $path");
}
} else {
if (!@unlink($path)) {
error_log("Warning: unlink failed for $path");
}
}
}
// 最後刪除根目錄
if (!@rmdir($dir)) {
throw new RuntimeException("Failed to remove root directory: $dir");
}
return true;
}
?>
| 特性 | opendir 遞迴 | scandir 遞迴 | RecursiveIterator |
|---|---|---|---|
| 代碼行數 | 較多 | 中等 | 最少 |
| 記憶體佔用 | 低 (流式) | 中 (scandir 回傳陣列) | 低 (迭代器) |
| 錯誤處理 | 手動控制 | 手動控制 | 統一 try-catch |
| 符號連結處理 | 需額外判斷 | 需額外判斷 | RecursiveDirectoryIterator::FOLLOW_SYMLINKS |
| 推薦場景 | 極舊版 PHP、嵌入式 | 一般專案、簡單邏輯 | 現代專案 (PHP 7.1+)、標準庫風格 |
write + execute 權限。unlink) 還是跟隨刪除目標 (rmdir/unlink 目標),預設建議不跟隨。exec('rm -rf ' . escapeshellarg($dir)) (僅 Linux、極謹慎) 或分批異步處理。<?php
try {
// 刪除上傳目錄下某用戶的臨時資料夾
$target = '/var/www/app/uploads/temp/user_12345';
recursiveDeleteIterator($target);
echo "刪除成功\n";
} catch (Exception $e) {
error_log("Delete failed: {$e->getMessage()}");
echo "刪除失敗: {$e->getMessage()}\n";
}
?>
選擇適合專案 PHP 版本與複雜度的實作,務必加上路徑驗證與權限檢查,避免「一行代碼刪光硬碟」的慘劇。