[PHP]lstat関数完全ガイド – ファイル情報取得とstat関数との違い

PHP

はじめに

PHPのlstat関数は、ファイルやディレクトリの詳細な情報を取得するための関数です。似たような機能を持つstat関数との大きな違いは、シンボリックリンクの扱い方にあります。

今回は、lstat関数の基本的な使い方から、実際の開発現場での活用方法まで詳しく解説します。

lstat関数とは

基本構文

array|false lstat(string $filename)

パラメータ:

  • $filename:情報を取得したいファイルまたはディレクトリのパス

戻り値:

  • 成功時:ファイル情報を含む配列
  • 失敗時:false

基本的な使用例

$file_info = lstat('/path/to/file.txt');

if ($file_info !== false) {
    print_r($file_info);
} else {
    echo "ファイル情報の取得に失敗しました\n";
}

lstatとstatの違い

重要な違い:シンボリックリンクの扱い

  • stat関数:シンボリックリンクのリンク先の情報を取得
  • lstat関数:シンボリックリンク自体の情報を取得
// シンボリックリンクの作成例(Linux/Unix環境)
// ln -s /var/log/access.log /tmp/loglink

$original_file = '/var/log/access.log';
$symlink = '/tmp/loglink';

// stat:リンク先の情報
$stat_info = stat($symlink);
echo "stat - ファイルサイズ: " . $stat_info['size'] . " bytes\n";

// lstat:リンク自体の情報
$lstat_info = lstat($symlink);
echo "lstat - リンクサイズ: " . $lstat_info['size'] . " bytes\n";

// シンボリックリンクかどうかの判定
if (($lstat_info['mode'] & 0170000) === 0120000) {
    echo "これはシンボリックリンクです\n";
}

実用的な比較関数

function compareStatAndLstat($filepath) {
    if (!file_exists($filepath)) {
        return "ファイルが存在しません: {$filepath}";
    }
    
    $stat_info = stat($filepath);
    $lstat_info = lstat($filepath);
    
    echo "=== ファイル: {$filepath} ===\n";
    
    // シンボリックリンクかどうかの判定
    $is_symlink = is_link($filepath);
    echo "シンボリックリンク: " . ($is_symlink ? "はい" : "いいえ") . "\n";
    
    echo "\nstat() の結果:\n";
    echo "  サイズ: {$stat_info['size']} bytes\n";
    echo "  最終更新: " . date('Y-m-d H:i:s', $stat_info['mtime']) . "\n";
    
    echo "\nlstat() の結果:\n";
    echo "  サイズ: {$lstat_info['size']} bytes\n";
    echo "  最終更新: " . date('Y-m-d H:i:s', $lstat_info['mtime']) . "\n";
    
    if ($is_symlink) {
        echo "\nリンク先: " . readlink($filepath) . "\n";
        echo "注意: stat()はリンク先、lstat()はリンク自体の情報です\n";
    }
    
    echo "\n" . str_repeat("-", 50) . "\n\n";
}

戻り値の詳細解説

配列のキーと意味

function explainLstatResult($filepath) {
    $info = lstat($filepath);
    
    if ($info === false) {
        echo "ファイル情報を取得できませんでした\n";
        return;
    }
    
    $explanations = [
        'dev' => 'デバイス番号',
        'ino' => 'inode番号',
        'mode' => 'アクセス許可とファイルタイプ',
        'nlink' => 'ハードリンク数',
        'uid' => 'ユーザーID(所有者)',
        'gid' => 'グループID',
        'rdev' => '特殊ファイルの場合のデバイスタイプ',
        'size' => 'ファイルサイズ(bytes)',
        'atime' => '最終アクセス時刻',
        'mtime' => '最終更新時刻',
        'ctime' => '最終inode変更時刻',
        'blksize' => 'ファイルシステムのブロックサイズ',
        'blocks' => '使用ブロック数'
    ];
    
    echo "=== ファイル情報: {$filepath} ===\n";
    
    foreach ($explanations as $key => $description) {
        $value = $info[$key] ?? 'N/A';
        
        // 特別な表示処理
        if (in_array($key, ['atime', 'mtime', 'ctime']) && is_numeric($value)) {
            $formatted_time = date('Y-m-d H:i:s', $value);
            echo sprintf("%-10s: %-20s (%s)\n", $key, $value, $formatted_time);
        } elseif ($key === 'mode') {
            $permissions = substr(sprintf('%o', $value), -4);
            $type = $this->getFileTypeFromMode($value);
            echo sprintf("%-10s: %-20s (%s, %s)\n", $key, $value, $permissions, $type);
        } elseif ($key === 'size') {
            $readable_size = $this->formatBytes($value);
            echo sprintf("%-10s: %-20s (%s)\n", $key, $value, $readable_size);
        } else {
            echo sprintf("%-10s: %s\n", $key, $value);
        }
    }
}

