文件系統操作是 PHP 開發中的高頻需求。本文系統整理了目錄遞歸遍歷、整體複製、遞歸刪除、大小統計四大核心操作的標準實現模式,並深度解析遞歸算法在文件系統中的應用精髓,助你構建健壯的文件管理功能。
<?php
/**
* 遞歸遍歷目錄,輸出所有文件名
* @param string $dirPath 目錄路徑
* @param int $depth 當前遞歸深度(內部使用)
*/
function listDir(string $dirPath, int $depth = 0): void {
// 規範化路徑
$dirPath = rtrim($dirPath, '/\\');
if (!is_dir($dirPath)) {
throw new InvalidArgumentException("目錄不存在: {$dirPath}");
}
$handle = opendir($dirPath);
if (!$handle) {
throw new RuntimeException("無法打開目錄: {$dirPath}");
}
try {
while (($entry = readdir($handle)) !== false) {
// 跳過特殊目錄
if ($entry === '.' || $entry === '..') continue;
$fullPath = $dirPath . DIRECTORY_SEPARATOR . $entry;
if (is_dir($fullPath)) {
// 目錄:遞歸處理
echo str_repeat(' ', $depth) . "[DIR] {$entry}\n";
listDir($fullPath, $depth + 1);
} else {
// 文件:直接輸出
echo str_repeat(' ', $depth) . "[FILE] {$entry}\n";
}
}
} finally {
closedir($handle);
}
}
// 使用示例
listDir(__DIR__ . '/storage/app');
<?php
/**
* 遞歸複製整個目錄結構
* @param string $srcDir 源目錄
* @param string $dstDir 目標目錄
* @param int $mode 目錄權限(默認 0755)
* @return bool 是否成功
*/
function copyDir(string $srcDir, string $dstDir, int $mode = 0755): bool {
$srcDir = rtrim($srcDir, '/\\');
$dstDir = rtrim($dstDir, '/\\');
if (!is_dir($srcDir)) {
throw new InvalidArgumentException("源目錄不存在: {$srcDir}");
}
// 創建目標目錄
if (!is_dir($dstDir)) {
if (!mkdir($dstDir, $mode, true) && !is_dir($dstDir)) {
throw new RuntimeException("無法創建目標目錄: {$dstDir}");
}
}
$handle = opendir($srcDir);
if (!$handle) {
throw new RuntimeException("無法打開源目錄: {$srcDir}");
}
try {
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
$srcPath = $srcDir . DIRECTORY_SEPARATOR . $entry;
$dstPath = $dstDir . DIRECTORY_SEPARATOR . $entry;
if (is_dir($srcPath)) {
// 遞歸複製子目錄
if (!copyDir($srcPath, $dstPath, $mode)) {
return false;
}
} else {
// 複製文件
if (!copy($srcPath, $dstPath)) {
throw new RuntimeException("文件複製失敗: {$srcPath} -> {$dstPath}");
}
// 可選:保持文件權限
@chmod($dstPath, 0644);
}
}
return true;
} finally {
closedir($handle);
}
}
<?php
/**
* 遞歸刪除目錄及其所有內容
* @param string $dirPath 目錄路徑
* @return bool 是否成功
*/
function deleteDir(string $dirPath): bool {
$dirPath = rtrim($dirPath, '/\\');
if (!is_dir($dirPath)) {
return true; // 已不存在視為成功
}
$handle = opendir($dirPath);
if (!$handle) {
throw new RuntimeException("無法打開目錄進行刪除: {$dirPath}");
}
try {
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
$fullPath = $dirPath . DIRECTORY_SEPARATOR . $entry;
if (is_dir($fullPath)) {
// 遞歸刪除子目錄
if (!deleteDir($fullPath)) {
return false;
}
} else {
// 刪除文件
if (!unlink($fullPath)) {
throw new RuntimeException("文件刪除失敗: {$fullPath}");
}
}
}
} finally {
closedir($handle);
}
// 最後刪除空目錄本身
return rmdir($dirPath);
}
<?php
/**
* 遞歸計算目錄總大小(位元組)
* @param string $dirPath 目錄路徑
* @return int 總大小(位元組)
*/
function getDirSize(string $dirPath): int {
$dirPath = rtrim($dirPath, '/\\');
if (!is_dir($dirPath)) {
throw new InvalidArgumentException("目錄不存在: {$dirPath}");
}
$totalSize = 0;
$handle = opendir($dirPath);
if (!$handle) {
throw new RuntimeException("無法打開目錄: {$dirPath}");
}
try {
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
$fullPath = $dirPath . DIRECTORY_SEPARATOR . $entry;
if (is_dir($fullPath)) {
// 遞歸累加子目錄大小
$totalSize += getDirSize($fullPath);
} elseif (is_file($fullPath)) {
// 累加文件大小
$totalSize += filesize($fullPath);
}
// 忽略符號鏈接等特殊文件
}
} finally {
closedir($handle);
}
return $totalSize;
}
/**
* 格式化位元組為人類可讀格式
*/
function formatBytes(int $bytes, int $precision = 2): string {
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
$pow = floor(($bytes > 0 ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1024 ** $pow);
return round($bytes, $precision) . ' ' . $units[$pow];
}
// 使用示例
$size = getDirSize('/var/www/uploads');
echo "目錄大小: " . formatBytes($size); // 輸出如:目錄大小: 12.45 MB
| 細節 | 說明 |
|---|---|
| 路徑拼接 | 使用 DIRECTORY_SEPARATOR 常量確保跨平台兼容 |
| 資源管理 | 使用 try/finally 確保 closedir() 必執行 |
| 錯誤處理 | 檢查 opendir() 返回值,異常時拋出具體異常 |
| 權限保持 | 複製時可選保持文件權限(chmod) |
| 符號鏈接 | 生產環境建議添加 is_link() 判斷,避免循環引用 |
PHP 5.3+ 提供了更優雅的 RecursiveDirectoryIterator:
<?php
// 遍歷所有文件(現代寫法)
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('/path/to/dir', FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
echo $file->getPathname() . ' (' . $file->getSize() . " bytes)\n";
}
// 計算目錄大小(一行代碼)
$totalSize = iterator_reduce(
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('/path/to/dir', FilesystemIterator::SKIP_DOTS)
),
fn($carry, $file) => $carry + $file->getSize(),
0
);
scandir() + 非遞歸隊列算法,避免棧溢出disk_free_space() 檢查剩餘空間,防止寫滿磁盤