[PHP]link・symlink関数完全マスター!ハードリンク・シンボリックリンク作成の実践ガイド

PHP

はじめに

ファイルシステムでのリンク機能を活用することで、効率的なファイル管理やWebアプリケーションの最適化が可能になります。PHPではlink関数とsymlink関数を使用してハードリンクとシンボリックリンクを作成できます。

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

link関数とsymlink関数の概要

ハードリンクとシンボリックリンクの違い

ハードリンク(link関数):

  • 同一ファイルシステム内でのみ作成可能
  • 元ファイルと同じinode番号を持つ
  • 元ファイルが削除されても、リンク先は残存
  • ディレクトリには作成不可

シンボリックリンク(symlink関数):

  • ファイルシステムを跨いで作成可能
  • 元ファイルへのパスを保存
  • 元ファイルが削除されるとリンク切れが発生
  • ディレクトリにも作成可能

基本構文

// ハードリンクの作成
link(string $target, string $link): bool

// シンボリックリンクの作成
symlink(string $target, string $link): bool

link関数(ハードリンク)の使用方法

1. 基本的なハードリンクの作成

<?php
// ハードリンクの基本例
$targetFile = '/var/www/html/original.txt';
$linkFile = '/var/www/html/hardlink.txt';

// テスト用ファイルの作成
file_put_contents($targetFile, "これは元ファイルです。\n");

// ハードリンクの作成
if (link($targetFile, $linkFile)) {
    echo "ハードリンクの作成に成功しました。\n";
    
    // ファイル情報の確認
    $targetStat = stat($targetFile);
    $linkStat = stat($linkFile);
    
    echo "元ファイルのinode: " . $targetStat['ino'] . "\n";
    echo "リンクファイルのinode: " . $linkStat['ino'] . "\n";
    echo "同じinode?: " . ($targetStat['ino'] === $linkStat['ino'] ? 'はい' : 'いいえ') . "\n";
    
} else {
    echo "ハードリンクの作成に失敗しました。\n";
    echo "エラー: " . error_get_last()['message'] . "\n";
}

// リンクカウントの確認
$linkCount = stat($targetFile)['nlink'];
echo "リンクカウント: {$linkCount}\n";
?>

2. ハードリンク管理クラス

<?php
class HardLinkManager {
    private $basePath;
    
    public function __construct($basePath) {
        $this->basePath = rtrim($basePath, '/');
        
        if (!is_dir($this->basePath)) {
            throw new InvalidArgumentException("ベースパスが存在しません: {$this->basePath}");
        }
    }
    
    public function createHardLink($target, $linkName) {
        $fullTarget = $this->basePath . '/' . $target;
        $fullLink = $this->basePath . '/' . $linkName;
        
        // ターゲットファイルの存在確認
        if (!file_exists($fullTarget)) {
            throw new InvalidArgumentException("ターゲットファイルが存在しません: {$fullTarget}");
        }
        
        // ディレクトリの場合はエラー
        if (is_dir($fullTarget)) {
            throw new InvalidArgumentException("ディレクトリにはハードリンクを作成できません");
        }
        
        // 既存リンクの確認
        if (file_exists($fullLink)) {
            throw new InvalidArgumentException("リンクファイルが既に存在します: {$fullLink}");
        }
        
        if (link($fullTarget, $fullLink)) {
            return [
                'success' => true,
                'message' => 'ハードリンク作成成功',
                'target' => $fullTarget,
                'link' => $fullLink,
                'inode' => stat($fullTarget)['ino']
            ];
        } else {
            return [
                'success' => false,
                'message' => 'ハードリンク作成失敗: ' . error_get_last()['message'],
                'target' => $fullTarget,
                'link' => $fullLink
            ];
        }
    }
    
    public function getHardLinks($targetFile) {
        $fullTarget = $this->basePath . '/' . $targetFile;
        
        if (!file_exists($fullTarget)) {
            throw new InvalidArgumentException("ファイルが存在しません: {$fullTarget}");
        }
        
        $targetInode = stat($fullTarget)['ino'];
        $links = [];
        
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($this->basePath)
        );
        
        foreach ($iterator as $file) {
            if ($file->isFile() && stat($file->getPathname())['ino'] === $targetInode) {
                $links[] = $file->getPathname();
            }
        }
        
        return $links;
    }
    
    public function removeLink($linkName) {
        $fullLink = $this->basePath . '/' . $linkName;
        
        if (!file_exists($fullLink)) {
            return ['success' => false, 'message' => 'リンクファイルが存在しません'];
        }
        
        $linkCount = stat($fullLink)['nlink'];
        
        if (unlink($fullLink)) {
            return [
                'success' => true,
                'message' => 'リンク削除成功',
                'remaining_links' => $linkCount - 1
            ];
        } else {
            return [
                'success' => false,
                'message' => 'リンク削除失敗: ' . error_get_last()['message']
            ];
        }
    }
}

// 使用例
try {
    $linkManager = new HardLinkManager('/var/www/html/files');
    
    // テストファイルの作成
    file_put_contents('/var/www/html/files/document.txt', 'テストドキュメント');
    
    // ハードリンクの作成
    $result1 = $linkManager->createHardLink('document.txt', 'backup1.txt');
    $result2 = $linkManager->createHardLink('document.txt', 'backup2.txt');
    
    print_r($result1);
    print_r($result2);
    
    // 関連するハードリンクの取得
    $links = $linkManager->getHardLinks('document.txt');
    echo "関連ハードリンク:\n";
    foreach ($links as $link) {
        echo "- {$link}\n";
    }
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

symlink関数(シンボリックリンク)の使用方法

1. 基本的なシンボリックリンクの作成

<?php
// シンボリックリンクの基本例
$targetFile = '/var/www/html/target.txt';
$symlinkFile = '/var/www/html/symlink.txt';

// テスト用ファイルの作成
file_put_contents($targetFile, "シンボリックリンクのテストファイル\n");

// シンボリックリンクの作成
if (symlink($targetFile, $symlinkFile)) {
    echo "シンボリックリンクの作成に成功しました。\n";
    
    // リンクの詳細情報
    echo "リンク先: " . readlink($symlinkFile) . "\n";
    echo "リンクの種類: " . (is_link($symlinkFile) ? 'シンボリックリンク' : 'その他') . "\n";
    echo "実際のパス: " . realpath($symlinkFile) . "\n";
    
} else {
    echo "シンボリックリンクの作成に失敗しました。\n";
    echo "エラー: " . error_get_last()['message'] . "\n";
}

// 相対パスでのシンボリックリンク
$relativePath = '../documents/readme.txt';
$relativeLink = '/var/www/html/readme_link.txt';

if (symlink($relativePath, $relativeLink)) {
    echo "相対パスでのシンボリックリンク作成成功\n";
}
?>

2. シンボリックリンク管理システム

<?php
class SymbolicLinkManager {
    private $basePath;
    private $allowExternalLinks;
    
    public function __construct($basePath, $allowExternalLinks = false) {
        $this->basePath = realpath($basePath);
        $this->allowExternalLinks = $allowExternalLinks;
        
        if (!$this->basePath || !is_dir($this->basePath)) {
            throw new InvalidArgumentException("無効なベースパス: {$basePath}");
        }
    }
    
    public function createSymlink($target, $linkName, $relative = true) {
        $fullLink = $this->basePath . '/' . $linkName;
        
        // セキュリティチェック
        if (!$this->isSecurePath($linkName)) {
            throw new InvalidArgumentException("不正なリンク名: {$linkName}");
        }
        
        // 既存リンクの確認
        if (file_exists($fullLink) || is_link($fullLink)) {
            throw new InvalidArgumentException("リンクが既に存在します: {$fullLink}");
        }
        
        // ターゲットパスの処理
        if ($relative && !$this->isAbsolutePath($target)) {
            $finalTarget = $target;
        } else if ($relative && $this->isAbsolutePath($target)) {
            $finalTarget = $this->makeRelativePath($target, dirname($fullLink));
        } else {
            $finalTarget = $this->isAbsolutePath($target) ? $target : $this->basePath . '/' . $target;
        }
        
        // 外部リンクのチェック
        if (!$this->allowExternalLinks && $this->isAbsolutePath($finalTarget)) {
            $realTarget = realpath($finalTarget);
            if ($realTarget && strpos($realTarget, $this->basePath) !== 0) {
                throw new InvalidArgumentException("外部リンクは許可されていません");
            }
        }
        
        if (symlink($finalTarget, $fullLink)) {
            return [
                'success' => true,
                'message' => 'シンボリックリンク作成成功',
                'link' => $fullLink,
                'target' => $finalTarget,
                'absolute_target' => realpath($finalTarget) ?: $finalTarget,
                'is_valid' => file_exists($fullLink)
            ];
        } else {
            return [
                'success' => false,
                'message' => 'シンボリックリンク作成失敗: ' . error_get_last()['message']
            ];
        }
    }
    
    public function updateSymlink($linkName, $newTarget) {
        $fullLink = $this->basePath . '/' . $linkName;
        
        if (!is_link($fullLink)) {
            throw new InvalidArgumentException("シンボリックリンクが存在しません: {$fullLink}");
        }
        
        // 古いリンクを削除
        if (unlink($fullLink)) {
            return $this->createSymlink($newTarget, $linkName);
        } else {
            return [
                'success' => false,
                'message' => '既存リンクの削除に失敗'
            ];
        }
    }
    
    public function getSymlinkInfo($linkName) {
        $fullLink = $this->basePath . '/' . $linkName;
        
        if (!is_link($fullLink)) {
            return ['exists' => false, 'message' => 'シンボリックリンクが存在しません'];
        }
        
        $target = readlink($fullLink);
        $absoluteTarget = realpath($fullLink);
        
        return [
            'exists' => true,
            'link' => $fullLink,
            'target' => $target,
            'absolute_target' => $absoluteTarget,
            'is_valid' => $absoluteTarget !== false,
            'is_broken' => !file_exists($fullLink),
            'target_type' => $this->getTargetType($fullLink),
            'link_stat' => lstat($fullLink),
            'target_stat' => $absoluteTarget ? stat($absoluteTarget) : null
        ];
    }
    
    public function findBrokenSymlinks($recursive = false) {
        $brokenLinks = [];
        
        if ($recursive) {
            $iterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($this->basePath)
            );
        } else {
            $iterator = new DirectoryIterator($this->basePath);
        }
        
        foreach ($iterator as $file) {
            if ($file->isLink()) {
                $linkPath = $file->getPathname();
                if (!file_exists($linkPath)) {
                    $brokenLinks[] = [
                        'link' => $linkPath,
                        'target' => readlink($linkPath),
                        'relative_link' => str_replace($this->basePath . '/', '', $linkPath)
                    ];
                }
            }
        }
        
        return $brokenLinks;
    }
    
    public function cleanupBrokenSymlinks($recursive = false) {
        $brokenLinks = $this->findBrokenSymlinks($recursive);
        $cleaned = [];
        
        foreach ($brokenLinks as $link) {
            if (unlink($link['link'])) {
                $cleaned[] = $link;
            }
        }
        
        return [
            'found' => count($brokenLinks),
            'cleaned' => count($cleaned),
            'cleaned_links' => $cleaned
        ];
    }
    
    private function isSecurePath($path) {
        // ディレクトリトラバーサル攻撃の防止
        return strpos($path, '..') === false && strpos($path, '/') !== 0;
    }
    
    private function isAbsolutePath($path) {
        return substr($path, 0, 1) === '/';
    }
    
    private function makeRelativePath($target, $base) {
        $target = realpath($target);
        $base = realpath($base);
        
        if ($target === false || $base === false) {
            return $target;
        }
        
        $targetParts = explode('/', $target);
        $baseParts = explode('/', $base);
        
        // 共通部分を除去
        while (count($targetParts) && count($baseParts) && $targetParts[0] === $baseParts[0]) {
            array_shift($targetParts);
            array_shift($baseParts);
        }
        
        // ../ を追加
        $relative = str_repeat('../', count($baseParts)) . implode('/', $targetParts);
        
        return $relative;
    }
    
    private function getTargetType($link) {
        if (!file_exists($link)) {
            return 'broken';
        }
        
        if (is_dir($link)) {
            return 'directory';
        } else if (is_file($link)) {
            return 'file';
        }
        
        return 'unknown';
    }
}

