[PHP]linkinfo関数完全マスター!ハードリンクのリンク数取得と活用方法

PHP

はじめに

ファイルシステムでハードリンクを管理する際、どのファイルがいくつのリンクを持っているかを知る必要がある場面はありませんか?PHPのlinkinfo関数は、ファイルのハードリンク数を取得するための専用関数です。

この記事では、linkinfo関数の基本的な使い方から実践的な活用例まで詳しく解説します。

linkinfo関数とは?

linkinfo関数は、指定されたファイルのハードリンク数(link count)を取得するPHP関数です。ファイルが持つハードリンクの総数を返し、ファイルシステムの管理やデバッグに役立ちます。

基本構文

linkinfo(string $path): int|false

パラメータ:

  • $path: ハードリンク数を調べたいファイルのパス

戻り値:

  • 成功時: ハードリンクの数(整数)
  • 失敗時: false

注意点:

  • シンボリックリンクには使用できません
  • ディレクトリでは正確な値が返されない場合があります
  • Windows環境では制限があります

基本的な使用例

1. シンプルなリンク数取得

<?php
// テスト用ファイルの作成
$originalFile = '/tmp/test_original.txt';
file_put_contents($originalFile, "テストファイルの内容\n");

echo "=== 基本的なlinkinfo使用例 ===\n";

// 元ファイルのリンク数
$linkCount = linkinfo($originalFile);
if ($linkCount !== false) {
    echo "'{$originalFile}' のリンク数: {$linkCount}\n";
} else {
    echo "linkinfo取得に失敗しました\n";
}

// ハードリンクを作成
$hardLink1 = '/tmp/test_hardlink1.txt';
$hardLink2 = '/tmp/test_hardlink2.txt';

if (link($originalFile, $hardLink1)) {
    echo "ハードリンク1を作成しました\n";
}

if (link($originalFile, $hardLink2)) {
    echo "ハードリンク2を作成しました\n";
}

// 各ファイルのリンク数を確認
$files = [$originalFile, $hardLink1, $hardLink2];

foreach ($files as $file) {
    $count = linkinfo($file);
    echo "'{$file}' のリンク数: {$count}\n";
}

// 比較用:stat関数でも同じ情報を取得可能
$statInfo = stat($originalFile);
echo "\nstat関数での確認: {$statInfo['nlink']}\n";
?>

2. エラーハンドリングを含む実用的な例

<?php
function getLinkCount($filePath) {
    // ファイルの存在確認
    if (!file_exists($filePath)) {
        return [
            'success' => false,
            'error' => 'ファイルが存在しません',
            'path' => $filePath
        ];
    }
    
    // シンボリックリンクの確認
    if (is_link($filePath)) {
        return [
            'success' => false,
            'error' => 'シンボリックリンクは対象外です',
            'path' => $filePath,
            'is_symlink' => true
        ];
    }
    
    // ディレクトリの確認
    if (is_dir($filePath)) {
        return [
            'success' => false,
            'error' => 'ディレクトリは対象外です',
            'path' => $filePath,
            'is_directory' => true
        ];
    }
    
    // linkinfo実行
    $linkCount = linkinfo($filePath);
    
    if ($linkCount === false) {
        return [
            'success' => false,
            'error' => 'linkinfo関数の実行に失敗しました',
            'path' => $filePath
        ];
    }
    
    return [
        'success' => true,
        'link_count' => $linkCount,
        'path' => $filePath,
        'is_multiple_links' => $linkCount > 1
    ];
}

// テストファイルの準備
$testFiles = [
    '/tmp/single_file.txt' => '単一ファイル',
    '/tmp/multi_link.txt' => 'マルチリンクファイル'
];

foreach ($testFiles as $file => $content) {
    file_put_contents($file, $content);
}

// マルチリンクファイルにハードリンクを追加
link('/tmp/multi_link.txt', '/tmp/multi_link_copy1.txt');
link('/tmp/multi_link.txt', '/tmp/multi_link_copy2.txt');

// シンボリックリンクも作成(テスト用)
symlink('/tmp/single_file.txt', '/tmp/symlink_test.txt');

// 各ファイルのテスト
$testPaths = [
    '/tmp/single_file.txt',
    '/tmp/multi_link.txt',
    '/tmp/symlink_test.txt',
    '/tmp/nonexistent.txt',
    '/tmp'  // ディレクトリ
];

echo "=== エラーハンドリング付きlinkinfo テスト ===\n";
foreach ($testPaths as $path) {
    $result = getLinkCount($path);
    
    echo "\nファイル: {$path}\n";
    if ($result['success']) {
        echo "  リンク数: {$result['link_count']}\n";
        echo "  複数リンク: " . ($result['is_multiple_links'] ? 'はい' : 'いいえ') . "\n";
    } else {
        echo "  エラー: {$result['error']}\n";
    }
}
?>

実践的な活用例

1. ファイル重複検出システム

<?php
class DuplicateFileDetector {
    private $basePath;
    private $results;
    
