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

PHP 遞歸刪除非空目錄:rmdir 與 unlink 組合實現完整清理

     編輯:PHP 相關知識

核心問題:rmdir 僅能刪除空目錄

PHP 內建 rmdir() 要求目錄為空,否則回傳 false 並拋出警告。刪除包含檔案/子目錄的目錄,必須遞歸遍歷:先刪除所有檔案(unlink),再遞歸刪除子目錄,最後刪除當前目錄。

安全、健壯的遞歸刪除函數

/**
 * 遞歸刪除目錄及其內容
 *
 * @param string $dirPath      目標目錄絕對路徑
 * @param bool   $deleteSelf   是否刪除目標目錄本身(false 僅清空內容)
 * @param array  $options      進階選項
 *   - 'skip_errors'   => bool, 忽略單個檔案刪除失敗繼續執行 (預設 false)
 *   - 'follow_links'  => bool, 是否跟隨符號連結 (預設 false,避免刪除連結目標)
 *   - 'max_depth'     => int,  最大遞歸深度防無限迴圈 (預設 100)
 * @return bool  完全成功 true,失敗 false
 */
function rmdirRecursive(
    string $dirPath,
    bool $deleteSelf = true,
    array $options = []
): bool {
    $opts = array_merge([
        'skip_errors'  => false,
        'follow_links' => false,
        'max_depth'    => 100,
    ], $options);

    // 正規化路徑
    $dirPath = rtrim(realpath($dirPath), DIRECTORY_SEPARATOR);
    if ($dirPath === false || !is_dir($dirPath)) {
        trigger_error("目錄不存在: $dirPath", E_USER_WARNING);
        return false;
    }

    // 遞歸內部函數
    $success = true;
    $depth = 0;

    $fn = function ($currentDir, $currentDepth) use (&$fn, $opts, &$success) {
        if ($currentDepth > $opts['max_depth']) {
            trigger_error("超過最大遞歸深度: $currentDir", E_USER_WARNING);
            $success = false;
            return;
        }

        $handle = @opendir($currentDir);
        if (!$handle) {
            if (!$opts['skip_errors']) $success = false;
            return;
        }

        while (($entry = readdir($handle)) !== false) {
            if ($entry === '.' || $entry === '..') continue;

            $fullPath = $currentDir . DIRECTORY_SEPARATOR . $entry;

            // 符號連結處理
            if (is_link($fullPath)) {
                if (!$opts['follow_links']) {
                    // 僅刪除連結本身
                    if (!@unlink($fullPath) && !$opts['skip_errors']) $success = false;
                } else {
                    // 跟隨連結刪除目標(危險,預設關閉)
                    $fn($fullPath, $currentDepth + 1);
                    if (!@rmdir($fullPath) && !$opts['skip_errors']) $success = false;
                }
                continue;
            }

            if (is_dir($fullPath)) {
                // 遞歸處理子目錄
                $fn($fullPath, $currentDepth + 1);
                if (!@rmdir($fullPath) && !$opts['skip_errors']) $success = false;
            } elseif (is_file($fullPath)) {
                // 刪除檔案
                if (!@unlink($fullPath) && !$opts['skip_errors']) $success = false;
            }
        }
        closedir($handle);
    };

    $fn($dirPath, 0);

    // 最後刪除目標目錄本身
    if ($deleteSelf && !@rmdir($dirPath) && !$opts['skip_errors']) {
        $success = false;
    }

    return $success;
}

// 使用範例
// 1. 完整刪除
rmdirRecursive('/var/www/uploads/temp_123');

// 2. 僅清空目錄內容,保留目錄
rmdirRecursive('/var/www/cache', false);

// 3. 生產環境建議:忽略單一失敗、不跟隨連結
rmdirRecursive('/tmp/app_cache', true, ['skip_errors' => true]);

進階版:支援檔案副檔名過濾、時間條件

