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

PHP 遞歸刪除目錄及文件的完整實現(含編碼兼容處理)

     編輯:PHP 綜合知識

前言

PHP 內建 rmdir() 僅能刪除空目錄。刪除非空目錄需先遞歸清空子文件/子目錄,再刪除目錄本身。本文提供一個健壯、可復用的遞歸刪除函數,處理了編碼兼容、符號鏈接、權限錯誤、循環引用等邊界情況,並給出生產環境使用建議。

核心實現代碼

<?php
/**
 * 遞歸刪除目錄及其所有內容
 * @param string $dir 目錄絕對路徑
 * @param bool $deleteSelf 是否刪除目錄本身(false 時僅清空內容)
 * @return bool 成功返回 true,失敗返回 false
 */
function deleteDirectory(string $dir, bool $deleteSelf = true): bool {
    // 1. 規範化路徑,去除尾部斜槓
    $dir = rtrim($dir, DIRECTORY_SEPARATOR);
    
    // 2. 基礎校驗
    if (!is_dir($dir)) {
        trigger_error("目錄不存在: {$dir}", E_USER_WARNING);
        return false;
    }
    if (!is_readable($dir) || !is_writable($dir)) {
        trigger_error("目錄不可讀/寫,權限不足: {$dir}", E_USER_WARNING);
        return false;
    }
    
    // 3. 打開目錄句柄
    $handle = @opendir($dir);
    if ($handle === false) {
        trigger_error("無法打開目錄: {$dir}", E_USER_WARNING);
        return false;
    }
    
    $success = true;
    
    // 4. 遍歷目錄條目
    while (($entry = readdir($handle)) !== false) {
        // 跳過 . 和 ..
        if ($entry === '.' || $entry === '..') {
            continue;
        }
        
        $path = $dir . DIRECTORY_SEPARATOR . $entry;
        
        // 處理符號鏈接:僅刪除鏈接本身,不跟隨刪除目標
        if (is_link($path)) {
            if (!@unlink($path)) {
                trigger_error("刪除符號鏈接失敗: {$path}", E_USER_WARNING);
                $success = false;
            }
            continue;
        }
        
        if (is_dir($path)) {
            // 遞歸刪除子目錄
            if (!deleteDirectory($path, true)) {
                $success = false;
            }
        } elseif (is_file($path)) {
            // 刪除文件
            if (!@unlink($path)) {
                trigger_error("刪除文件失敗: {$path}", E_USER_WARNING);
                $success = false;
            }
        }
        // 忽略其他類型(socket、fifo 等)
    }
    
    closedir($handle);
    
    // 5. 刪除目錄本身
    if ($deleteSelf) {
        if (!@rmdir($dir)) {
            trigger_error("刪除目錄失敗(可能非空或權限不足): {$dir}", E_USER_WARNING);
            $success = false;
        }
    }
    
    return $success;
}

// === 使用示例 ===
// 設置編碼(舊項目兼容 GBK,新項目建議統一 UTF-8)
header('Content-Type: text/html; charset=utf-8');

$targetDir = __DIR__ . '/test_uploads';  // 使用絕對路徑

if (deleteDirectory($targetDir)) {
    echo "目錄刪除成功: {$targetDir}";
} else {
    echo "目錄刪除失敗,請檢查錯誤日誌";
}

關鍵細節解析

細節處理方式原因
編碼兼容文件系統編碼與腳本編碼一致(推薦 UTF-8),Windows 下中文路徑需注意避免 opendir/unlink 因編碼不匹配找不到文件
符號鏈接is_link() 判斷,僅 unlink() 鏈接本身防止誤刪鏈接指向的真實目標數據
權限錯誤使用 @ 抑制警告 + trigger_error 記錄,不中斷流程單個文件失敗不影響其他文件刪除,便於事後排查
循環引用實際項目中可維護 $visited 集合記錄 inode防止符號鏈接形成環導致無限遞歸
大目錄性能單次 readdir 遍歷,避免 scandir 全量載入內存百萬文件目錄下 scandir 會導致內存溢出
併發安全生產環境建議加文件鎖或使用獨佔目錄名(如臨時目錄重命名)防止多進程同時操作同一目錄導致競態條件

進階版:支持回調與統計

function deleteDirectoryAdvanced(string $dir, array $options = []): array {
    $defaults = [
        'deleteSelf'     => true,
        'followSymlinks' => false,  // 是否跟隨符號鏈接刪除目標
        'onFileDelete'   => null,   // callable(string $path): void
        'onDirDelete'    => null,   // callable(string $path): void
        'onError'        => null,   // callable(string $path, string $error): void
    ];
    $opts = array_merge($defaults, $options);
    
    $stats = ['files' => 0, 'dirs' => 0, 'errors' => 0, 'bytes' => 0];
    
    $handler = function ($path) use (&$handler, &$stats, $opts) {
        if (is_link($path)) {
            if ($opts['followSymlinks'] && is_dir($path)) {
                $handler($path);  // 遞歸進入目標目錄
            }
            if (@unlink($path)) {
                $stats['files']++;
                $opts['onFileDelete']?.($path);
            } else {
                $stats['errors']++;
                $opts['onError']?.($path, 'unlink symlink failed');
            }
            return;
        }
        
        if (is_dir($path)) {
            $handle = @opendir($path);
            if ($handle) {
                while (($entry = readdir($handle)) !== false) {
                    if ($entry !== '.' && $entry !== '..') {
                        $handler($path . DIRECTORY_SEPARATOR . $entry);
                    }
                }
                closedir($handle);
            }
            if ($opts['deleteSelf'] || $path !== $dir) {  // 根目錄由外層控制
                if (@rmdir($path)) {
                    $stats['dirs']++;
                    $opts['onDirDelete']?.($path);
                } else {
                    $stats['errors']++;
                    $opts['onError']?.($path, 'rmdir failed');
                }
            }
        } elseif (is_file($path)) {
            $stats['bytes'] += filesize($path);
            if (@unlink($path)) {
                $stats['files']++;
                $opts['onFileDelete']?.($path);
            } else {
                $stats['errors']++;
                $opts['onError']?.($path, 'unlink file failed');
            }
        }
    };
    
    $handler($dir);
    
    // 最後刪除根目錄
    if ($opts['deleteSelf'] && is_dir($dir)) {
        if (@rmdir($dir)) {
            $stats['dirs']++;
        } else {
            $stats['errors']++;
        }
    }
    
    return $stats;
}

// 使用示例
$stats = deleteDirectoryAdvanced('/var/www/uploads/temp', [
    'onError' => fn($p, $e) => error_log("DELETE ERROR: {$p} - {$e}"),
]);
echo "刪除統計: " . json_encode($stats);

生產環境最佳實踐

  1. 軟刪除優先:重要數據不直接刪除,改為移動到回收站目錄或標記 deleted_at,定期異步清理。
  2. 異步執行:大目錄刪除放入隊列,避免阻塞 Web 請求、超時或內存峰值。
  3. 權限最小化:Web 進程用戶僅擁有上傳目錄寫入權限,不可刪除代碼目錄、配置文件等敏感路徑。
  4. 路徑校驗:嚴格限制刪除路徑在允許範圍內(realpath() 對比),防止目錄遍歷攻擊(../../etc)。
  5. 審計日誌:記錄操作人、時間、路徑、結果,滿足合規審計要求。

延伸閱讀

相關專題:PHP 遞歸操作、PHP 文件操作總結、PHP 目錄遍歷與文件處理。

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