    public function __construct($basePath) {
        $this->basePath = rtrim($basePath, '/');
        $this->results = [];
    }
    
    public function findDuplicatesByHardLink($recursive = true) {
        $duplicates = [];
        $inodeMap = [];
        
        $iterator = $recursive ? 
            new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($this->basePath)
            ) : 
            new DirectoryIterator($this->basePath);
        
        foreach ($iterator as $file) {
            if (!$file->isFile() || $file->isLink()) {
                continue;
            }
            
            $filePath = $file->getPathname();
            $linkCount = linkinfo($filePath);
            
            // リンク数が1より大きい場合のみ処理
            if ($linkCount && $linkCount > 1) {
                $inode = stat($filePath)['ino'];
                
                if (!isset($inodeMap[$inode])) {
                    $inodeMap[$inode] = [];
                }
                
                $inodeMap[$inode][] = [
                    'path' => $filePath,
                    'link_count' => $linkCount,
                    'size' => $file->getSize(),
                    'modified' => $file->getMTime()
                ];
            }
        }
        
        // 複数パスを持つinodeのみを結果として返す
        foreach ($inodeMap as $inode => $files) {
            if (count($files) > 1) {
                $duplicates[$inode] = [
                    'inode' => $inode,
                    'link_count' => $files[0]['link_count'],
                    'size' => $files[0]['size'],
                    'files' => $files
                ];
            }
        }
        
        return $duplicates;
    }
    
    public function generateDuplicateReport($duplicates) {
        $report = [
            'total_duplicate_groups' => count($duplicates),
            'total_duplicate_files' => 0,
            'total_saved_space' => 0,
            'details' => []
        ];
        
        foreach ($duplicates as $inode => $group) {
            $fileCount = count($group['files']);
            $savedSpace = ($fileCount - 1) * $group['size'];
            
            $report['total_duplicate_files'] += $fileCount;
            $report['total_saved_space'] += $savedSpace;
            
            $report['details'][] = [
                'inode' => $inode,
                'file_count' => $fileCount,
                'link_count' => $group['link_count'],
                'size' => $group['size'],
                'saved_space' => $savedSpace,
                'files' => array_column($group['files'], 'path')
            ];
        }
        
        return $report;
    }
    
    public function optimizeStorage($duplicates, $strategy = 'keep_first') {
        $optimized = [];
        
        foreach ($duplicates as $group) {
            $files = $group['files'];
            
            switch ($strategy) {
                case 'keep_first':
                    $keepFile = $files[0]['path'];
                    break;
                case 'keep_shortest_path':
                    $keepFile = array_reduce($files, function($carry, $file) {
                        return !$carry || strlen($file['path']) < strlen($carry) ? $file['path'] : $carry;
                    });
                    break;
                case 'keep_newest':
                    $keepFile = array_reduce($files, function($carry, $file) {
                        return !$carry || $file['modified'] > $carry ? $file['path'] : $carry;
                    });
                    break;
                default:
                    $keepFile = $files[0]['path'];
            }
            
            $toRemove = [];
            foreach ($files as $file) {
                if ($file['path'] !== $keepFile) {
                    $toRemove[] = $file['path'];
                }
            }
            
            $optimized[] = [
                'keep' => $keepFile,
                'remove' => $toRemove,
                'saved_space' => (count($toRemove)) * $group['size']
            ];
        }
        
        return $optimized;
    }
}

// 使用例
$detector = new DuplicateFileDetector('/var/www/html/files');

// テスト用ファイルとハードリンクの作成
@mkdir('/var/www/html/files', 0755, true);

$testContent = str_repeat('テストデータ', 1000);
file_put_contents('/var/www/html/files/original1.txt', $testContent);
file_put_contents('/var/www/html/files/original2.txt', '別のファイル');

// ハードリンクの作成
link('/var/www/html/files/original1.txt', '/var/www/html/files/backup1.txt');
link('/var/www/html/files/original1.txt', '/var/www/html/files/copy1.txt');
link('/var/www/html/files/original2.txt', '/var/www/html/files/backup2.txt');

echo "=== ハードリンク重複検出 ===\n";

// 重複検出
$duplicates = $detector->findDuplicatesByHardLink();

if (empty($duplicates)) {
    echo "重複ファイルは見つかりませんでした。\n";
} else {
    // レポート生成
    $report = $detector->generateDuplicateReport($duplicates);
    
    echo "重複グループ数: {$report['total_duplicate_groups']}\n";
    echo "重複ファイル総数: {$report['total_duplicate_files']}\n";
    echo "節約容量: " . number_format($report['total_saved_space']) . " bytes\n\n";
    
    foreach ($report['details'] as $detail) {
        echo "inode {$detail['inode']}:\n";
        echo "  ファイル数: {$detail['file_count']}\n";
        echo "  リンク数: {$detail['link_count']}\n";
        echo "  サイズ: " . number_format($detail['size']) . " bytes\n";
        echo "  節約容量: " . number_format($detail['saved_space']) . " bytes\n";
        echo "  ファイル一覧:\n";
        foreach ($detail['files'] as $file) {
            echo "    - {$file}\n";
        }
        echo "\n";
    }
}
?>