function rmdirSelective(
    string $dirPath,
    array $options = []
): bool {
    $opts = array_merge([
        'extensions'      => null,      // 允許刪除的副檔名陣列 ['log','tmp'],null 為全部
        'older_than'      => null,      // 秒數,僅刪除修改時間早於此秒數的檔案
        'newer_than'      => null,      // 秒數,僅刪除修改時間晚於此秒數的檔案
        'min_size'        => null,      // 位元組,最小檔案大小
        'max_size'        => null,      // 位元組,最大檔案大小
        'delete_empty_dirs' => true,    // 刪除檔案後是否清理空目錄
        'dry_run'         => false,     // 試運行,不實際刪除
    ], $options);

    $deleted = 0;
    $errors  = 0;

    $fn = function ($currentDir) use (&$fn, $opts, &$deleted, &$errors) {
        $handle = @opendir($currentDir);
        if (!$handle) return;

        while (($entry = readdir($handle)) !== false) {
            if ($entry === '.' || $entry === '..') continue;
            $fullPath = $currentDir . DIRECTORY_SEPARATOR . $entry;

            if (is_dir($fullPath)) {
                $fn($fullPath);
                if ($opts['delete_empty_dirs'] && !$opts['dry_run']) {
                    @rmdir($fullPath); // 僅刪除空目錄
                }
            } elseif (is_file($fullPath)) {
                $stat = @stat($fullPath);
                if (!$stat) { $errors++; continue; }

                // 條件過濾
                if ($opts['extensions'] !== null) {
                    $ext = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));
                    if (!in_array($ext, $opts['extensions'], true)) continue;
                }
                if ($opts['older_than'] !== null && ($stat['mtime'] > (time() - $opts['older_than']))) continue;
                if ($opts['newer_than'] !== null && ($stat['mtime'] < (time() - $opts['newer_than']))) continue;
                if ($opts['min_size'] !== null && $stat['size'] < $opts['min_size']) continue;
                if ($opts['max_size'] !== null && $stat['size'] > $opts['max_size']) continue;

                if (!$opts['dry_run'] && @unlink($fullPath)) {
                    $deleted++;
                } elseif (!$opts['dry_run']) {
                    $errors++;
                }
            }
        }
        closedir($handle);
    };

    $fn($dirPath);

    if ($opts['dry_run']) {
        echo "試運行完成:將刪除 $deleted 個檔案\n";
    } else {
        echo "實際刪除:$deleted 個檔案,錯誤:$errors 次\n";
    }
    return $errors === 0;
}

// 使用範例:清理 7 天前的 .log 檔案
rmdirSelective('/var/log/app', [
    'extensions' => ['log'],
    'older_than' => 7 * 24 * 3600,
    'delete_empty_dirs' => true
]);

常見陷阱與最佳實踐

陷阱後果解決方案
直接 rmdir($dir) 非空目錄回傳 false、警告必須先遞歸清空
未檢查符號連結誤刪連結指向的真實目錄is_link() 判斷,預設只刪連結本身
權限不足unlink/rmdir 失敗檢查 is_writable()、PHP 進程使用者權限、SELinux/AppArmor
檔案被其他進程佔用Windows 下無法刪除延遲刪佇列、重啟服務、使用 MoveFileEx 延遲刪除
路徑遍歷攻擊刪除非預期目錄使用 realpath() 正規化,限制在允許根目錄下
無限遞歸(循環連結)記憶體耗盡、死循環設定 max_depth、檢測已訪問 inode

Windows 特殊處理

  • Windows 不支援 unlink 刪除唯讀檔案,需先 chmod($file, 0666) 或用 attrib -R。
  • 目錄刪除需確認無隱藏檔(thumbs.db, desktop.ini)。
  • 長路徑(>260 字元)需啟用「啟用 Win32 長路徑」群組原則或使用 \\?\ 前綴。

刪除前驗證清單

function safeDelete(string $path, array $options = []): bool {
    // 1. 路徑正規化與安全檢查
    $realPath = realpath($path);
    if (!$realPath) return false;

    $allowedRoots = $options['allowed_roots'] ?? [sys_get_temp_dir(), '/var/www'];
    $allowed = false;
    foreach ($allowedRoots as $root) {
        if (strpos($realPath, realpath($root)) === 0) { $allowed = true; break; }
    }
    if (!$allowed) {
        trigger_error("嘗試刪除不允許路徑: $realPath", E_USER_ERROR);
        return false;
    }

    // 2. 確認是目錄
    if (!is_dir($realPath)) {
        trigger_error("非目錄: $realPath", E_USER_WARNING);
        return false;
    }

    // 3. 執行刪除
    return rmdirRecursive($realPath, true, $options);
}
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved