はじめに
ファイルシステムでのリンク機能を活用することで、効率的なファイル管理や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アプリケーションの構築において重要な役割を果たします。
重要なポイント
- ハードリンクとシンボリックリンクの適切な使い分け
- ハードリンク: 同一ファイルシステム内での確実なリンク
- シンボリックリンク: 柔軟性の高いリンク、ディレクトリにも対応
- セキュリティ対策
- パストラバーサル攻撃の防止
- 許可されたディレクトリ外へのリンク制限
- 適切な権限設定
- パフォーマンス最適化
- 大量処理時のバッチ処理とメモリ管理
- 効率的な検索とスキャン機能
- 実用的な応用
- 動的ダウンロードリンクシステム
- バックアップとアーカイブ管理
- CDN連携とファイル配信
- 保守性とデバッグ
- 壊れたリンクの自動検出と修復
- 包括的な診断ツール
- ログとモニタリング機能
これらの機能を適切に活用することで、スケーラブルで保守性の高いファイル管理システムを構築できます。特に大規模なWebアプリケーションやコンテンツ管理システムにおいて、パフォーマンスとセキュリティの両面で大きなメリットを提供します。