2. ファイル完全性チェックシステム

<?php
class FileIntegrityChecker {
    private $expectedLinks;
    
    public function __construct() {
        $this->expectedLinks = [];
    }
    
    public function registerExpectedLink($filePath, $expectedLinkCount) {
        $this->expectedLinks[realpath($filePath)] = $expectedLinkCount;
    }
    
    public function checkIntegrity() {
        $results = [];
        
        foreach ($this->expectedLinks as $filePath => $expectedCount) {
            $result = $this->checkSingleFile($filePath, $expectedCount);
            $results[] = $result;
        }
        
        return $results;
    }
    
    private function checkSingleFile($filePath, $expectedCount) {
        $result = [
            'file' => $filePath,
            'expected_links' => $expectedCount,
            'actual_links' => null,
            'status' => 'unknown',
            'issues' => []
        ];
        
        // ファイル存在確認
        if (!file_exists($filePath)) {
            $result['status'] = 'missing';
            $result['issues'][] = 'ファイルが存在しません';
            return $result;
        }
        
        // シンボリックリンクの場合
        if (is_link($filePath)) {
            $result['status'] = 'symlink';
            $result['issues'][] = 'シンボリックリンクです(ハードリンクではありません)';
            return $result;
        }
        
        // リンク数取得
        $actualCount = linkinfo($filePath);
        
        if ($actualCount === false) {
            $result['status'] = 'error';
            $result['issues'][] = 'linkinfo関数の実行に失敗しました';
            return $result;
        }
        
        $result['actual_links'] = $actualCount;
        
        // リンク数の比較
        if ($actualCount === $expectedCount) {
            $result['status'] = 'ok';
        } elseif ($actualCount < $expectedCount) {
            $result['status'] = 'missing_links';
            $result['issues'][] = "リンクが不足しています(期待値: {$expectedCount}, 実際: {$actualCount})";
        } else {
            $result['status'] = 'extra_links';
            $result['issues'][] = "予期しないリンクがあります(期待値: {$expectedCount}, 実際: {$actualCount})";
        }
        
        return $result;
    }
    
    public function findAllLinksForFile($filePath) {
        if (!file_exists($filePath) || is_link($filePath)) {
            return [];
        }
        
        $inode = stat($filePath)['ino'];
        $filesystem = dirname($filePath);
        $links = [];
        
        // 同一ファイルシステム内を検索
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($filesystem)
        );
        
        foreach ($iterator as $file) {
            if ($file->isFile() && !$file->isLink()) {
                if (stat($file->getPathname())['ino'] === $inode) {
                    $links[] = $file->getPathname();
                }
            }
        }
        
        return $links;
    }
    
    public function generateIntegrityReport($results) {
        $summary = [
            'total_files' => count($results),
            'ok' => 0,
            'missing_links' => 0,
            'extra_links' => 0,
            'missing_files' => 0,
            'errors' => 0
        ];
        
        echo "=== ファイル完全性チェック結果 ===\n\n";
        
        foreach ($results as $result) {
            $summary[$result['status']]++;
            
            echo "ファイル: {$result['file']}\n";
            echo "ステータス: {$result['status']}\n";
            echo "期待リンク数: {$result['expected_links']}\n";
            echo "実際リンク数: " . ($result['actual_links'] ?? 'N/A') . "\n";
            
            if (!empty($result['issues'])) {
                echo "問題:\n";
                foreach ($result['issues'] as $issue) {
                    echo "  - {$issue}\n";
                }
            }
            
            // 修復提案
            if ($result['status'] === 'missing_links') {
                echo "修復提案: 不足しているハードリンクを作成してください\n";
            } elseif ($result['status'] === 'extra_links') {
                $allLinks = $this->findAllLinksForFile($result['file']);
                echo "関連リンク:\n";
                foreach ($allLinks as $link) {
                    echo "  - {$link}\n";
                }
            }
            
            echo "\n";
        }
        
        echo "=== サマリー ===\n";
        echo "総ファイル数: {$summary['total_files']}\n";
        echo "正常: {$summary['ok']}\n";
        echo "リンク不足: {$summary['missing_links']}\n";
        echo "余分なリンク: {$summary['extra_links']}\n";
        echo "ファイル不在: {$summary['missing_files']}\n";
        echo "エラー: {$summary['errors']}\n";
        
        return $summary;
    }
}

// 使用例
$checker = new FileIntegrityChecker();

// テストファイルの準備
$testFile1 = '/tmp/integrity_test1.txt';
$testFile2 = '/tmp/integrity_test2.txt';

file_put_contents($testFile1, 'テストファイル1');
file_put_contents($testFile2, 'テストファイル2');

// ハードリンクの作成
link($testFile1, '/tmp/integrity_test1_backup.txt');
link($testFile1, '/tmp/integrity_test1_copy.txt');
link($testFile2, '/tmp/integrity_test2_backup.txt');