// ヘルパー関数
function getFileTypeFromMode($mode) {
    switch ($mode & 0170000) {
        case 0140000: return 'ソケット';
        case 0120000: return 'シンボリックリンク';
        case 0100000: return '通常ファイル';
        case 0060000: return 'ブロックデバイス';
        case 0040000: return 'ディレクトリ';
        case 0020000: return 'キャラクターデバイス';
        case 0010000: return 'FIFO';
        default: return '不明';
    }
}

function formatBytes($bytes) {
    $units = ['B', 'KB', 'MB', 'GB', 'TB'];
    $bytes = max($bytes, 0);
    $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
    $pow = min($pow, count($units) - 1);
    
    $bytes /= pow(1024, $pow);
    
    return round($bytes, 2) . ' ' . $units[$pow];
}

実用的な応用例

1. ファイルシステム分析ツール

class FileSystemAnalyzer {
    private $stats = [];
    
    public function analyzeDirectory($directory, $recursive = false) {
        if (!is_dir($directory)) {
            throw new InvalidArgumentException("指定されたパスはディレクトリではありません: {$directory}");
        }
        
        $this->stats = [
            'files' => 0,
            'directories' => 0,
            'symlinks' => 0,
            'total_size' => 0,
            'largest_file' => ['name' => '', 'size' => 0],
            'oldest_file' => ['name' => '', 'mtime' => PHP_INT_MAX],
            'newest_file' => ['name' => '', 'mtime' => 0]
        ];
        
        $this->scanDirectory($directory, $recursive);
        
        return $this->stats;
    }
    
    private function scanDirectory($directory, $recursive) {
        $iterator = new DirectoryIterator($directory);
        
        foreach ($iterator as $item) {
            if ($item->isDot()) {
                continue;
            }
            
            $filepath = $item->getPathname();
            $info = lstat($filepath);
            
            if ($info === false) {
                continue;
            }
            
            // ファイルタイプの判定
            if (is_link($filepath)) {
                $this->stats['symlinks']++;
            } elseif (is_dir($filepath)) {
                $this->stats['directories']++;
                
                if ($recursive) {
                    $this->scanDirectory($filepath, $recursive);
                }
            } else {
                $this->stats['files']++;
                $this->stats['total_size'] += $info['size'];
                
                // 最大ファイル
                if ($info['size'] > $this->stats['largest_file']['size']) {
                    $this->stats['largest_file'] = [
                        'name' => $filepath,
                        'size' => $info['size']
                    ];
                }
                
                // 最古・最新ファイル
                if ($info['mtime'] < $this->stats['oldest_file']['mtime']) {
                    $this->stats['oldest_file'] = [
                        'name' => $filepath,
                        'mtime' => $info['mtime']
                    ];
                }
                
                if ($info['mtime'] > $this->stats['newest_file']['mtime']) {
                    $this->stats['newest_file'] = [
                        'name' => $filepath,
                        'mtime' => $info['mtime']
                    ];
                }
            }
        }
    }
    
