はじめに
PHPのlstat
関数は、ファイルやディレクトリの詳細な情報を取得するための関数です。似たような機能を持つstat
関数との大きな違いは、シンボリックリンクの扱い方にあります。
今回は、lstat
関数の基本的な使い方から、実際の開発現場での活用方法まで詳しく解説します。
lstat関数とは
基本構文
array|false lstat(string $filename)
パラメータ:
$filename
:情報を取得したいファイルまたはディレクトリのパス
戻り値:
- 成功時:ファイル情報を含む配列
- 失敗時:
false
基本的な使用例
$file_info = lstat('/path/to/file.txt');
if ($file_info !== false) {
print_r($file_info);
} else {
echo "ファイル情報の取得に失敗しました\n";
}
lstatとstatの違い
重要な違い:シンボリックリンクの扱い
stat
関数:シンボリックリンクのリンク先の情報を取得lstat
関数:シンボリックリンク自体の情報を取得
// シンボリックリンクの作成例(Linux/Unix環境)
// ln -s /var/log/access.log /tmp/loglink
$original_file = '/var/log/access.log';
$symlink = '/tmp/loglink';
// stat:リンク先の情報
$stat_info = stat($symlink);
echo "stat - ファイルサイズ: " . $stat_info['size'] . " bytes\n";
// lstat:リンク自体の情報
$lstat_info = lstat($symlink);
echo "lstat - リンクサイズ: " . $lstat_info['size'] . " bytes\n";
// シンボリックリンクかどうかの判定
if (($lstat_info['mode'] & 0170000) === 0120000) {
echo "これはシンボリックリンクです\n";
}
実用的な比較関数
function compareStatAndLstat($filepath) {
if (!file_exists($filepath)) {
return "ファイルが存在しません: {$filepath}";
}
$stat_info = stat($filepath);
$lstat_info = lstat($filepath);
echo "=== ファイル: {$filepath} ===\n";
// シンボリックリンクかどうかの判定
$is_symlink = is_link($filepath);
echo "シンボリックリンク: " . ($is_symlink ? "はい" : "いいえ") . "\n";
echo "\nstat() の結果:\n";
echo " サイズ: {$stat_info['size']} bytes\n";
echo " 最終更新: " . date('Y-m-d H:i:s', $stat_info['mtime']) . "\n";
echo "\nlstat() の結果:\n";
echo " サイズ: {$lstat_info['size']} bytes\n";
echo " 最終更新: " . date('Y-m-d H:i:s', $lstat_info['mtime']) . "\n";
if ($is_symlink) {
echo "\nリンク先: " . readlink($filepath) . "\n";
echo "注意: stat()はリンク先、lstat()はリンク自体の情報です\n";
}
echo "\n" . str_repeat("-", 50) . "\n\n";
}
戻り値の詳細解説
配列のキーと意味
function explainLstatResult($filepath) {
$info = lstat($filepath);
if ($info === false) {
echo "ファイル情報を取得できませんでした\n";
return;
}
$explanations = [
'dev' => 'デバイス番号',
'ino' => 'inode番号',
'mode' => 'アクセス許可とファイルタイプ',
'nlink' => 'ハードリンク数',
'uid' => 'ユーザーID(所有者)',
'gid' => 'グループID',
'rdev' => '特殊ファイルの場合のデバイスタイプ',
'size' => 'ファイルサイズ(bytes)',
'atime' => '最終アクセス時刻',
'mtime' => '最終更新時刻',
'ctime' => '最終inode変更時刻',
'blksize' => 'ファイルシステムのブロックサイズ',
'blocks' => '使用ブロック数'
];
echo "=== ファイル情報: {$filepath} ===\n";
foreach ($explanations as $key => $description) {
$value = $info[$key] ?? 'N/A';
// 特別な表示処理
if (in_array($key, ['atime', 'mtime', 'ctime']) && is_numeric($value)) {
$formatted_time = date('Y-m-d H:i:s', $value);
echo sprintf("%-10s: %-20s (%s)\n", $key, $value, $formatted_time);
} elseif ($key === 'mode') {
$permissions = substr(sprintf('%o', $value), -4);
$type = $this->getFileTypeFromMode($value);
echo sprintf("%-10s: %-20s (%s, %s)\n", $key, $value, $permissions, $type);
} elseif ($key === 'size') {
$readable_size = $this->formatBytes($value);
echo sprintf("%-10s: %-20s (%s)\n", $key, $value, $readable_size);
} else {
echo sprintf("%-10s: %s\n", $key, $value);
}
}
}
// ヘルパー関数
function getFileTypeFromMode($mode) {
switch ($mode & 0170000) {
case 0140000: return 'ソケット';
case 0120000: return 'シンボリックリンク';
case 0100000: return '通常ファイル';
case 0060000: return 'ブロックデバイス';
case 0040000: return 'ディレクトリ';
case 0020000: return 'キャラクターデバイス';
case 0010000: return 'FIFO';
default: return '不明';
}
}
function formatBytes($bytes) {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . ' ' . $units[$pow];
}
実用的な応用例
1. ファイルシステム分析ツール
class FileSystemAnalyzer {
private $stats = [];
public function analyzeDirectory($directory, $recursive = false) {
if (!is_dir($directory)) {
throw new InvalidArgumentException("指定されたパスはディレクトリではありません: {$directory}");
}
$this->stats = [
'files' => 0,
'directories' => 0,
'symlinks' => 0,
'total_size' => 0,
'largest_file' => ['name' => '', 'size' => 0],
'oldest_file' => ['name' => '', 'mtime' => PHP_INT_MAX],
'newest_file' => ['name' => '', 'mtime' => 0]
];
$this->scanDirectory($directory, $recursive);
return $this->stats;
}
private function scanDirectory($directory, $recursive) {
$iterator = new DirectoryIterator($directory);
foreach ($iterator as $item) {
if ($item->isDot()) {
continue;
}
$filepath = $item->getPathname();
$info = lstat($filepath);
if ($info === false) {
continue;
}
// ファイルタイプの判定
if (is_link($filepath)) {
$this->stats['symlinks']++;
} elseif (is_dir($filepath)) {
$this->stats['directories']++;
if ($recursive) {
$this->scanDirectory($filepath, $recursive);
}
} else {
$this->stats['files']++;
$this->stats['total_size'] += $info['size'];
// 最大ファイル
if ($info['size'] > $this->stats['largest_file']['size']) {
$this->stats['largest_file'] = [
'name' => $filepath,
'size' => $info['size']
];
}
// 最古・最新ファイル
if ($info['mtime'] < $this->stats['oldest_file']['mtime']) {
$this->stats['oldest_file'] = [
'name' => $filepath,
'mtime' => $info['mtime']
];
}
if ($info['mtime'] > $this->stats['newest_file']['mtime']) {
$this->stats['newest_file'] = [
'name' => $filepath,
'mtime' => $info['mtime']
];
}
}
}
}
public function generateReport() {
$report = "=== ファイルシステム分析レポート ===\n";
$report .= "ファイル数: " . number_format($this->stats['files']) . "\n";
$report .= "ディレクトリ数: " . number_format($this->stats['directories']) . "\n";
$report .= "シンボリックリンク数: " . number_format($this->stats['symlinks']) . "\n";
$report .= "合計サイズ: " . $this->formatBytes($this->stats['total_size']) . "\n";
if (!empty($this->stats['largest_file']['name'])) {
$report .= "\n最大ファイル:\n";
$report .= " " . $this->stats['largest_file']['name'] . "\n";
$report .= " サイズ: " . $this->formatBytes($this->stats['largest_file']['size']) . "\n";
}
if (!empty($this->stats['oldest_file']['name'])) {
$report .= "\n最古ファイル:\n";
$report .= " " . $this->stats['oldest_file']['name'] . "\n";
$report .= " 更新日時: " . date('Y-m-d H:i:s', $this->stats['oldest_file']['mtime']) . "\n";
}
if (!empty($this->stats['newest_file']['name'])) {
$report .= "\n最新ファイル:\n";
$report .= " " . $this->stats['newest_file']['name'] . "\n";
$report .= " 更新日時: " . date('Y-m-d H:i:s', $this->stats['newest_file']['mtime']) . "\n";
}
return $report;
}
private function formatBytes($bytes) {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . ' ' . $units[$pow];
}
}
// 使用例
$analyzer = new FileSystemAnalyzer();
$stats = $analyzer->analyzeDirectory('/var/log', false);
echo $analyzer->generateReport();
2. シンボリックリンクのメンテナンスツール
class SymlinkManager {
public function findBrokenSymlinks($directory) {
$broken_links = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory)
);
foreach ($iterator as $item) {
if ($item->isDot()) {
continue;
}
$filepath = $item->getPathname();
// シンボリックリンクかどうか確認
if (is_link($filepath)) {
$link_target = readlink($filepath);
// リンク先が存在するかチェック
if (!file_exists($filepath)) { // file_existsはリンク先をチェック
$lstat_info = lstat($filepath);
$broken_links[] = [
'link' => $filepath,
'target' => $link_target,
'mtime' => $lstat_info['mtime']
];
}
}
}
return $broken_links;
}
public function analyzeSymlinks($directory) {
$symlinks = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory)
);
foreach ($iterator as $item) {
if ($item->isDot()) {
continue;
}
$filepath = $item->getPathname();
if (is_link($filepath)) {
$link_target = readlink($filepath);
$lstat_info = lstat($filepath);
$target_exists = file_exists($filepath);
$symlinks[] = [
'link' => $filepath,
'target' => $link_target,
'target_exists' => $target_exists,
'link_size' => $lstat_info['size'],
'created' => date('Y-m-d H:i:s', $lstat_info['ctime']),
'modified' => date('Y-m-d H:i:s', $lstat_info['mtime'])
];
}
}
return $symlinks;
}
public function generateSymlinkReport($directory) {
$symlinks = $this->analyzeSymlinks($directory);
$broken = array_filter($symlinks, function($link) {
return !$link['target_exists'];
});
$report = "=== シンボリックリンク分析レポート ===\n";
$report .= "検索ディレクトリ: {$directory}\n";
$report .= "シンボリックリンク総数: " . count($symlinks) . "\n";
$report .= "壊れたリンク数: " . count($broken) . "\n\n";
if (!empty($broken)) {
$report .= "=== 壊れたシンボリックリンク ===\n";
foreach ($broken as $link) {
$report .= "リンク: {$link['link']}\n";
$report .= " → {$link['target']} (存在しません)\n";
$report .= " 作成日時: {$link['created']}\n\n";
}
}
return $report;
}
}
// 使用例
$manager = new SymlinkManager();
echo $manager->generateSymlinkReport('/usr/local');
3. ファイル変更監視システム
class FileChangeMonitor {
private $baseline_file;
private $baseline_data;
public function __construct($baseline_file = 'file_baseline.json') {
$this->baseline_file = $baseline_file;
$this->loadBaseline();
}
public function createBaseline($directory, $recursive = true) {
$this->baseline_data = [];
$this->scanDirectory($directory, $recursive);
$this->saveBaseline();
return count($this->baseline_data);
}
private function scanDirectory($directory, $recursive) {
$iterator = $recursive
? new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory))
: new DirectoryIterator($directory);
foreach ($iterator as $item) {
if ($item->isDot()) {
continue;
}
$filepath = $item->getPathname();
$info = lstat($filepath);
if ($info !== false) {
$this->baseline_data[$filepath] = [
'size' => $info['size'],
'mtime' => $info['mtime'],
'mode' => $info['mode'],
'is_link' => is_link($filepath)
];
}
}
}
public function checkChanges($directory, $recursive = true) {
$current_data = [];
$this->scanDirectoryForCheck($directory, $recursive, $current_data);
$changes = [
'modified' => [],
'added' => [],
'deleted' => []
];
// 変更されたファイル
foreach ($current_data as $filepath => $current_info) {
if (isset($this->baseline_data[$filepath])) {
$baseline_info = $this->baseline_data[$filepath];
if ($current_info['mtime'] > $baseline_info['mtime'] ||
$current_info['size'] != $baseline_info['size']) {
$changes['modified'][] = [
'file' => $filepath,
'old_size' => $baseline_info['size'],
'new_size' => $current_info['size'],
'old_mtime' => $baseline_info['mtime'],
'new_mtime' => $current_info['mtime']
];
}
} else {
$changes['added'][] = $filepath;
}
}
// 削除されたファイル
foreach ($this->baseline_data as $filepath => $baseline_info) {
if (!isset($current_data[$filepath])) {
$changes['deleted'][] = $filepath;
}
}
return $changes;
}
private function scanDirectoryForCheck($directory, $recursive, &$current_data) {
$iterator = $recursive
? new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory))
: new DirectoryIterator($directory);
foreach ($iterator as $item) {
if ($item->isDot()) {
continue;
}
$filepath = $item->getPathname();
$info = lstat($filepath);
if ($info !== false) {
$current_data[$filepath] = [
'size' => $info['size'],
'mtime' => $info['mtime'],
'mode' => $info['mode'],
'is_link' => is_link($filepath)
];
}
}
}
public function generateChangeReport($changes) {
$report = "=== ファイル変更レポート ===\n";
$report .= "生成日時: " . date('Y-m-d H:i:s') . "\n\n";
if (!empty($changes['added'])) {
$report .= "新規ファイル (" . count($changes['added']) . "件):\n";
foreach ($changes['added'] as $file) {
$report .= " + {$file}\n";
}
$report .= "\n";
}
if (!empty($changes['modified'])) {
$report .= "変更されたファイル (" . count($changes['modified']) . "件):\n";
foreach ($changes['modified'] as $change) {
$report .= " * {$change['file']}\n";
$report .= " サイズ: {$change['old_size']} → {$change['new_size']}\n";
$report .= " 更新日時: " . date('Y-m-d H:i:s', $change['old_mtime']) .
" → " . date('Y-m-d H:i:s', $change['new_mtime']) . "\n";
}
$report .= "\n";
}
if (!empty($changes['deleted'])) {
$report .= "削除されたファイル (" . count($changes['deleted']) . "件):\n";
foreach ($changes['deleted'] as $file) {
$report .= " - {$file}\n";
}
$report .= "\n";
}
if (empty($changes['added']) && empty($changes['modified']) && empty($changes['deleted'])) {
$report .= "変更はありません。\n";
}
return $report;
}
private function loadBaseline() {
if (file_exists($this->baseline_file)) {
$this->baseline_data = json_decode(file_get_contents($this->baseline_file), true) ?? [];
} else {
$this->baseline_data = [];
}
}
private function saveBaseline() {
file_put_contents($this->baseline_file, json_encode($this->baseline_data, JSON_PRETTY_PRINT));
}
}
// 使用例
$monitor = new FileChangeMonitor();
// 初回:ベースライン作成
$file_count = $monitor->createBaseline('/var/www/html');
echo "ベースライン作成完了: {$file_count}個のファイルを記録\n\n";
// 後日:変更をチェック
$changes = $monitor->checkChanges('/var/www/html');
echo $monitor->generateChangeReport($changes);
パフォーマンス考慮事項
大量ファイル処理の最適化
class OptimizedFileScanner {
private $batch_size = 1000;
public function scanLargeDirectory($directory, $callback) {
$batch = [];
$count = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($iterator as $item) {
if ($item->isDot()) {
continue;
}
$filepath = $item->getPathname();
$info = lstat($filepath);
if ($info !== false) {
$batch[] = ['path' => $filepath, 'info' => $info];
$count++;
if (count($batch) >= $this->batch_size) {
call_user_func($callback, $batch);
$batch = [];
// メモリ使用量を抑制
if ($count % 10000 === 0) {
gc_collect_cycles();
}
}
}
}
// 残りのバッチを処理
if (!empty($batch)) {
call_user_func($callback, $batch);
}
return $count;
}
}
// 使用例
$scanner = new OptimizedFileScanner();
$total_size = 0;
$scanner->scanLargeDirectory('/var/log', function($batch) use (&$total_size) {
foreach ($batch as $item) {
$total_size += $item['info']['size'];
}
echo "処理中... 累計サイズ: " . number_format($total_size) . " bytes\n";
});
エラーハンドリング
安全なlstat実装
function safe_lstat($filepath, $suppress_errors = false) {
try {
// パスの正規化
$filepath = realpath($filepath);
if ($filepath === false) {
throw new InvalidArgumentException("無効なパス");
}
// エラー報告レベルを一時的に変更
$old_error_reporting = error_reporting($suppress_errors ? 0 : E_ALL);
$result = lstat($filepath);
// エラー報告レベルを復元
error_reporting($old_error_reporting);
if ($result === false) {
throw new RuntimeException("lstat failed for: {$filepath}");
}
return $result;
} catch (Exception $e) {
if (!$suppress_errors) {
error_log("lstat error: " . $e->getMessage());
}
return false;
}
}
// 使用例
$info = safe_lstat('/some/file.txt', true);
if ($info !== false) {
echo "ファイルサイズ: " . $info['size'] . " bytes\n";
} else {
echo "ファイル情報を取得できませんでした\n";
}
まとめ
lstat
関数は、PHPでファイルシステムの詳細情報を取得する重要な関数です。
主要なポイント:
- シンボリックリンク自体の情報を取得(
stat
はリンク先) - ファイルタイプ、サイズ、タイムスタンプなどの詳細情報
- ファイルシステム分析やセキュリティ監視に活用
- 大量ファイル処理時はメモリ使用量に注意
実用的な用途:
- ファイル変更監視システム
- シンボリックリンクの管理
- ディスク使用量の分析
- セキュリティ監査
適切に活用することで、ファイルシステムの詳細な分析や監視が可能になります。