// 期待値の登録
$checker->registerExpectedLink($testFile1, 3);  // 元ファイル + 2つのハードリンク
$checker->registerExpectedLink($testFile2, 2);  // 元ファイル + 1つのハードリンク
$checker->registerExpectedLink('/tmp/nonexistent.txt', 1);  // 存在しないファイル

// 完全性チェック実行
$results = $checker->checkIntegrity();
$checker->generateIntegrityReport($results);
?>

3. バックアップシステムの最適化

<?php
class HardLinkBackupOptimizer {
    private $backupPath;
    private $sourcePath;
    
    public function __construct($sourcePath, $backupPath) {
        $this->sourcePath = rtrim($sourcePath, '/');
        $this->backupPath = rtrim($backupPath, '/');
        
        if (!is_dir($this->backupPath)) {
            mkdir($this->backupPath, 0755, true);
        }
    }
    
    public function createIncrementalBackup($backupName) {
        $backupDir = $this->backupPath . '/' . $backupName;
        $previousBackup = $this->findLatestBackup();
        
        if (!mkdir($backupDir, 0755, true)) {
            throw new Exception("バックアップディレクトリの作成に失敗: {$backupDir}");
        }
        
        $stats = [
            'files_copied' => 0,
            'files_linked' => 0,
            'space_saved' => 0,
            'total_size' => 0
        ];
        
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($this->sourcePath)
        );
        
        foreach ($iterator as $file) {
            if (!$file->isFile()) continue;
            
            $relativePath = substr($file->getPathname(), strlen($this->sourcePath) + 1);
            $backupFilePath = $backupDir . '/' . $relativePath;
            $backupDirPath = dirname($backupFilePath);
            
            // バックアップディレクトリの作成
            if (!is_dir($backupDirPath)) {
                mkdir($backupDirPath, 0755, true);
            }
            
            $fileSize = $file->getSize();
            $stats['total_size'] += $fileSize;
            
            // 前回のバックアップに同一ファイルが存在するかチェック
            if ($previousBackup && $this->canUseHardLink($file->getPathname(), $previousBackup, $relativePath)) {
                $previousFile = $previousBackup . '/' . $relativePath;
                
                if (link($previousFile, $backupFilePath)) {
                    $stats['files_linked']++;
                    $stats['space_saved'] += $fileSize;
                    
                    // リンク数の確認
                    $linkCount = linkinfo($backupFilePath);
                    echo "ハードリンク作成: {$relativePath} (リンク数: {$linkCount})\n";
                } else {
                    // ハードリンクが失敗した場合はコピー
                    copy($file->getPathname(), $backupFilePath);
                    $stats['files_copied']++;
                }
            } else {
                // 新しいファイルはコピー
                copy($file->getPathname(), $backupFilePath);
                $stats['files_copied']++;
            }
        }
        
        // バックアップ統計の保存
        $this->saveBackupStats($backupName, $stats);
        
        return $stats;
    }
    
    private function canUseHardLink($sourceFile, $previousBackupDir, $relativePath) {
        $previousFile = $previousBackupDir . '/' . $relativePath;
        
        if (!file_exists($previousFile)) {
            return false;
        }
        
        // ファイルサイズの比較
        if (filesize($sourceFile) !== filesize($previousFile)) {
            return false;
        }
        
        // 更新日時の比較
        if (filemtime($sourceFile) !== filemtime($previousFile)) {
            return false;
        }
        
        // より厳密な比較が必要な場合はハッシュ値を比較
        return md5_file($sourceFile) === md5_file($previousFile);
    }
    
    private function findLatestBackup() {
        $backups = glob($this->backupPath . '/*', GLOB_ONLYDIR);
        
        if (empty($backups)) {
            return null;
        }
        
        // 最新のバックアップディレクトリを取得
        usort($backups, function($a, $b) {
            return filemtime($b) <=> filemtime($a);
        });
        
        return $backups[0];
    }
    
    private function saveBackupStats($backupName, $stats) {
        $statsFile = $this->backupPath . '/' . $backupName . '/backup_stats.json';
        
        $statsData = $stats;
        $statsData['backup_name'] = $backupName;
        $statsData['created_at'] = date('Y-m-d H:i:s');
        $statsData['compression_ratio'] = $stats['total_size'] > 0 ? 
            ($stats['space_saved'] / $stats['total_size']) * 100 : 0;
        
        file_put_contents($statsFile, json_encode($statsData, JSON_PRETTY_PRINT));
    }
    
    public function analyzeBackupEfficiency() {
        $backups = glob($this->backupPath . '/*/backup_stats.json');
        $analysis = [];
        
        foreach ($backups as $statsFile) {
            $stats = json_decode(file_get_contents($statsFile), true);
            $analysis[] = $stats;
        }
        
        // 日付順でソート
        usort($analysis, function($a, $b) {
            return strtotime($a['created_at']) <=> strtotime($b['created_at']);
        });
        
        return $analysis;
    }
    
    public function generateEfficiencyReport() {
        $analysis = $this->analyzeBackupEfficiency();
        
        if (empty($analysis)) {
            echo "バックアップ統計が見つかりません。\n";
            return;
        }
        
        echo "=== バックアップ効率性レポート ===\n\n";
        
        $totalSpaceSaved = 0;
        $totalSize = 0;
        
        foreach ($analysis as $stats) {
            echo "バックアップ: {$stats['backup_name']}\n";
            echo "作成日時: {$stats['created_at']}\n";
            echo "コピーファイル数: {$stats['files_copied']}\n";
            echo "リンクファイル数: {$stats['files_linked']}\n";
            echo "総サイズ: " . $this->formatBytes($stats['total_size']) . "\n";
            echo "節約容量: " . $this->formatBytes($stats['space_saved']) . "\n";
            echo "圧縮率: " . number_format($stats['compression_ratio'], 1) . "%\n\n";
            
            $totalSpaceSaved += $stats['space_saved'];
            $totalSize += $stats['total_size'];
        }
        
        echo "=== 総計 ===\n";
        echo "総節約容量: " . $this->formatBytes($totalSpaceSaved) . "\n";
        echo "総データサイズ: " . $this->formatBytes($totalSize) . "\n";
        echo "全体圧縮率: " . number_format(($totalSpaceSaved / $totalSize) * 100, 1) . "%\n";
    }
    
    private function formatBytes($bytes, $precision = 2) {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        
        for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
            $bytes /= 1024;
        }
        
        return round($bytes, $precision) . ' ' . $units[$i];
    }
}

