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

PHP 遞歸遍歷刪除目錄與文件的完整實現

     編輯:PHP 相關知識

PHP 遞歸遍歷刪除目錄與文件的完整實現

遞歸刪除目錄樹是文件系統操作基礎功能。本文提供健壯的實現,處理權限、符號鏈接、錯誤恢復、大目錄性能等實戰問題。

基礎遞歸刪除

<?php
/**
 * 遞歸刪除目錄及其所有內容
 *
 * @param string $dir 目錄路徑
 * @param bool $deleteSelf 是否刪除目錄本身(false 時僅清空內容)
 * @return bool 成功返回 true
 * @throws RuntimeException 遇到不可恢復錯誤
 */
function deleteDirectory(string $dir, bool $deleteSelf = true): bool {
    // 規範化路徑
    $dir = rtrim($dir, '/\\');
    
    if (!is_dir($dir)) {
        throw new InvalidArgumentException("Not a directory: $dir");
    }
    
    $items = scandir($dir);
    if ($items === false) {
        throw new RuntimeException("Cannot read directory: $dir");
    }
    
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') continue;
        
        $path = "$dir/$item";
        
        // 處理符號鏈接:僅刪除鏈接本身,不跟隨刪除目標
        if (is_link($path)) {
            if (!unlink($path)) {
                throw new RuntimeException("Failed to unlink symlink: $path");
            }
            continue;
        }
        
        if (is_dir($path)) {
            // 遞歸刪除子目錄
            deleteDirectory($path, true);
        } else {
            // 刪除文件
            if (!@unlink($path)) {
                // 嘗試修改權限後重試
                @chmod($path, 0666);
                if (!unlink($path)) {
                    throw new RuntimeException("Failed to delete file: $path");
                }
            }
        }
    }
    
    if ($deleteSelf) {
        if (!@rmdir($dir)) {
            @chmod($dir, 0777);
            if (!rmdir($dir)) {
                throw new RuntimeException("Failed to remove directory: $dir");
            }
        }
    }
    
    return true;
}
?>

增強版:支援進度回調、錯誤收集、大目錄優化

<?php
class DirectoryCleaner {
    public int $deletedFiles = 0;
    public int $deletedDirs = 0;
    public int $errors = 0;
    public array $errorDetails = [];
    
    private callable|null $progressCallback = null;
    private int $batchSize = 1000; // 大目錄分批處理
    
    public function __construct(
        private bool $followSymlinks = false,
        private bool $continueOnError = true
    ) {}
    
    public function setProgressCallback(callable $cb): self {
        $this->progressCallback = $cb;
        return $this;
    }
    
    public function delete(string $dir, bool $deleteSelf = true): bool {
        $this->resetCounters();
        return $this->doDelete(rtrim($dir, '/\\'), $deleteSelf);
    }
    
    private function doDelete(string $dir, bool $deleteSelf): bool {
        if (!is_dir($dir)) {
            $this->recordError("Not a directory: $dir");
            return false;
        }
        
        $handle = @opendir($dir);
        if (!$handle) {
            $this->recordError("Cannot open directory: $dir");
            return false;
        }
        
        $batch = [];
        $success = true;
        
        while (($entry = readdir($handle)) !== false) {
            if ($entry === '.' || $entry === '..') continue;
            
            $path = "$dir/$entry";
            $batch[] = $path;
            
            // 分批處理,避免內存堆積
            if (count($batch) >= $this->batchSize) {
                $success &= $this->processBatch($batch);
                $batch = [];
            }
        }
        closedir($handle);
        
        // 處理剩餘
        if ($batch) {
            $success &= $this->processBatch($batch);
        }
        
        // 刪除目錄本身
        if ($deleteSelf && $success) {
            if (!@rmdir($dir)) {
                @chmod($dir, 0777);
                if (!rmdir($dir)) {
                    $this->recordError("Failed to rmdir: $dir");
                    $success = false;
                } else {
                    $this->deletedDirs++;
                    $this->reportProgress("Deleted dir: $dir");
                }
            } else {
                $this->deletedDirs++;
                $this->reportProgress("Deleted dir: $dir");
            }
        }
        
        return $success;
    }
    