    public function generateReport() {
        $report = "=== ファイルシステム分析レポート ===\n";
        $report .= "ファイル数: " . number_format($this->stats['files']) . "\n";
        $report .= "ディレクトリ数: " . number_format($this->stats['directories']) . "\n";
        $report .= "シンボリックリンク数: " . number_format($this->stats['symlinks']) . "\n";
        $report .= "合計サイズ: " . $this->formatBytes($this->stats['total_size']) . "\n";
        
        if (!empty($this->stats['largest_file']['name'])) {
            $report .= "\n最大ファイル:\n";
            $report .= "  " . $this->stats['largest_file']['name'] . "\n";
            $report .= "  サイズ: " . $this->formatBytes($this->stats['largest_file']['size']) . "\n";
        }
        
        if (!empty($this->stats['oldest_file']['name'])) {
            $report .= "\n最古ファイル:\n";
            $report .= "  " . $this->stats['oldest_file']['name'] . "\n";
            $report .= "  更新日時: " . date('Y-m-d H:i:s', $this->stats['oldest_file']['mtime']) . "\n";
        }
        
        if (!empty($this->stats['newest_file']['name'])) {
            $report .= "\n最新ファイル:\n";
            $report .= "  " . $this->stats['newest_file']['name'] . "\n";
            $report .= "  更新日時: " . date('Y-m-d H:i:s', $this->stats['newest_file']['mtime']) . "\n";
        }
        
        return $report;
    }
    
    private function formatBytes($bytes) {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        $bytes = max($bytes, 0);
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow = min($pow, count($units) - 1);
        
        $bytes /= pow(1024, $pow);
        
        return round($bytes, 2) . ' ' . $units[$pow];
    }
}

// 使用例
$analyzer = new FileSystemAnalyzer();
$stats = $analyzer->analyzeDirectory('/var/log', false);
echo $analyzer->generateReport();

2. シンボリックリンクのメンテナンスツール

class SymlinkManager {
    public function findBrokenSymlinks($directory) {
        $broken_links = [];
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($directory)
        );
        
        foreach ($iterator as $item) {
            if ($item->isDot()) {
                continue;
            }
            
            $filepath = $item->getPathname();
            
            // シンボリックリンクかどうか確認
            if (is_link($filepath)) {
                $link_target = readlink($filepath);
                
                // リンク先が存在するかチェック
                if (!file_exists($filepath)) {  // file_existsはリンク先をチェック
                    $lstat_info = lstat($filepath);
                    $broken_links[] = [
                        'link' => $filepath,
                        'target' => $link_target,
                        'mtime' => $lstat_info['mtime']
                    ];
                }
            }
        }
        
        return $broken_links;
    }
    
    public function analyzeSymlinks($directory) {
        $symlinks = [];
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($directory)
        );
        
        foreach ($iterator as $item) {
            if ($item->isDot()) {
                continue;
            }
            
            $filepath = $item->getPathname();
            
            if (is_link($filepath)) {
                $link_target = readlink($filepath);
                $lstat_info = lstat($filepath);
                $target_exists = file_exists($filepath);
                
                $symlinks[] = [
                    'link' => $filepath,
                    'target' => $link_target,
                    'target_exists' => $target_exists,
                    'link_size' => $lstat_info['size'],
                    'created' => date('Y-m-d H:i:s', $lstat_info['ctime']),
                    'modified' => date('Y-m-d H:i:s', $lstat_info['mtime'])
                ];
            }
        }
        
        return $symlinks;
    }
    
    public function generateSymlinkReport($directory) {
        $symlinks = $this->analyzeSymlinks($directory);
        $broken = array_filter($symlinks, function($link) {
            return !$link['target_exists'];
        });
        
        $report = "=== シンボリックリンク分析レポート ===\n";
        $report .= "検索ディレクトリ: {$directory}\n";
        $report .= "シンボリックリンク総数: " . count($symlinks) . "\n";
        $report .= "壊れたリンク数: " . count($broken) . "\n\n";
        
        if (!empty($broken)) {
            $report .= "=== 壊れたシンボリックリンク ===\n";
            foreach ($broken as $link) {
                $report .= "リンク: {$link['link']}\n";
                $report .= "  → {$link['target']} (存在しません)\n";
                $report .= "  作成日時: {$link['created']}\n\n";
            }
        }
        
        return $report;
    }
}

// 使用例
$manager = new SymlinkManager();
echo $manager->generateSymlinkReport('/usr/local');