// 使用例
$optimizer = new HardLinkBackupOptimizer('/var/www/html/source', '/var/backup/hardlink');

// テストディレクトリとファイルの準備
@mkdir('/var/www/html/source', 0755, true);
file_put_contents('/var/www/html/source/file1.txt', 'テストファイル1の内容');
file_put_contents('/var/www/html/source/file2.txt', 'テストファイル2の内容');
file_put_contents('/var/www/html/source/file3.txt', 'テストファイル3の内容');

echo "=== ハードリンク最適化バックアップ ===\n";

try {
    // 初回バックアップ
    echo "初回バックアップ実行中...\n";
    $stats1 = $optimizer->createIncrementalBackup('backup_' . date('Y-m-d_H-i-s'));
    
    sleep(2); // 時間差を作る
    
    // 一部ファイルを変更
    file_put_contents('/var/www/html/source/file1.txt', 'テストファイル1の内容(更新版)');
    file_put_contents('/var/www/html/source/file4.txt', '新しいファイル4');
    
    // 増分バックアップ
    echo "\n増分バックアップ実行中...\n";
    $stats2 = $optimizer->createIncrementalBackup('backup_' . date('Y-m-d_H-i-s'));
    
    // 効率性レポートの生成
    echo "\n";
    $optimizer->generateEfficiencyReport();
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

制限事項と対処法

1. プラットフォーム依存性の対処

<?php
class CrossPlatformLinkInfo {
    private $isWindows;
    
    public function __construct() {
        $this->isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    }
    
    public function getLinkCount($filePath) {
        if ($this->isWindows) {
            return $this->getWindowsLinkCount($filePath);
        } else {
            return $this->getUnixLinkCount($filePath);
        }
    }
    
    private function getUnixLinkCount($filePath) {
        // Unix系システムでは通常のlinkinfo関数を使用
        $count = linkinfo($filePath);
        
        if ($count === false) {
            // stat関数をフォールバックとして使用
            $stat = stat($filePath);
            return $stat ? $stat['nlink'] : false;
        }
        
        return $count;
    }
    
    private function getWindowsLinkCount($filePath) {
        // Windowsではlinkinfo関数が制限されているため、stat関数を使用
        $stat = stat($filePath);
        
        if ($stat === false) {
            return false;
        }
        
        // WindowsのNTFSでハードリンクが作成されている場合、nlink値が参照できる
        return $stat['nlink'];
    }
    
    public function isLinkInfoSupported() {
        if ($this->isWindows) {
            // Windowsでの制限事項を返す
            return [
                'supported' => false,
                'reason' => 'linkinfo関数はWindowsでは制限があります',
                'alternative' => 'stat関数のnlink値を使用してください'
            ];
        }
        
        return [
            'supported' => function_exists('linkinfo'),
            'reason' => function_exists('linkinfo') ? 'サポートされています' : '関数が無効です',
            'alternative' => 'stat関数のnlink値を使用できます'
        ];
    }
}

// 使用例
$crossPlatform = new CrossPlatformLinkInfo();

// サポート状況の確認
$support = $crossPlatform->isLinkInfoSupported();
echo "linkinfo サポート状況:\n";
print_r($support);

// テストファイルでのリンク数取得
$testFile = sys_get_temp_dir() . '/test_linkinfo.txt';
file_put_contents($testFile, 'テストデータ');

$linkCount = $crossPlatform->getLinkCount($testFile);
echo "\nリンク数: " . ($linkCount !== false ? $linkCount : 'エラー') . "\n";
?>

2. 大容量ファイルシステムでの最適化

<?php
class OptimizedLinkAnalyzer {
    private $cache;
    private $cacheSize;
    private $maxCacheSize;
    
    public function __construct($maxCacheSize = 10000) {
        $this->cache = [];
        $this->cacheSize = 0;
        $this->maxCacheSize = $maxCacheSize;
    }
    
    public function analyzeLinkUsage($directory, $options = []) {
        $defaultOptions = [
            'recursive' => true,
            'use_cache' => true,
            'batch_size' => 1000,
            'memory_limit' => 128 * 1024 * 1024, // 128MB
            'progress_callback' => null
        ];
        
        $options = array_merge($defaultOptions, $options);
        
        $results = [
            'total_files' => 0,
            'files_with_multiple_links' => 0,
            'total_links' => 0,
            'link_groups' => [],
            'memory_usage' => 0,
            'processing_time' => 0
        ];
        
        $startTime = microtime(true);
        $processed = 0;
        
        $iterator = $options['recursive'] ? 
            new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($directory)
            ) : 
            new DirectoryIterator($directory);
        
        $batch = [];
        
        foreach ($iterator as $file) {
            if (!$file->isFile() || $file->isLink()) {
                continue;
            }
            
            $batch[] = $file->getPathname();
            
            // バッチ処理
            if (count($batch) >= $options['batch_size']) {
                $this->processBatch($batch, $results, $options);
                $processed += count($batch);
                $batch = [];
                
                // メモリ使用量チェック
                if (memory_get_usage() > $options['memory_limit']) {
                    $this->clearCache();
                    gc_collect_cycles();
                }
                
                // プログレス報告
                if ($options['progress_callback'] && is_callable($options['progress_callback'])) {
                    $options['progress_callback']($processed);
                }
            }
        }
        
        // 残りのバッチを処理
        if (!empty($batch)) {
            $this->processBatch($batch, $results, $options);
            $processed += count($batch);
        }
        
        $results['memory_usage'] = memory_get_peak_usage(true);
        $results['processing_time'] = microtime(true) - $startTime;
        
        return $results;
    }
    
    private function processBatch($filePaths, &$results, $options) {
        foreach ($filePaths as $filePath) {
            $results['total_files']++;
            
            $linkCount = $this->getCachedLinkCount($filePath, $options['use_cache']);
            
            if ($linkCount > 1) {
                $results['files_with_multiple_links']++;
                $results['total_links'] += $linkCount;
                
                $inode = stat($filePath)['ino'];
                
                if (!isset($results['link_groups'][$inode])) {
                    $results['link_groups'][$inode] = [
                        'link_count' => $linkCount,
                        'files' => [],
                        'total_size' => 0
                    ];
                }
                
                $fileSize = filesize($filePath);
                $results['link_groups'][$inode]['files'][] = [
                    'path' => $filePath,
                    'size' => $fileSize
                ];
                $results['link_groups'][$inode]['total_size'] = $fileSize;
            }
        }
    }
    
    private function getCachedLinkCount($filePath, $useCache) {
        if (!$useCache) {
            return linkinfo($filePath) ?: 1;
        }
        
        $cacheKey = $filePath;
        
        if (isset($this->cache[$cacheKey])) {
            return $this->cache[$cacheKey];
        }
        
        $linkCount = linkinfo($filePath) ?: 1;
        
        // キャッシュサイズ制限
        if ($this->cacheSize >= $this->maxCacheSize) {
            $this->clearCache(true); // 部分クリア
        }
        
        $this->cache[$cacheKey] = $linkCount;
        $this->cacheSize++;
        
        return $linkCount;
    }
    
    private function clearCache($partial = false) {
        if ($partial) {
            // 半分のキャッシュをクリア
            $this->cache = array_slice($this->cache, $this->maxCacheSize / 2, null, true);
            $this->cacheSize = count($this->cache);
        } else {
            $this->cache = [];
            $this->cacheSize = 0;
        }
    }
    
    public function generateOptimizedReport($results) {
        echo "=== 最適化されたリンク分析レポート ===\n\n";
        echo "処理時間: " . number_format($results['processing_time'], 2) . " 秒\n";
        echo "メモリ使用量: " . $this->formatBytes($results['memory_usage']) . "\n\n";
        
        echo "総ファイル数: " . number_format($results['total_files']) . "\n";
        echo "複数リンクファイル数: " . number_format($results['files_with_multiple_links']) . "\n";
        echo "総リンク数: " . number_format($results['total_links']) . "\n";
        echo "リンクグループ数: " . count($results['link_groups']) . "\n\n";
        
        if (!empty($results['link_groups'])) {
            echo "=== トップ10 リンクグループ ===\n";
            
            // リンク数でソート
            $sortedGroups = $results['link_groups'];
            uasort($sortedGroups, function($a, $b) {
                return $b['link_count'] <=> $a['link_count'];
            });
            
            $count = 0;
            foreach ($sortedGroups as $inode => $group) {
                if (++$count > 10) break;
                
                echo "inode {$inode}:\n";
                echo "  リンク数: {$group['link_count']}\n";
                echo "  ファイルサイズ: " . $this->formatBytes($group['total_size']) . "\n";
                echo "  節約容量: " . $this->formatBytes(($group['link_count'] - 1) * $group['total_size']) . "\n";
                echo "  ファイル:\n";
                
                foreach ($group['files'] as $file) {
                    echo "    - {$file['path']}\n";
                }
                echo "\n";
            }
        }
    }
    
    private function formatBytes($bytes, $precision = 2) {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        
        for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
            $bytes /= 1024;
        }
        
        return round($bytes, $precision) . ' ' . $units[$i];
    }
}

