萬盛學電腦網

您现在的位置: 萬盛學電腦網 >> 程式設計 >  >> 網路程式設計 >> PHP 程式設計

PHP 遞迴刪除目錄與所有檔案的完整實作

在 PHP 中刪除非空目錄是常見需求。由於 rmdir() 僅能刪除空目錄,我們必須結合 opendir()、readdir() 遞迴遍歷並刪除所有檔案與子目錄。本文提供生產環境可用的完整實作,包含錯誤處理、符號連結防護與效能優化。

核心邏輯流程

  1. 開啟目錄控制代碼(opendir())
  2. 迴圈讀取每個項目(readdir()),略過 . 和 ..
  3. 組合完整路徑
  4. 若為檔案 → unlink() 刪除
  5. 若為目錄 → 遞迴呼叫自身刪除子目錄
  6. 關閉目錄控制代碼(closedir())
  7. 最後刪除當前目錄(rmdir())

基礎版實作

function deldir(string $dir): bool {
    // 確保路徑以目錄分隔符結尾
    $dir = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;

    if (!is_dir($dir)) {
        return false;
    }

    $dh = opendir($dir);
    if ($dh === false) {
        return false; // 無法開啟目錄(權限不足等)
    }

    while (($file = readdir($dh)) !== false) {
        if ($file === '.' || $file === '..') {
            continue;
        }

        $fullPath = $dir . $file;

        if (is_dir($fullPath)) {
            // 遞迴刪除子目錄
            if (!deldir($fullPath)) {
                closedir($dh);
                return false;
            }
        } else {
            // 刪除檔案
            if (!@unlink($fullPath)) {
                closedir($dh);
                return false;
            }
        }
    }

    closedir($dh);
    return rmdir($dir);
}

生產環境增強版

增加符號連結檢測、例外處理、權限嘗試修復、進度回呼:

class DirectoryCleaner {
    private int $deletedFiles = 0;
    private int $deletedDirs = 0;
    private array $errors = [];

    public function delete(string $dir, ?callable $progressCallback = null): bool {
        $this->resetCounters();
        $result = $this->deleteRecursive($dir, $progressCallback);
        return $result && empty($this->errors);
    }

    private function deleteRecursive(string $dir, ?callable $progressCallback): bool {
        $dir = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;

        if (!is_dir($dir)) {
            $this->errors[] = "路徑不是目錄或不存在:{$dir}";
            return false;
        }

        // 檢查是否為符號連結,避免刪除連結目標
        if (is_link($dir)) {
            $this->errors[] = "跳過符號連結:{$dir}";
            return false;
        }

        $dh = @opendir($dir);
        if ($dh === false) {
            $this->errors[] = "無法開啟目錄(權限不足):{$dir}";
            return false;
        }

        while (($file = readdir($dh)) !== false) {
            if ($file === '.' || $file === '..') continue;

            $fullPath = $dir . $file;

            // 處理符號連結
            if (is_link($fullPath)) {
                if (!@unlink($fullPath)) {
                    $this->errors[] = "無法刪除符號連結:{$fullPath}";
                } else {
                    $this->deletedFiles++;
                    $progressCallback?.($fullPath, 'symlink');
                }
                continue;
            }

            if (is_dir($fullPath)) {
                if (!$this->deleteRecursive($fullPath, $progressCallback)) {
                    closedir($dh);
                    return false;
                }
                $this->deletedDirs++;
                $progressCallback?.($fullPath, 'dir');
            } else {
                // 嘗試修復唯讀檔案權限
                if (!is_writable($fullPath)) {
                    @chmod($fullPath, 0644);
                }
                if (!@unlink($fullPath)) {
                    $this->errors[] = "無法刪除檔案:{$fullPath}";
                    closedir($dh);
                    return false;
                }
                $this->deletedFiles++;
                $progressCallback?.($fullPath, 'file');
            }
        }

        closedir($dh);

        // 刪除當前目錄
        if (!@rmdir($dir)) {
            // 嘗試修復目錄權限
            @chmod($dir, 0755);
            if (!@rmdir($dir)) {
                $this->errors[] = "無法刪除目錄:{$dir}";
                return false;
            }
        }
        $this->deletedDirs++;
        $progressCallback?.($dir, 'dir');
        return true;
    }

    public function getStats(): array {
        return [
            'deleted_files' => $this->deletedFiles,
            'deleted_dirs' => $this->deletedDirs,
            'errors' => $this->errors
        ];
    }

    private function resetCounters(): void {
        $this->deletedFiles = 0;
        $this->deletedDirs = 0;
        $this->errors = [];
    }
}

// 使用範例
$cleaner = new DirectoryCleaner();
$success = $cleaner->delete('/path/to/target', function($path, $type) {
    echo "已刪除 {$type}:{$path}
"; }); if ($success) { $stats = $cleaner->getStats(); echo "完成!刪除檔案:{$stats['deleted_files']},刪除目錄:{$stats['deleted_dirs']}"; } else { echo "刪除過程發生錯誤:
"; foreach ($cleaner->getStats()['errors'] as $err) { echo "- {$err}
"; } }

使用 SPL 迭代器的現代寫法(PHP 5.3+)

RecursiveDirectoryIterator + RecursiveIteratorIterator 提供更優雅的遞迴遍歷:

function deleteDirectoryModern(string $dir): bool {
    if (!is_dir($dir)) return false;

    $files = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
        RecursiveIteratorIterator::CHILD_FIRST // 先處理子項目,再處理父目錄
    );

    foreach ($files as $file) {
        $path = $file->getPathname();
        if ($file->isDir()) {
            if (!@rmdir($path)) return false;
        } else {
            if (!@unlink($path)) return false;
        }
    }

    return @rmdir($dir);
}

效能比較與選擇建議

方法優點缺點適用場景
原生遞迴函數 相容性最好(PHP 4+),記憶體效率高,可精細控制 程式碼較長 舊專案、嵌入式環境、需精細錯誤處理
SPL 迭代器 程式碼簡潔、可讀性高、內建 CHILD_FIRST 排序 建立迭代器物件有額外開銷,大量檔案時記憶體佔用較高 現代 PHP 專案(5.3+)、中小型目錄
系統指令 exec('rm -rf') 速度最快,支援所有檔案系統特性 安全風險極高(命令注入)、跨平台相容性差、需 shell 執行權限 極不推薦用於 Web 應用

重要安全提醒

  • 路徑驗證:務必使用 realpath() 解析真實路徑,防止目錄遍歷攻擊(../../etc)
  • 權限最小化:Web 使用者僅擁有必要目錄的寫入權限
  • 避免刪除系統關鍵目錄:加入白名單/黑名單機制
  • 大量檔案處理:超過萬個檔案時考慮分批處理或使用背景任務,避免逾時
  • 檔案鎖定:Windows 下檔案被佔用無法刪除,需重試機制

總結

PHP 刪除非空目錄的核心在於「深度優先遞迴刪除」。基礎版適合簡單場景,增強版提供生產級的錯誤處理與監控,SPL 版則是現代專案的首選。無論採用哪種方式,路徑驗證與權限控制都是安全的前提。

copyright © 萬盛學電腦網 all rights reserved