核心實現
<?php
/**
* 遞歸刪除目錄及其所有內容
* @param string $path 目錄或文件路徑
* @param bool $deleteRoot 是否刪除根目錄本身(false 時僅清空目錄)
* @return bool 成功返回 true
* @throws Exception 路徑不存在或權限不足
*/
function deleteDirectory(string $path, bool $deleteRoot = true): bool {
// 規範化路徑
$path = rtrim(realpath($path), DIRECTORY_SEPARATOR);
if ($path === false || !file_exists($path)) {
throw new Exception("路徑不存在:{$path}");
}
if (is_file($path) || is_link($path)) {
return unlink($path);
}
// 目錄處理
$items = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST // 先刪除子項
);
foreach ($items as $item) {
if ($item->isFile() || $item->isLink()) {
if (!@unlink($item->getPathname())) {
throw new Exception("無法刪除文件:{$item->getPathname()}");
}
} elseif ($item->isDir()) {
if (!@rmdir($item->getPathname())) {
throw new Exception("無法刪除目錄:{$item->getPathname()}");
}
}
}
if ($deleteRoot) {
return @rmdir($path);
}
return true;
}
// === 使用示例 ===
try {
// 刪除整個目錄
deleteDirectory('/var/www/uploads/temp');
// 僅清空目錄,保留目錄本身
deleteDirectory('/var/www/cache', false);
echo "操作成功";
} catch (Exception $e) {
echo "錯誤:" . $e->getMessage();
}
?>
傳統遞歸函數寫法(兼容老版本 PHP)
function deleteDirRecursive(string $dir): bool {
if (!is_dir($dir)) {
return is_file($dir) ? @unlink($dir) : false;
}
$handle = opendir($dir);
while (($file = readdir($handle)) !== false) {
if ($file === '.' || $file === '..') continue;
$full = $dir . DIRECTORY_SEPARATOR . $file;
if (is_dir($full)) {
deleteDirRecursive($full);
} else {
@unlink($full);
}
}
closedir($handle);
return @rmdir($dir);
}
安全加固清單
| 風險 | 防範措施 |
| 路徑遍歷攻擊 | 使用 realpath() 解析絕對路徑,檢查是否在允許的根目錄下 |
| 誤刪關鍵目錄 | 維護白名單目錄,禁止刪除系統目錄、項目核心目錄 |
| 權限不足導致部分刪除 | 捕獲異常、記錄日誌、返回失敗狀態供上層處理 |
| 符號鏈接逃逸 | RecursiveDirectoryIterator::SKIP_DOTS 不跟隨鏈接,或顯式檢查 is_link() |
| 大目錄執行超時 | 設置 set_time_limit(0),或分批處理(佇列+後台任務) |
生產環境推薦封裝類
class FileCleaner {
private array $allowedRoots = [];
private int $maxDepth = 10;
private int $maxFiles = 10000;
public function addAllowedRoot(string $path): self {
$this->allowedRoots[] = rtrim(realpath($path), DIRECTORY_SEPARATOR);
return $this;
}
public function delete(string $targetPath): array {
$result = ['success' => false, 'deleted' => 0, 'errors' => []];
$target = rtrim(realpath($targetPath), DIRECTORY_SEPARATOR);
// 安全檢查
if (!$target || !file_exists($target)) {
$result['errors'][] = '目標路徑不存在';
return $result;
}
if (!$this->isPathAllowed($target)) {
$result['errors'][] = '路徑不在允許範圍內';
return $result;
}
try {
$this->doDelete($target, $result, 0);
$result['success'] = true;
} catch (Exception $e) {
$result['errors'][] = $e->getMessage();
}
return $result;
}
private function isPathAllowed(string $path): bool {
foreach ($this->allowedRoots as $root) {
if (str_starts_with($path, $root)) return true;
}
return false;
}
private function doDelete(string $path, array &$result, int $depth): void {
if ($depth > $this->maxDepth) {
throw new Exception("超過最大遞歸深度 {$this->maxDepth}");
}
if ($result['deleted'] >= $this->maxFiles) {
throw new Exception("超過最大文件數 {$this->maxFiles}");
}
if (is_file($path) || is_link($path)) {
if (@unlink($path)) $result['deleted']++;
return;
}
$items = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item) {
$this->doDelete($item->getPathname(), $result, $depth + 1);
}
if (@rmdir($path)) $result['deleted']++;
}
}
// 使用
$cleaner = new FileCleaner();
$cleaner->addAllowedRoot('/var/www/uploads');
$res = $cleaner->delete('/var/www/uploads/temp');
print_r($res);
- 上一篇文章: PHP 獲取文件擴展名的多種方法與最佳實踐
- 下一篇文章: PHP + JavaScript 在線圖片裁剪:基於 YUI ImageCropper 實現上傳即時裁剪下載
Copyright ©
程式碼之家 All Rights Reserved