// 使用例
$analyzer = new OptimizedLinkAnalyzer(5000);

echo "=== 大容量ファイルシステム分析 ===\n";

// プログレスコールバック
$progressCallback = function($processed) {
    echo "\r処理済み: " . number_format($processed) . " ファイル";
};

$results = $analyzer->analyzeLinkUsage('/var/www', [
    'recursive' => true,
    'use_cache' => true,
    'batch_size' => 500,
    'memory_limit' => 64 * 1024 * 1024,
    'progress_callback' => $progressCallback
]);

echo "\n\n";
$analyzer->generateOptimizedReport($results);
?>

3. 高度なデバッグ機能

<?php
class LinkInfoDebugger {
    private $debugLevel;
    private $logFile;
    
    public function __construct($debugLevel = 1, $logFile = null) {
        $this->debugLevel = $debugLevel;
        $this->logFile = $logFile;
    }
    
    public function debugLinkInfo($filePath) {
        $debug = [
            'file' => $filePath,
            'timestamp' => date('Y-m-d H:i:s'),
            'tests' => []
        ];
        
        // 基本情報
        $debug['tests']['file_exists'] = [
            'test' => 'ファイル存在確認',
            'result' => file_exists($filePath),
            'details' => file_exists($filePath) ? 'ファイルが存在します' : 'ファイルが存在しません'
        ];
        
        if (!file_exists($filePath)) {
            $this->logDebug($debug);
            return $debug;
        }
        
        // ファイルタイプ
        $debug['tests']['file_type'] = [
            'test' => 'ファイルタイプ確認',
            'result' => [
                'is_file' => is_file($filePath),
                'is_dir' => is_dir($filePath),
                'is_link' => is_link($filePath)
            ]
        ];
        
        // linkinfo関数のテスト
        $linkinfoResult = @linkinfo($filePath);
        $debug['tests']['linkinfo'] = [
            'test' => 'linkinfo関数テスト',
            'result' => $linkinfoResult,
            'success' => $linkinfoResult !== false,
            'error' => $linkinfoResult === false ? error_get_last() : null
        ];
        
        // stat関数での確認
        $statResult = @stat($filePath);
        $debug['tests']['stat'] = [
            'test' => 'stat関数でのnlink確認',
            'result' => $statResult ? $statResult['nlink'] : false,
            'success' => $statResult !== false,
            'full_stat' => $this->debugLevel >= 2 ? $statResult : null
        ];
        
        // lstat関数での確認(シンボリックリンク用)
        if (is_link($filePath)) {
            $lstatResult = @lstat($filePath);
            $debug['tests']['lstat'] = [
                'test' => 'lstat関数テスト(シンボリックリンク)',
                'result' => $lstatResult ? $lstatResult['nlink'] : false,
                'success' => $lstatResult !== false
            ];
        }
        
        // 結果の整合性チェック
        $debug['consistency_check'] = $this->checkConsistency($debug['tests']);
        
        // 推奨事項
        $debug['recommendations'] = $this->generateRecommendations($debug);
        
        $this->logDebug($debug);
        return $debug;
    }
    