3. ファイル変更監視システム

class FileChangeMonitor {
    private $baseline_file;
    private $baseline_data;
    
    public function __construct($baseline_file = 'file_baseline.json') {
        $this->baseline_file = $baseline_file;
        $this->loadBaseline();
    }
    
    public function createBaseline($directory, $recursive = true) {
        $this->baseline_data = [];
        $this->scanDirectory($directory, $recursive);
        $this->saveBaseline();
        
        return count($this->baseline_data);
    }
    
    private function scanDirectory($directory, $recursive) {
        $iterator = $recursive 
            ? new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory))
            : new DirectoryIterator($directory);
        
        foreach ($iterator as $item) {
            if ($item->isDot()) {
                continue;
            }
            
            $filepath = $item->getPathname();
            $info = lstat($filepath);
            
            if ($info !== false) {
                $this->baseline_data[$filepath] = [
                    'size' => $info['size'],
                    'mtime' => $info['mtime'],
                    'mode' => $info['mode'],
                    'is_link' => is_link($filepath)
                ];
            }
        }
    }
    
    public function checkChanges($directory, $recursive = true) {
        $current_data = [];
        $this->scanDirectoryForCheck($directory, $recursive, $current_data);
        
        $changes = [
            'modified' => [],
            'added' => [],
            'deleted' => []
        ];
        
        // 変更されたファイル
        foreach ($current_data as $filepath => $current_info) {
            if (isset($this->baseline_data[$filepath])) {
                $baseline_info = $this->baseline_data[$filepath];
                
                if ($current_info['mtime'] > $baseline_info['mtime'] ||
                    $current_info['size'] != $baseline_info['size']) {
                    $changes['modified'][] = [
                        'file' => $filepath,
                        'old_size' => $baseline_info['size'],
                        'new_size' => $current_info['size'],
                        'old_mtime' => $baseline_info['mtime'],
                        'new_mtime' => $current_info['mtime']
                    ];
                }
            } else {
                $changes['added'][] = $filepath;
            }
        }
        
        // 削除されたファイル
        foreach ($this->baseline_data as $filepath => $baseline_info) {
            if (!isset($current_data[$filepath])) {
                $changes['deleted'][] = $filepath;
            }
        }
        
        return $changes;
    }
    
    private function scanDirectoryForCheck($directory, $recursive, &$current_data) {
        $iterator = $recursive 
            ? new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory))
            : new DirectoryIterator($directory);
        
        foreach ($iterator as $item) {
            if ($item->isDot()) {
                continue;
            }
            
            $filepath = $item->getPathname();
            $info = lstat($filepath);
            
            if ($info !== false) {
                $current_data[$filepath] = [
                    'size' => $info['size'],
                    'mtime' => $info['mtime'],
                    'mode' => $info['mode'],
                    'is_link' => is_link($filepath)
                ];
            }
        }
    }
    
    public function generateChangeReport($changes) {
        $report = "=== ファイル変更レポート ===\n";
        $report .= "生成日時: " . date('Y-m-d H:i:s') . "\n\n";
        
        if (!empty($changes['added'])) {
            $report .= "新規ファイル (" . count($changes['added']) . "件):\n";
            foreach ($changes['added'] as $file) {
                $report .= "  + {$file}\n";
            }
            $report .= "\n";
        }
        
        if (!empty($changes['modified'])) {
            $report .= "変更されたファイル (" . count($changes['modified']) . "件):\n";
            foreach ($changes['modified'] as $change) {
                $report .= "  * {$change['file']}\n";
                $report .= "    サイズ: {$change['old_size']} → {$change['new_size']}\n";
                $report .= "    更新日時: " . date('Y-m-d H:i:s', $change['old_mtime']) . 
                          " → " . date('Y-m-d H:i:s', $change['new_mtime']) . "\n";
            }
            $report .= "\n";
        }
        
        if (!empty($changes['deleted'])) {
            $report .= "削除されたファイル (" . count($changes['deleted']) . "件):\n";
            foreach ($changes['deleted'] as $file) {
                $report .= "  - {$file}\n";
            }
            $report .= "\n";
        }
        
        if (empty($changes['added']) && empty($changes['modified']) && empty($changes['deleted'])) {
            $report .= "変更はありません。\n";
        }
        
        return $report;
    }
    
    private function loadBaseline() {
        if (file_exists($this->baseline_file)) {
            $this->baseline_data = json_decode(file_get_contents($this->baseline_file), true) ?? [];
        } else {
            $this->baseline_data = [];
        }
    }
    
    private function saveBaseline() {
        file_put_contents($this->baseline_file, json_encode($this->baseline_data, JSON_PRETTY_PRINT));
    }
}

