遞歸刪除目錄樹是文件系統操作基礎功能。本文提供健壯的實現,處理權限、符號鏈接、錯誤恢復、大目錄性能等實戰問題。
<?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 應用場景。