    private function checkConsistency($tests) {
        $consistency = [
            'linkinfo_vs_stat' => null,
            'issues' => []
        ];
        
        if (isset($tests['linkinfo']) && isset($tests['stat'])) {
            $linkinfoValue = $tests['linkinfo']['result'];
            $statValue = $tests['stat']['result'];
            
            if ($linkinfoValue !== false && $statValue !== false) {
                $consistency['linkinfo_vs_stat'] = $linkinfoValue === $statValue;
                
                if ($linkinfoValue !== $statValue) {
                    $consistency['issues'][] = "linkinfo({$linkinfoValue})とstat({$statValue})の値が一致しません";
                }
            }
        }
        
        return $consistency;
    }
    
    private function generateRecommendations($debug) {
        $recommendations = [];
        
        // ファイルが存在しない場合
        if (!$debug['tests']['file_exists']['result']) {
            $recommendations[] = 'ファイルパスを確認してください';
            return $recommendations;
        }
        
        // シンボリックリンクの場合
        if ($debug['tests']['file_type']['result']['is_link']) {
            $recommendations[] = 'シンボリックリンクです。ハードリンク数を取得する場合は、リンク先ファイルを指定してください';
        }
        
        // ディレクトリの場合
        if ($debug['tests']['file_type']['result']['is_dir']) {
            $recommendations[] = 'ディレクトリです。ディレクトリのリンク数は通常のファイルとは異なる意味を持ちます';
        }
        
        // linkinfo関数が失敗した場合
        if (isset($debug['tests']['linkinfo']) && !$debug['tests']['linkinfo']['success']) {
            $recommendations[] = 'linkinfo関数が失敗しました。stat関数のnlink値を代替として使用してください';
        }
        
        // 整合性の問題がある場合
        if (!empty($debug['consistency_check']['issues'])) {
            $recommendations[] = '値の不整合が検出されました。システム管理者に確認してください';
        }
        
        return $recommendations;
    }
    
