/**
* 遞迴計算目錄總大小(位元組)
* @param string $dir 目錄路徑
* @return int 總位元組數
* @throws InvalidArgumentException 路徑不存在或不可讀
*/
function getDirSize(string $dir): int {
// 規範化路徑
$dir = rtrim(realpath($dir), DIRECTORY_SEPARATOR);
if ($dir === false || !is_dir($dir) || !is_readable($dir)) {
throw new InvalidArgumentException("目錄不存在或不可讀: $dir");
}
$size = 0;
$handle = opendir($dir);
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
$path = $dir . DIRECTORY_SEPARATOR . $entry;
if (is_dir($path)) {
// 遞迴處理子目錄
$size += getDirSize($path);
} elseif (is_file($path)) {
// 累加檔案大小
$size += filesize($path);
}
// 忽略連結、裝置檔等特殊類型
}
closedir($handle);
return $size;
}
/**
* 位元組轉人類可讀格式
* @param int $bytes 位元組數
* @param int $precision 小數位數
* @return string 如 1.5 MB、2.3 GB
*/
function formatBytes(int $bytes, int $precision = 2): string {
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
if ($bytes === 0) return '0 B';
// 計算對應單位
$exp = (int)floor(log($bytes, 1024));
$exp = min($exp, count($units) - 1); // 限制最大單位
$value = $bytes / (1024 ** $exp);
return round($value, $precision) . ' ' . $units[$exp];
}
class DirectorySize {
private array $excludePatterns = [];
private int $maxDepth = -1; // -1 無限制
private array $cache = [];
public function __construct(array $options = []) {
$this->excludePatterns = $options['exclude'] ?? [];
$this->maxDepth = $options['max_depth'] ?? -1;
}
public function calculate(string $dir, int $currentDepth = 0): int {
$dir = rtrim(realpath($dir), DIRECTORY_SEPARATOR);
// 快取鍵
$cacheKey = $dir . '|' . $currentDepth;
if (isset($this->cache[$cacheKey])) {
return $this->cache[$cacheKey];
}
// 深度限制
if ($this->maxDepth >= 0 && $currentDepth > $this->maxDepth) {
return 0;
}
// 排除模式匹配
foreach ($this->excludePatterns as $pattern) {
if (fnmatch($pattern, basename($dir)) || fnmatch($pattern, $dir)) {
return 0;
}
}
$size = 0;
$handle = @opendir($dir);
if (!$handle) return 0; // 權限不足靜默跳過
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
$path = $dir . DIRECTORY_SEPARATOR . $entry;
if (is_dir($path)) {
$size += $this->calculate($path, $currentDepth + 1);
} elseif (is_file($path)) {
$size += filesize($path);
}
}
closedir($handle);
return $this->cache[$cacheKey] = $size;
}
public function format(int $bytes): string {
return formatBytes($bytes);
}
}
// 使用
$calculator = new DirectorySize([
'exclude' => ['.git', 'node_modules', 'vendor', '*.log', 'cache/*'],
'max_depth' => 3,
]);
$bytes = $calculator->calculate('/var/www/project');
echo "專案大小: " . $calculator->format($bytes); // 專案大小: 45.2 MB
| 場景 | 優化策略 |
|---|---|
| 超大目錄(10萬+檔案) | 使用 RecursiveDirectoryIterator + RecursiveIteratorIterator,C 層面遍歷更快 |
| 頻繁查詢同一目錄 | 快取結果,監聽 inotify 事件失效 |
| 跨網路掛載(NFS/SMB) | 啟用非同步 I/O,避免阻塞 |
| 只需特定副檔名 | 加入 pathinfo($path, PATHINFO_EXTENSION) 過濾 |
function getDirSizeIterator(string $dir): int {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
$size = 0;
foreach ($iterator as $file) {
if ($file->isFile()) {
$size += $file->getSize();
}
}
return $size;
}
function checkDiskQuota(string $userDir, int $quotaMB): array {
$bytes = getDirSize($userDir);
$quotaBytes = $quotaMB * 1024 * 1024;
$usagePercent = $quotaBytes > 0 ? ($bytes / $quotaBytes) * 100 : 0;
return [
'used' => formatBytes($bytes),
'quota' => formatBytes($quotaBytes),
'percent' => round($usagePercent, 1),
'exceeded' => $bytes > $quotaBytes,
'remaining' => formatBytes(max(0, $quotaBytes - $bytes)),
];
}
// 使用
$quota = checkDiskQuota('/home/user1', 1024); // 1GB 配額
if ($quota['exceeded']) {
// 發送告警、限制寫入...
}
@opendir 抑制警告,不可讀目錄返回 0 而非拋出異常is_dir() 對 symlink 返回 true,需用 is_link() 排除或追蹤已訪問 inodefilesize() 最大 2GB,需用 stat()['size'] 或 sprintf('%u', filesize())set_time_limit(0)getDirSize() + formatBytes() 是經典組合。現代專案建議封裝為 DirectorySize 類別,支援排除模式、深度限制、快取機制。生產環境注意權限處理、符號連結、大檔案與執行時間限制。