// 使用例
$monitor = new FileChangeMonitor();

// 初回:ベースライン作成
$file_count = $monitor->createBaseline('/var/www/html');
echo "ベースライン作成完了: {$file_count}個のファイルを記録\n\n";

// 後日:変更をチェック
$changes = $monitor->checkChanges('/var/www/html');
echo $monitor->generateChangeReport($changes);

パフォーマンス考慮事項

大量ファイル処理の最適化

class OptimizedFileScanner {
    private $batch_size = 1000;
    
    public function scanLargeDirectory($directory, $callback) {
        $batch = [];
        $count = 0;
        
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($directory),
            RecursiveIteratorIterator::LEAVES_ONLY
        );
        
        foreach ($iterator as $item) {
            if ($item->isDot()) {
                continue;
            }
            
            $filepath = $item->getPathname();
            $info = lstat($filepath);
            
            if ($info !== false) {
                $batch[] = ['path' => $filepath, 'info' => $info];
                $count++;
                
                if (count($batch) >= $this->batch_size) {
                    call_user_func($callback, $batch);
                    $batch = [];
                    
                    // メモリ使用量を抑制
                    if ($count % 10000 === 0) {
                        gc_collect_cycles();
                    }
                }
            }
        }
        
        // 残りのバッチを処理
        if (!empty($batch)) {
            call_user_func($callback, $batch);
        }
        
        return $count;
    }
}

// 使用例
$scanner = new OptimizedFileScanner();
$total_size = 0;

$scanner->scanLargeDirectory('/var/log', function($batch) use (&$total_size) {
    foreach ($batch as $item) {
        $total_size += $item['info']['size'];
    }
    echo "処理中... 累計サイズ: " . number_format($total_size) . " bytes\n";
});

エラーハンドリング

安全なlstat実装

function safe_lstat($filepath, $suppress_errors = false) {
    try {
        // パスの正規化
        $filepath = realpath($filepath);
        if ($filepath === false) {
            throw new InvalidArgumentException("無効なパス");
        }
        
        // エラー報告レベルを一時的に変更
        $old_error_reporting = error_reporting($suppress_errors ? 0 : E_ALL);
        
        $result = lstat($filepath);
        
        // エラー報告レベルを復元
        error_reporting($old_error_reporting);
        
        if ($result === false) {
            throw new RuntimeException("lstat failed for: {$filepath}");
        }
        
        return $result;
        
    } catch (Exception $e) {
        if (!$suppress_errors) {
            error_log("lstat error: " . $e->getMessage());
        }
        return false;
    }
}

// 使用例
$info = safe_lstat('/some/file.txt', true);
if ($info !== false) {
    echo "ファイルサイズ: " . $info['size'] . " bytes\n";
} else {
    echo "ファイル情報を取得できませんでした\n";
}

まとめ

lstat関数は、PHPでファイルシステムの詳細情報を取得する重要な関数です。

主要なポイント:

  • シンボリックリンク自体の情報を取得(statはリンク先)
  • ファイルタイプ、サイズ、タイムスタンプなどの詳細情報
  • ファイルシステム分析やセキュリティ監視に活用
  • 大量ファイル処理時はメモリ使用量に注意

実用的な用途:

  • ファイル変更監視システム
  • シンボリックリンクの管理
  • ディスク使用量の分析
  • セキュリティ監査

適切に活用することで、ファイルシステムの詳細な分析や監視が可能になります。

タイトルとURLをコピーしました