    private function logDebug($debug) {
        if (!$this->logFile) {
            return;
        }
        
        $logEntry = [
            'timestamp' => $debug['timestamp'],
            'file' => $debug['file'],
            'debug_data' => $debug
        ];
        
        file_put_contents(
            $this->logFile, 
            json_encode($logEntry) . "\n", 
            FILE_APPEND | LOCK_EX
        );
    }
    
    public function generateDebugReport($debug) {
        echo "=== linkinfo デバッグレポート ===\n";
        echo "ファイル: {$debug['file']}\n";
        echo "実行時刻: {$debug['timestamp']}\n\n";
        
        foreach ($debug['tests'] as $testName => $test) {
            echo "テスト: {$test['test']}\n";
            
            if (is_array($test['result'])) {
                foreach ($test['result'] as $key => $value) {
                    echo "  {$key}: " . ($value ? 'true' : 'false') . "\n";
                }
            } else {
                echo "  結果: " . ($test['result'] !== false ? $test['result'] : 'false') . "\n";
            }
            
            if (isset($test['success'])) {
                echo "  成功: " . ($test['success'] ? 'はい' : 'いいえ') . "\n";
            }
            
            if (isset($test['error']) && $test['error']) {
                echo "  エラー: {$test['error']['message']}\n";
            }
            
            echo "\n";
        }
        
        // 整合性チェック結果
        if (!empty($debug['consistency_check']['issues'])) {
            echo "=== 整合性の問題 ===\n";
            foreach ($debug['consistency_check']['issues'] as $issue) {
                echo "- {$issue}\n";
            }
            echo "\n";
        }
        
        // 推奨事項
        if (!empty($debug['recommendations'])) {
            echo "=== 推奨事項 ===\n";
            foreach ($debug['recommendations'] as $rec) {
                echo "- {$rec}\n";
            }
            echo "\n";
        }
    }
    
    public function runBatchDebug($filePaths) {
        $results = [];
        
        foreach ($filePaths as $filePath) {
            $results[] = $this->debugLinkInfo($filePath);
        }
        
        return $results;
    }
}

// 使用例
$debugger = new LinkInfoDebugger(2, '/tmp/linkinfo_debug.log');

// テストファイルの準備
$testFiles = [
    '/tmp/normal_file.txt',
    '/tmp/hardlinked_file.txt',
    '/tmp/symlink_file.txt',
    '/tmp/nonexistent.txt'
];

// 通常のファイル
file_put_contents('/tmp/normal_file.txt', 'ノーマルファイル');

// ハードリンクファイル
file_put_contents('/tmp/hardlinked_file.txt', 'ハードリンクファイル');
link('/tmp/hardlinked_file.txt', '/tmp/hardlinked_copy.txt');

// シンボリックリンク
symlink('/tmp/normal_file.txt', '/tmp/symlink_file.txt');

echo "=== linkinfo バッチデバッグ ===\n";

$results = $debugger->runBatchDebug($testFiles);

foreach ($results as $result) {
    $debugger->generateDebugReport($result);
}
?>

まとめ

PHPのlinkinfo関数は、ファイルシステムのハードリンク管理において重要な役割を果たします。単純にリンク数を取得するだけでなく、システム最適化、データ整合性チェック、効率的なバックアップシステムの構築など、様々な場面で活用できます。

重要なポイント

  1. 基本機能の理解
    • ハードリンク数の正確な取得
    • シンボリックリンクとの違いを理解
    • stat関数との関係性
  2. 実践的な応用
    • 重複ファイル検出システム
    • ファイル完全性チェック
    • 効率的なバックアップ最適化
  3. 制限事項への対処
    • プラットフォーム依存性の考慮
    • Windows環境での代替手段
    • 大容量ファイルシステムでの最適化
  4. パフォーマンス最適化
    • キャッシュ機能の実装
    • バッチ処理による効率化
    • メモリ使用量の管理
  5. デバッグとトラブルシューティング
    • 包括的な診断機能
    • 整合性チェック
    • 詳細なエラー情報の提供

これらの機能を適切に活用することで、ファイルシステムを効率的に管理し、ストレージの最適化やデータの整合性保証を実現できます。特に大規模なWebアプリケーションやバックアップシステムにおいて、linkinfo関数の理解と活用は重要なスキルとなります。

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