基本遞迴實作
<?php
/**
* 遞迴計算目錄總大小(位元組)
* @param string $directory 目錄絕對路徑
* @return int|false 總位元組數,失敗回傳 false
*/
function getDirectorySize($directory) {
$directory = rtrim($directory, '/\\') . DIRECTORY_SEPARATOR;
if (!is_dir($directory) || !is_readable($directory)) {
return false;
}
$size = 0;
$handle = opendir($directory);
if (!$handle) return false;
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
$path = $directory . $entry;
if (is_dir($path)) {
$subSize = getDirectorySize($path);
if ($subSize === false) return false;
$size += $subSize;
} elseif (is_file($path)) {
$size += filesize($path);
}
// 忽略連結、裝置等特殊檔案
}
closedir($handle);
return $size;
}
// 使用
$bytes = getDirectorySize('/var/www/html');
if ($bytes !== false) {
echo formatBytes($bytes); // 見下方格式化函數
}
/** 位元組轉人類可讀格式 */
function formatBytes($bytes, $precision = 2) {
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$value = $bytes / (1024 ** $pow);
return round($value, $precision) . ' ' . $units[$pow];
}
?>
SPL 迭代器版本(更現代、可中斷)
<?php
function getDirectorySizeSPL($directory) {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY // 只遍歷檔案,不進入目錄節點
);
$size = 0;
foreach ($iterator as $file) {
if ($file->isFile()) {
$size += $file->getSize();
}
}
return $size;
}
// 進階:支援排除模式、最大深度、進度回調
function getDirectorySizeAdvanced($directory, array $options = []) {
$defaults = [
'exclude_patterns' => ['/\.git/', '/node_modules/', '/vendor/', '/\.cache/'],
'max_depth' => -1,
'follow_links' => false,
'progress_callback' => null, // function(int $currentBytes, int $fileCount)
];
$opts = array_merge($defaults, $options);
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
$directory,
($opts['follow_links'] ? 0 : RecursiveDirectoryIterator::FOLLOW_SYMLINKS) |
RecursiveDirectoryIterator::SKIP_DOTS |
RecursiveDirectoryIterator::KEY_AS_PATHNAME
),
RecursiveIteratorIterator::LEAVES_ONLY
);
if ($opts['max_depth'] > 0) {
$iterator = new RegexIterator($iterator, '/^.{' . $opts['max_depth'] . '}/'); // 簡易深度限制
}
$size = 0;
$count = 0;
foreach ($iterator as $path => $file) {
// 排除模式檢查
$excluded = false;
foreach ($opts['exclude_patterns'] as $pattern) {
if (preg_match($pattern, $path)) {
$excluded = true;
break;
}
}
if ($excluded) continue;
if ($file->isFile()) {
$size += $file->getSize();
$count++;
if ($opts['progress_callback'] && $count % 1000 === 0) {
$opts['progress_callback']($size, $count);
}
}
}
return $size;
}
?>
系統命令版(大目錄最高效能)
<?php
function getDirectorySizeCLI($directory) {
// Linux/Unix: du -sb (bytes, 不跟隨符號連結)
// macOS: du -sk (KB) 再 * 1024
$directory = escapeshellarg($directory);
if (PHP_OS_FAMILY === 'Windows') {
// Windows: 使用 dir /s 或 PowerShell
$cmd = "powershell -Command \"(Get-ChildItem -Recurse -Force $directory | Measure-Object -Property Length -Sum).Sum\"";
} else {
$cmd = "du -sb $directory 2>/dev/null";
}
$output = shell_exec($cmd);
$size = (int)trim($output);
return $size > 0 ? $size : false;
}
/** 自動選擇最佳方法 */
function getDirectorySizeAuto($directory) {
// 嘗試系統命令(最快)
$size = getDirectorySizeCLI($directory);
if ($size !== false) return $size;
// 回退 SPL 迭代器
return getDirectorySizeSPL($directory);
}
?>
效能比較與選擇建議
| 方法 | 適用規模 | 優點 | 缺點 |
| 遞迴函數 | < 1 萬檔案 | 純 PHP、可攜、易理解 | 函數呼叫開銷大、易超出記憶體/執行時間 |
| SPL 迭代器 | < 10 萬檔案 | 記憶體友善、可中斷、支援過濾 | PHP 迴圈仍較慢 |
系統命令 du | 任意規模 | C 語言實作、核心優化、極快 | 平台相依、需 shell 執行權限、Windows 不同 |
| 資料庫/快取記錄 | 頻繁查詢 | 毫秒級回應 | 需維護同步機制 |
生產環境最佳實踐
- 非同步計算:佇列任務(如 Laravel Queue、Gearman),背景計算後存入 Redis
- 增量更新:檔案異動時(inotify、Flysystem 事件)只更新差值
- 分層快取:目錄大小快取 1 小時,檔案數快取 10 分鐘
- 監控告警:目錄超過閾值(如 100GB)自動通知