// 使用例
try {
    $symlinkManager = new SymbolicLinkManager('/var/www/html/links', true);
    
    // ディレクトリの作成
    @mkdir('/var/www/html/links', 0755, true);
    @mkdir('/var/www/html/documents', 0755, true);
    
    // テストファイルの作成
    file_put_contents('/var/www/html/documents/test.txt', 'テストファイル');
    file_put_contents('/var/www/html/documents/readme.md', '# README');
    
    // シンボリックリンクの作成
    $result1 = $symlinkManager->createSymlink('../documents/test.txt', 'test_link.txt');
    $result2 = $symlinkManager->createSymlink('/var/www/html/documents/readme.md', 'readme_link.md', false);
    
    print_r($result1);
    print_r($result2);
    
    // リンク情報の取得
    $info = $symlinkManager->getSymlinkInfo('test_link.txt');
    echo "リンク情報:\n";
    print_r($info);
    
    // 壊れたリンクの検索(テスト用に壊れたリンクを作成)
    symlink('/nonexistent/file.txt', '/var/www/html/links/broken_link.txt');
    
    $brokenLinks = $symlinkManager->findBrokenSymlinks();
    echo "壊れたリンク:\n";
    print_r($brokenLinks);
    
    // クリーンアップ
    $cleanupResult = $symlinkManager->cleanupBrokenSymlinks();
    echo "クリーンアップ結果:\n";
    print_r($cleanupResult);
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

3. Webアプリケーションでの動的リンク管理

<?php
class WebSymlinkManager {
    private $uploadPath;
    private $publicPath;
    private $linkPrefix;
    
    public function __construct($uploadPath, $publicPath, $linkPrefix = 'dl_') {
        $this->uploadPath = rtrim($uploadPath, '/');
        $this->publicPath = rtrim($publicPath, '/');
        $this->linkPrefix = $linkPrefix;
        
        // ディレクトリの作成
        if (!is_dir($this->publicPath)) {
            mkdir($this->publicPath, 0755, true);
        }
    }
    
    public function createDownloadLink($filePath, $expirationMinutes = 60) {
        $fullPath = $this->uploadPath . '/' . $filePath;
        
        if (!file_exists($fullPath)) {
            throw new InvalidArgumentException("ファイルが存在しません: {$fullPath}");
        }
        
        // 一意なリンク名の生成
        $linkName = $this->linkPrefix . uniqid() . '_' . basename($filePath);
        $linkPath = $this->publicPath . '/' . $linkName;
        
        // シンボリックリンクの作成
        if (symlink($fullPath, $linkPath)) {
            // 有効期限の設定
            $expirationTime = time() + ($expirationMinutes * 60);
            
            // メタデータファイルの作成
            $metaFile = $linkPath . '.meta';
            $metadata = [
                'original_file' => $fullPath,
                'created' => time(),
                'expires' => $expirationTime,
                'downloads' => 0,
                'max_downloads' => null
            ];
            
            file_put_contents($metaFile, json_encode($metadata));
            
            return [
                'success' => true,
                'link_name' => $linkName,
                'link_path' => $linkPath,
                'url' => $this->getLinkURL($linkName),
                'expires' => date('Y-m-d H:i:s', $expirationTime)
            ];
        }
        
        return ['success' => false, 'message' => 'リンク作成失敗'];
    }
    
    public function validateDownloadLink($linkName) {
        $linkPath = $this->publicPath . '/' . $linkName;
        $metaFile = $linkPath . '.meta';
        
        // リンクの存在確認
        if (!is_link($linkPath)) {
            return ['valid' => false, 'reason' => 'link_not_found'];
        }
        
        // メタデータの確認
        if (!file_exists($metaFile)) {
            return ['valid' => false, 'reason' => 'metadata_missing'];
        }
        
        $metadata = json_decode(file_get_contents($metaFile), true);
        
        // 有効期限の確認
        if (time() > $metadata['expires']) {
            $this->removeDownloadLink($linkName);
            return ['valid' => false, 'reason' => 'expired'];
        }
        
        // ダウンロード回数の確認
        if ($metadata['max_downloads'] && $metadata['downloads'] >= $metadata['max_downloads']) {
            $this->removeDownloadLink($linkName);
            return ['valid' => false, 'reason' => 'download_limit_exceeded'];
        }
        
        // ターゲットファイルの存在確認
        if (!file_exists($linkPath)) {
            $this->removeDownloadLink($linkName);
            return ['valid' => false, 'reason' => 'target_file_missing'];
        }
        
        return [
            'valid' => true,
            'metadata' => $metadata,
            'file_path' => realpath($linkPath)
        ];
    }
    
    public function recordDownload($linkName) {
        $metaFile = $this->publicPath . '/' . $linkName . '.meta';
        
        if (file_exists($metaFile)) {
            $metadata = json_decode(file_get_contents($metaFile), true);
            $metadata['downloads']++;
            $metadata['last_download'] = time();
            
            file_put_contents($metaFile, json_encode($metadata));
            
            return $metadata['downloads'];
        }
        
        return false;
    }
    
    public function removeDownloadLink($linkName) {
        $linkPath = $this->publicPath . '/' . $linkName;
        $metaFile = $linkPath . '.meta';
        
        $result = ['link_removed' => false, 'meta_removed' => false];
        
        if (is_link($linkPath)) {
            $result['link_removed'] = unlink($linkPath);
        }
        
        if (file_exists($metaFile)) {
            $result['meta_removed'] = unlink($metaFile);
        }
        
        return $result;
    }
    
    public function cleanupExpiredLinks() {
        $cleaned = [];
        $currentTime = time();
        
        $files = glob($this->publicPath . '/' . $this->linkPrefix . '*.meta');
        
        foreach ($files as $metaFile) {
            $metadata = json_decode(file_get_contents($metaFile), true);
            
            if ($metadata && $currentTime > $metadata['expires']) {
                $linkName = str_replace('.meta', '', basename($metaFile));
                $this->removeDownloadLink($linkName);
                $cleaned[] = $linkName;
            }
        }
        
        return $cleaned;
    }
    
    private function getLinkURL($linkName) {
        // 実際の環境では適切なベースURLを設定
        return 'https://example.com/downloads/' . $linkName;
    }
}

// 使用例(Webアプリケーション)
try {
    $webLinkManager = new WebSymlinkManager(
        '/var/www/private/uploads',
        '/var/www/public/downloads'
    );
    
    // プライベートファイルのテスト作成
    @mkdir('/var/www/private/uploads', 0755, true);
    file_put_contents('/var/www/private/uploads/document.pdf', 'PDF内容のサンプル');
    
    // 一時的なダウンロードリンクの作成(1時間有効)
    $linkResult = $webLinkManager->createDownloadLink('document.pdf', 60);
    
    if ($linkResult['success']) {
        echo "ダウンロードリンクが作成されました:\n";
        echo "URL: " . $linkResult['url'] . "\n";
        echo "有効期限: " . $linkResult['expires'] . "\n";
        
        // リンクの検証
        $validation = $webLinkManager->validateDownloadLink($linkResult['link_name']);
        
        if ($validation['valid']) {
            echo "リンクは有効です。\n";
            
            // ダウンロード記録
            $downloadCount = $webLinkManager->recordDownload($linkResult['link_name']);
            echo "ダウンロード回数: {$downloadCount}\n";
        }
    }
    
    // 期限切れリンクのクリーンアップ
    $cleanedLinks = $webLinkManager->cleanupExpiredLinks();
    echo "クリーンアップされたリンク数: " . count($cleanedLinks) . "\n";
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

セキュリティとベストプラクティス

1. セキュアなリンク管理

<?php
class SecureLinkManager {
    private $basePath;
    private $allowedTargets;
    
    public function __construct($basePath, $allowedTargets = []) {
        $this->basePath = realpath($basePath);
        $this->allowedTargets = array_map('realpath', $allowedTargets);
    }
    
    public function createSecureSymlink($target, $linkName) {
        // パストラバーサル攻撃の防止
        if (strpos($linkName, '..') !== false || strpos($linkName, '/') === 0) {
            throw new SecurityException("不正なリンク名: {$linkName}");
        }
        
        $fullTarget = realpath($target);
        $fullLink = $this->basePath . '/' . basename($linkName);
        
        // ターゲットが許可されたディレクトリ内にあるかチェック
        if (!$this->isTargetAllowed($fullTarget)) {
            throw new SecurityException("許可されていないターゲット: {$target}");
        }
        
        // 既存ファイルの上書き防止
        if (file_exists($fullLink)) {
            throw new InvalidArgumentException("ファイルが既に存在します");
        }
        
        // シンボリックリンクの作成
        if (symlink($fullTarget, $fullLink)) {
            // 権限の設定
            chmod($fullLink, 0644);
            
            return [
                'success' => true,
                'link' => $fullLink,
                'target' => $fullTarget
            ];
        }
        
        return ['success' => false, 'message' => 'リンク作成失敗'];
    }
    
    private function isTargetAllowed($target) {
        if (empty($this->allowedTargets)) {
            return true;
        }
        
        foreach ($this->allowedTargets as $allowed) {
            if (strpos($target, $allowed) === 0) {
                return true;
            }
        }
        
        return false;
    }
}

class SecurityException extends Exception {}
?>

2. プラットフォーム対応

<?php
class CrossPlatformLinkManager {
    private $isWindows;
    
    public function __construct() {
        $this->isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    }
    
    public function createLink($target, $link, $type = 'symbolic') {
        if ($this->isWindows) {
            return $this->createWindowsLink($target, $link, $type);
        } else {
            return $this->createUnixLink($target, $link, $type);
        }
    }
    
    private function createWindowsLink($target, $link, $type) {
        if ($type === 'symbolic') {
            // Windows でのシンボリックリンク
            symlink()関数はWindowsプラットフォームでは動作しませんが、
            // Windows 10以降では管理者権限があれば作成可能
            if (function_exists('symlink')) {
                return symlink($target, $link);
            } else {
                // 代替手段: コピーまたはショートカット
                return copy($target, $link);
            }
        } else {
            // Windows でのハードリンク
            return link($target, $link);
        }
    }
    
    private function createUnixLink($target, $link, $type) {
        if ($type === 'symbolic') {
            return symlink($target, $link);
        } else {
            return link($target, $link);
        }
    }
    
    public function isLinkSupported($type = 'symbolic') {
        if ($this->isWindows && $type === 'symbolic') {
            return function_exists('symlink') && $this->hasSymlinkPermission();
        }
        
        return true;
    }
    
    private function hasSymlinkPermission() {
        // Windows での権限チェック(簡易版)
        if ($this->isWindows) {
            $testTarget = sys_get_temp_dir() . '/test_target.txt';
            $testLink = sys_get_temp_dir() . '/test_symlink.txt';
            
            file_put_contents($testTarget, 'test');
            
            $canCreate = @symlink($testTarget, $testLink);
            
            // クリーンアップ
            @unlink($testLink);
            @unlink($testTarget);
            
            return $canCreate;
        }
        
        return true;
    }
}
?>

パフォーマンスとリソース管理

1. 大量リンク処理の最適化

<?php
class BatchLinkManager {
    private $basePath;
    private $maxBatchSize;
    
    public function __construct($basePath, $maxBatchSize = 1000) {
        $this->basePath = $basePath;
        $this->maxBatchSize = $maxBatchSize;
    }
    
    public function createBatchSymlinks($linkData, $callback = null) {
        $results = [];
        $processed = 0;
        $errors = 0;
        
        foreach (array_chunk($linkData, $this->maxBatchSize) as $batch) {
            foreach ($batch as $data) {
                try {
                    $target = $data['target'];
                    $link = $this->basePath . '/' . $data['link'];
                    
                    if (symlink($target, $link)) {
                        $results[] = [
                            'success' => true,
                            'target' => $target,
                            'link' => $link
                        ];
                        $processed++;
                    } else {
                        $results[] = [
                            'success' => false,
                            'target' => $target,
                            'link' => $link,
                            'error' => error_get_last()['message']
                        ];
                        $errors++;
                    }
                    
                    // コールバック実行
                    if ($callback && is_callable($callback)) {
                        $callback($processed + $errors, count($linkData));
                    }
                    
                } catch (Exception $e) {
                    $results[] = [
                        'success' => false,
                        'target' => $data['target'] ?? 'unknown',
                        'link' => $data['link'] ?? 'unknown',
                        'error' => $e->getMessage()
                    ];
                    $errors++;
                }
            }
            
            // メモリ使用量をチェック
            if (memory_get_usage() > memory_get_usage(true) * 0.8) {
                gc_collect_cycles();
            }
        }
        
        return [
            'total' => count($linkData),
            'processed' => $processed,
            'errors' => $errors,
            'results' => $results
        ];
    }
    
    public function verifyBatchLinks($links, $removeInvalid = false) {
        $valid = [];
        $invalid = [];
        $removed = [];
        
        foreach ($links as $link) {
            $fullPath = $this->basePath . '/' . $link;
            
            if (is_link($fullPath)) {
                if (file_exists($fullPath)) {
                    $valid[] = $link;
                } else {
                    $invalid[] = $link;
                    
                    if ($removeInvalid && unlink($fullPath)) {
                        $removed[] = $link;
                    }
                }
            }
        }
        
        return [
            'valid' => $valid,
            'invalid' => $invalid,
            'removed' => $removed,
            'summary' => [
                'total' => count($links),
                'valid_count' => count($valid),
                'invalid_count' => count($invalid),
                'removed_count' => count($removed)
            ]
        ];
    }
}

// 使用例
$batchManager = new BatchLinkManager('/var/www/html/batch_links');

// 大量リンクデータの準備
$linkData = [];
for ($i = 1; $i <= 5000; $i++) {
    $linkData[] = [
        'target' => "/var/data/file_{$i}.txt",
        'link' => "batch_link_{$i}.txt"
    ];
}

// プログレスバー付きバッチ処理
$result = $batchManager->createBatchSymlinks($linkData, function($processed, $total) {
    $percent = round(($processed / $total) * 100, 1);
    echo "\rProgress: {$processed}/{$total} ({$percent}%)";
});

echo "\n\nバッチ処理完了:\n";
echo "総数: {$result['total']}\n";
echo "成功: {$result['processed']}\n";
echo "エラー: {$result['errors']}\n";
?>

2. メモリ効率的なリンク検索

<?php
class EfficientLinkScanner {
    private $basePath;
    
    public function __construct($basePath) {
        $this->basePath = $basePath;
    }
    
    public function findSymlinks($pattern = '*', $recursive = true) {
        $symlinks = [];
        
        if ($recursive) {
            $iterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($this->basePath),
                RecursiveIteratorIterator::LEAVES_ONLY
            );
        } else {
            $iterator = new DirectoryIterator($this->basePath);
        }
        
        foreach ($iterator as $file) {
            if ($file->isLink()) {
                $filename = $file->getFilename();
                
                if (fnmatch($pattern, $filename)) {
                    yield [
                        'path' => $file->getPathname(),
                        'target' => readlink($file->getPathname()),
                        'valid' => file_exists($file->getPathname()),
                        'size' => $file->getSize(),
                        'modified' => $file->getMTime()
                    ];
                }
            }
        }
    }
    
    public function analyzeLinkUsage() {
        $stats = [
            'total_symlinks' => 0,
            'valid_symlinks' => 0,
            'broken_symlinks' => 0,
            'target_types' => [
                'file' => 0,
                'directory' => 0,
                'broken' => 0
            ],
            'total_size' => 0
        ];
        
        foreach ($this->findSymlinks() as $link) {
            $stats['total_symlinks']++;
            
            if ($link['valid']) {
                $stats['valid_symlinks']++;
                $stats['total_size'] += $link['size'];
                
                if (is_file($link['path'])) {
                    $stats['target_types']['file']++;
                } elseif (is_dir($link['path'])) {
                    $stats['target_types']['directory']++;
                }
            } else {
                $stats['broken_symlinks']++;
                $stats['target_types']['broken']++;
            }
        }
        
        return $stats;
    }
}

// 使用例
$scanner = new EfficientLinkScanner('/var/www/html');

echo "シンボリックリンクの検索:\n";
$count = 0;
foreach ($scanner->findSymlinks('*.txt') as $link) {
    echo "- {$link['path']} -> {$link['target']} " . 
         ($link['valid'] ? '[有効]' : '[無効]') . "\n";
    $count++;
    
    // 大量データの場合、メモリ使用量を制限
    if ($count >= 100) {
        echo "... (100件で制限)\n";
        break;
    }
}

// 統計情報
$stats = $scanner->analyzeLinkUsage();
echo "\n統計情報:\n";
print_r($stats);
?>

トラブルシューティングとデバッグ

1. リンク診断ツール

<?php
class LinkDiagnosticTool {
    public static function diagnoseLink($linkPath) {
        $diagnosis = [
            'path' => $linkPath,
            'exists' => file_exists($linkPath),
            'is_link' => is_link($linkPath),
            'is_readable' => is_readable($linkPath),
            'is_writable' => is_writable($linkPath),
            'issues' => [],
            'recommendations' => []
        ];
        
        if (!file_exists($linkPath)) {
            $diagnosis['issues'][] = 'ファイルまたはリンクが存在しません';
            $diagnosis['recommendations'][] = 'パスが正しいか確認してください';
            return $diagnosis;
        }
        
        if (is_link($linkPath)) {
            $target = readlink($linkPath);
            $diagnosis['target'] = $target;
            $diagnosis['target_exists'] = file_exists($target);
            $diagnosis['target_readable'] = is_readable($target);
            
            if (!$diagnosis['target_exists']) {
                $diagnosis['issues'][] = 'リンク先が存在しません: ' . $target;
                $diagnosis['recommendations'][] = 'リンク先ファイルを作成するか、リンクを更新してください';
            }
            
            // 循環参照のチェック
            if (self::hasCircularReference($linkPath)) {
                $diagnosis['issues'][] = '循環参照が検出されました';
                $diagnosis['recommendations'][] = 'リンクチェーンを確認し、循環を解消してください';
            }
            
        } else {
            $diagnosis['type'] = is_file($linkPath) ? 'file' : (is_dir($linkPath) ? 'directory' : 'unknown');
            
            if (is_file($linkPath)) {
                $diagnosis['size'] = filesize($linkPath);
                $diagnosis['mime_type'] = mime_content_type($linkPath);
            }
        }
        
        // 権限の詳細分析
        $perms = fileperms($linkPath);
        $diagnosis['permissions'] = [
            'octal' => substr(sprintf('%o', $perms), -4),
            'owner' => [
                'read' => ($perms & 0x0100) ? true : false,
                'write' => ($perms & 0x0080) ? true : false,
                'execute' => ($perms & 0x0040) ? true : false
            ],
            'group' => [
                'read' => ($perms & 0x0020) ? true : false,
                'write' => ($perms & 0x0010) ? true : false,
                'execute' => ($perms & 0x0008) ? true : false
            ],
            'other' => [
                'read' => ($perms & 0x0004) ? true : false,
                'write' => ($perms & 0x0002) ? true : false,
                'execute' => ($perms & 0x0001) ? true : false
            ]
        ];
        
        return $diagnosis;
    }
    
    private static function hasCircularReference($linkPath, $visited = []) {
        if (in_array($linkPath, $visited)) {
            return true;
        }
        
        if (!is_link($linkPath)) {
            return false;
        }
        
        $target = readlink($linkPath);
        if (!$target) {
            return false;
        }
        
        // 相対パスの場合は絶対パスに変換
        if (!self::isAbsolutePath($target)) {
            $target = dirname($linkPath) . '/' . $target;
        }
        
        $visited[] = $linkPath;
        
        return self::hasCircularReference($target, $visited);
    }
    
    private static function isAbsolutePath($path) {
        return substr($path, 0, 1) === '/';
    }
    
    public static function generateReport($linkPath) {
        $diagnosis = self::diagnoseLink($linkPath);
        
        echo "=== リンク診断レポート ===\n";
        echo "パス: {$diagnosis['path']}\n";
        echo "存在: " . ($diagnosis['exists'] ? 'はい' : 'いいえ') . "\n";
        echo "リンク: " . ($diagnosis['is_link'] ? 'はい' : 'いいえ') . "\n";
        
        if ($diagnosis['is_link']) {
            echo "リンク先: {$diagnosis['target']}\n";
            echo "リンク先存在: " . ($diagnosis['target_exists'] ? 'はい' : 'いいえ') . "\n";
        }
        
        echo "読み取り可能: " . ($diagnosis['is_readable'] ? 'はい' : 'いいえ') . "\n";
        echo "書き込み可能: " . ($diagnosis['is_writable'] ? 'はい' : 'いいえ') . "\n";
        echo "権限: {$diagnosis['permissions']['octal']}\n";
        
        if (!empty($diagnosis['issues'])) {
            echo "\n=== 問題点 ===\n";
            foreach ($diagnosis['issues'] as $issue) {
                echo "- {$issue}\n";
            }
        }
        
        if (!empty($diagnosis['recommendations'])) {
            echo "\n=== 推奨事項 ===\n";
            foreach ($diagnosis['recommendations'] as $rec) {
                echo "- {$rec}\n";
            }
        }
        
        echo "\n";
    }
}

// 使用例
LinkDiagnosticTool::generateReport('/var/www/html/test_link.txt');
?>

まとめ

PHPのlink関数とsymlink関数は、効率的なファイル管理システムやWebアプリケーションの構築において重要な役割を果たします。

重要なポイント

  1. ハードリンクとシンボリックリンクの適切な使い分け
    • ハードリンク: 同一ファイルシステム内での確実なリンク
    • シンボリックリンク: 柔軟性の高いリンク、ディレクトリにも対応
  2. セキュリティ対策
    • パストラバーサル攻撃の防止
    • 許可されたディレクトリ外へのリンク制限
    • 適切な権限設定
  3. パフォーマンス最適化
    • 大量処理時のバッチ処理とメモリ管理
    • 効率的な検索とスキャン機能
  4. 実用的な応用
    • 動的ダウンロードリンクシステム
    • バックアップとアーカイブ管理
    • CDN連携とファイル配信
  5. 保守性とデバッグ
    • 壊れたリンクの自動検出と修復
    • 包括的な診断ツール
    • ログとモニタリング機能

これらの機能を適切に活用することで、スケーラブルで保守性の高いファイル管理システムを構築できます。特に大規模なWebアプリケーションやコンテンツ管理システムにおいて、パフォーマンスとセキュリティの両面で大きなメリットを提供します。

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