    private function processBatch(array $paths): bool {
        $success = true;
        foreach ($paths as $path) {
            if (is_link($path)) {
                if (!$this->followSymlinks) {
                    if (unlink($path)) {
                        $this->deletedFiles++;
                        $this->reportProgress("Unlinked: $path");
                    } else {
                        $this->recordError("Unlink failed: $path");
                        $success = false;
                    }
                    continue;
                }
                // followSymlinks=true 時按目標類型處理
                $target = readlink($path);
                // 遞歸處理目標(需防循環,此處簡化)
            } elseif (is_dir($path)) {
                if (!$this->doDelete($path, true)) {
                    $success = false;
                }
            } else {
                if (unlink($path)) {
                    $this->deletedFiles++;
                    $this->reportProgress("Deleted file: $path");
                } else {
                    @chmod($path, 0666);
                    if (unlink($path)) {
                        $this->deletedFiles++;
                        $this->reportProgress("Deleted file (after chmod): $path");
                    } else {
                        $this->recordError("Unlink failed: $path");
                        $success = false;
                    }
                }
            }
        }
        return $success;
    }
    
    private function recordError(string $msg): void {
        $this->errors++;
        $this->errorDetails[] = $msg;
        if (!$this->continueOnError) {
            throw new RuntimeException($msg);
        }
    }
    
    private function reportProgress(string $msg): void {
        if ($this->progressCallback) {
            ($this->progressCallback)($msg, $this->deletedFiles, $this->deletedDirs);
        }
    }
    
    private function resetCounters(): void {
        $this->deletedFiles = $this->deletedDirs = $this->errors = 0;
        $this->errorDetails = [];
    }
}

// 使用示例
$cleaner = new DirectoryCleaner(continueOnError: true);
$cleaner->setProgressCallback(function($msg, $files, $dirs) {
    echo "[$files files, $dirs dirs] $msg\n";
});

$ok = $cleaner->delete('/var/www/cache', true);
echo "完成: 文件{$cleaner->deletedFiles}, 目錄{$cleaner->deletedDirs}, 錯誤{$cleaner->errors}\n";
if ($cleaner->errors) {
    print_r($cleaner->errorDetails);
}
?>

移動/重命名目錄(跨文件系統安全)

<?php
/**
 * 移動目錄(支援跨文件系統:複製+刪除)
 */
function moveDirectory(string $src, string $dst, bool $overwrite = false): bool {
    $src = rtrim($src, '/\\');
    $dst = rtrim($dst, '/\\');
    
    if (!is_dir($src)) throw new InvalidArgumentException("Source not dir: $src");
    if (is_dir($dst)) {
        if (!$overwrite) throw new RuntimeException("Destination exists: $dst");
        deleteDirectory($dst, true);
    }
    
    // 嘗試直接重命名(同文件系統原子操作)
    if (@rename($src, $dst)) return true;
    
    // 跨文件系統:複製樹 + 刪除源
    return copyDirectoryTree($src, $dst) && deleteDirectory($src, true);
}

function copyDirectoryTree(string $src, string $dst): bool {
    @mkdir($dst, 0755, true);
    
    $handle = opendir($src);
    while (($entry = readdir($handle)) !== false) {
        if ($entry === '.' || $entry === '..') continue;
        
        $srcPath = "$src/$entry";
        $dstPath = "$dst/$entry";
        
        if (is_dir($srcPath)) {
            if (!copyDirectoryTree($srcPath, $dstPath)) return false;
        } else {
            if (!copy($srcPath, $dstPath)) return false;
        }
    }
    closedir($handle);
    return true;
}
?>

關鍵最佳實踐

問題解決方案
權限不足無法刪除刪除前 `chmod 0666/0777`,捕獲錯誤重試
符號鏈接誤刪目標`is_link()` 判斷,僅 `unlink()` 鏈接本身
大目錄遞歸棧溢出改用迭代+棧,或分批處理(scandir 分頁)
文件被進程佔用無法刪除Windows 下常見,延遲重試、記錄錯誤、稍後清理
跨文件系統移動`rename` 失敗時 fallback 到 copy+delete
併發操作衝突文件鎖、臨時目錄重命名、原子操作

系統命令替代方案(極大目錄)

<?php
// Linux/Unix: 系統命令極快,但需驗證路徑安全
function systemDelete(string $dir): bool {
    $dir = escapeshellarg($dir);
    // -f 強制 -r 遞歸
    $cmd = "rm -rf $dir 2>&1";
    $output = [];
    $returnCode = 0;
    exec($cmd, $output, $returnCode);
    return $returnCode === 0;
}

// Windows
function systemDeleteWin(string $dir): bool {
    $dir = escapeshellarg($dir);
    // /s 遞歸 /q 靜默 /f 強制
    $cmd = "rmdir /s /q $dir 2>&1";
    exec($cmd, $output, $returnCode);
    return $returnCode === 0;
}
// 注意:生產環境慎用,必須絕對路徑驗證,防止注入
?>

選擇純 PHP 實現還是系統命令,取決於目錄規模、權限模型、可移植性要求。純 PHP 可控性更強,適合大多數 Web 應用場景。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved