1. はじめに
Windows(だけでなく他の環境でも)では、各ファイルやディレクトリに一連のプロパティ(メタデータ)がある:フルパス、名前、拡張子、サイズ、作成/変更/アクセス日時、属性など。想像してみて:ファイルは単なるバイトの集まりじゃなくて、FileInfoやDirectoryInfoで読める1つの「プロフィール」みたいなものだよ。
| プロパティ | 説明 | 例 |
|---|---|---|
| フルパス | ファイル/フォルダのフルネーム | |
| 名前 | パスを除いた名前 | |
| 拡張子 | .txt, .csv, .jpg など | |
| サイズ (Bytes) | ファイルのバイト数 | |
| 作成日時 | ファイル/フォルダが作成された日時 | |
| 変更日時 | 最後に内容が変更された日時 | |
| 属性 | 例えば、読み取り専用、隠しなど | |
| 親フォルダ | ファイル/ディレクトリが属するフォルダ | |
2. ファイルプロパティの詳しい扱い
タイムスタンプの詳細解析
プロパティのCreationTime、LastWriteTime、LastAccessTimeはDateTimeを返し、その挙動はファイルシステムやファイル操作によって変わるよ。
var fileInfo = new FileInfo("document.txt");
if (fileInfo.Exists)
{
Console.WriteLine($"作成日時: {fileInfo.CreationTime:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"変更日時: {fileInfo.LastWriteTime:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"アクセス日時: {fileInfo.LastAccessTime:yyyy-MM-dd HH:mm:ss}");
// 作成と最終変更の差分
var age = fileInfo.LastWriteTime - fileInfo.CreationTime;
Console.WriteLine($"ファイルは {age.TotalDays:F1} 日の間に変更されました");
}
興味深いことに、ファイルをコピーすると作成日時は通常現在の日時に更新される一方、変更日時は元のまま残ることがある。バックアップやアクティビティ解析では重要な点だよ。
拡張子とファイル名の扱い
プロパティのFullName、Name、Extensionは単純に見えて細かい注意点がある:拡張子が無いファイル、.tar.gzのような複合拡張子、先頭がドットの隠しファイルなど。
var files = new[]
{
new FileInfo("document.txt"),
new FileInfo("archive.tar.gz"),
new FileInfo("README"),
new FileInfo(".gitignore")
};
foreach (var file in files)
{
Console.WriteLine($"フルネーム: {file.FullName}");
Console.WriteLine($"名前: {file.Name}");
Console.WriteLine($"拡張子: '{file.Extension}'");
// 拡張子を除いた名前
string nameWithoutExtension = Path.GetFileNameWithoutExtension(file.Name);
Console.WriteLine($"拡張子を除いた名前: {nameWithoutExtension}");
Console.WriteLine("---");
}
ファイルサイズとフォーマット
プロパティのLengthはバイト単位のサイズを返す。ユーザーにはKB/MB/GBの方が見やすいね。補助関数の例:
static string FormatFileSize(long bytes)
{
string[] suffixes = { "Б", "КБ", "МБ", "ГБ", "ТБ" };
int counter = 0;
decimal number = bytes;
while (Math.Round(number / 1024) >= 1)
{
number /= 1024;
counter++;
}
return $"{number:N1} {suffixes[counter]}";
}
// 使用例
var file = new FileInfo("bigfile.zip");
if (file.Exists)
{
Console.WriteLine($"ファイルサイズ: {FormatFileSize(file.Length)}");
}
3. ファイル属性の高度な扱い
属性はビットフラグの列挙型FileAttributesで表されるので、ファイルは複数の属性を同時に持てる。チェックにはHasFlagが便利だよ。
var fileInfo = new FileInfo("important.txt");
Console.WriteLine($"ファイル属性: {fileInfo.Attributes}");
// 特定の属性をチェック
if (fileInfo.Attributes.HasFlag(FileAttributes.Hidden))
{
Console.WriteLine("ファイルは隠しです!");
}
if (fileInfo.Attributes.HasFlag(FileAttributes.ReadOnly))
{
Console.WriteLine("ファイルは書き込み禁止です!");
}
if (fileInfo.Attributes.HasFlag(FileAttributes.System))
{
Console.WriteLine("これはシステムファイルです!");
}
属性はプログラムから変更できるよ:
// ファイルを隠しにする
fileInfo.Attributes |= FileAttributes.Hidden;
// "読み取り専用" 属性を外す
fileInfo.Attributes &= ~FileAttributes.ReadOnly;
// 複数の属性を一度に設定する
fileInfo.Attributes = FileAttributes.ReadOnly | FileAttributes.Hidden;
4. ディレクトリの高度な操作
パターンでファイルを検索
メソッドのGetFiles()とGetDirectories()はパターンを受け取って内容をフィルタできるよ。
var dir = new DirectoryInfo(@"C:\Projects");
if (dir.Exists)
{
// すべてのテキストファイルを見つける
var textFiles = dir.GetFiles("*.txt");
Console.WriteLine($"見つかったテキストファイル: {textFiles.Length}");
// "temp"で始まるファイル
var tempFiles = dir.GetFiles("temp*");
// 画像ファイルをすべて集める
var imageExtensions = new[] { "*.jpg", "*.png", "*.gif", "*.bmp" };
var allImages = imageExtensions.SelectMany(ext => dir.GetFiles(ext)).ToArray();
Console.WriteLine($"見つかった画像: {allImages.Length}");
}
サブフォルダを再帰的に検索
すべてのサブフォルダを走査するにはSearchOption.AllDirectoriesを使う。
var dir = new DirectoryInfo(@"C:\Development");
// すべてのサブフォルダから .cs ファイルを探す
var csharpFiles = dir.GetFiles("*.cs", SearchOption.AllDirectories);
Console.WriteLine($".cs ファイルの合計: {csharpFiles.Length}");
// 最初の10ファイルをパス付きで表示
foreach (var file in csharpFiles.Take(10))
{
Console.WriteLine($"{file.FullName} ({FormatFileSize(file.Length)})");
}
ディレクトリ内容の解析
要約解析の例:ファイル/フォルダの数、合計サイズ、拡張子ごとの分布、上位5つの大きいファイル。
static void AnalyzeDirectory(DirectoryInfo dir)
{
if (!dir.Exists)
{
Console.WriteLine("ディレクトリが存在しません!");
return;
}
var files = dir.GetFiles();
var subdirs = dir.GetDirectories();
Console.WriteLine($"ディレクトリ解析: {dir.FullName}");
Console.WriteLine($"ファイル数: {files.Length}, サブディレクトリ数: {subdirs.Length}");
if (files.Length == 0)
{
Console.WriteLine("ファイルが見つかりません。");
return;
}
long totalSize = files.Sum(f => f.Length);
Console.WriteLine($"ファイルの合計サイズ: {FormatFileSize(totalSize)}");
// 拡張子ごとのグループ化
var byExtension = files.GroupBy(f => f.Extension.ToLower())
.OrderByDescending(g => g.Sum(f => f.Length));
Console.WriteLine("\nファイルタイプ別分布:");
foreach (var group in byExtension)
{
string ext = string.IsNullOrEmpty(group.Key) ? "(拡張子なし)" : group.Key;
long groupSize = group.Sum(f => f.Length);
Console.WriteLine($" {ext}: {group.Count()} 個, {FormatFileSize(groupSize)}");
}
// 上位5つの大きいファイル
var largestFiles = files.OrderByDescending(f => f.Length).Take(5);
Console.WriteLine("\nもっとも大きいファイル:");
foreach (var file in largestFiles)
{
Console.WriteLine($" {file.Name}: {FormatFileSize(file.Length)}");
}
}
5. 最適化されたディレクトリサイズの計算
大きなフォルダではGetFiles()の代わりに遅延列挙のEnumerateFiles()/EnumerateDirectories()を使い、例外処理を行い、必要なら進捗を表示するといいよ。
static long GetDirectorySizeAdvanced(DirectoryInfo dir, bool showProgress = false)
{
long totalSize = 0;
int fileCount = 0;
var inaccessibleDirs = new List<string>();
try
{
// 大きなディレクトリには EnumerateFiles を使う(遅延読み込み)
foreach (var file in dir.EnumerateFiles())
{
try
{
totalSize += file.Length;
fileCount++;
if (showProgress && fileCount % 1000 == 0)
{
Console.WriteLine($"処理したファイル数: {fileCount}, サイズ: {FormatFileSize(totalSize)}");
}
}
catch (UnauthorizedAccessException)
{
// ファイルにアクセスできない、スキップ
}
catch (IOException)
{
// 読み取りエラー、スキップ
}
}
// サブディレクトリを再帰処理
foreach (var subdir in dir.EnumerateDirectories())
{
try
{
totalSize += GetDirectorySizeAdvanced(subdir, showProgress);
}
catch (UnauthorizedAccessException)
{
inaccessibleDirs.Add(subdir.FullName);
}
}
}
catch (UnauthorizedAccessException)
{
inaccessibleDirs.Add(dir.FullName);
}
if (inaccessibleDirs.Any() && showProgress)
{
Console.WriteLine($"アクセスできないディレクトリ数: {inaccessibleDirs.Count}");
}
return totalSize;
}
// 使用例
var targetDir = new DirectoryInfo(@"C:\Users");
Console.WriteLine("ディレクトリサイズの計算を開始します...");
long size = GetDirectorySizeAdvanced(targetDir, showProgress: true);
Console.WriteLine($"合計サイズ: {FormatFileSize(size)}");
6. メタデータの実用例
日付でファイルを検索
指定期間に変更されたファイルを探す(クリーンアップ、解析、監査に便利)。
static void FindFilesByDate(DirectoryInfo dir, DateTime fromDate, DateTime toDate)
{
Console.WriteLine($"{fromDate:yyyy-MM-dd} から {toDate:yyyy-MM-dd} までのファイルを検索します");
var matchingFiles = dir.GetFiles("*", SearchOption.AllDirectories)
.Where(f => f.LastWriteTime >= fromDate && f.LastWriteTime <= toDate)
.OrderByDescending(f => f.LastWriteTime);
Console.WriteLine($"見つかったファイル数: {matchingFiles.Count()}");
foreach (var file in matchingFiles.Take(20))
{
Console.WriteLine($"{file.LastWriteTime:yyyy-MM-dd HH:mm} - {file.Name} ({FormatFileSize(file.Length)})");
}
}
// 例: 過去1週間に変更されたすべてのファイルを検索
var dir = new DirectoryInfo(@"C:\Documents");
var weekAgo = DateTime.Now.AddDays(-7);
FindFilesByDate(dir, weekAgo, DateTime.Now);
重複ファイルの検索
簡易的方法はサイズでグループ化すること。正確にするならコンテンツのハッシュ比較を追加するといいよ。
static void FindPotentialDuplicates(DirectoryInfo dir)
{
Console.WriteLine($"{dir.FullName} で潜在的な重複を検索します");
var files = dir.GetFiles("*", SearchOption.AllDirectories)
.Where(f => f.Length > 0) // 空ファイルは除外
.GroupBy(f => f.Length)
.Where(g => g.Count() > 1) // 複数のファイルがあるグループのみ
.OrderByDescending(g => g.Key); // サイズ順にソート
foreach (var sizeGroup in files.Take(10))
{
Console.WriteLine($"\nサイズ {FormatFileSize(sizeGroup.Key)} のファイル ({sizeGroup.Count()} 個):");
foreach (var file in sizeGroup)
{
Console.WriteLine($" {file.FullName}");
Console.WriteLine($" 変更日時: {file.LastWriteTime:yyyy-MM-dd HH:mm:ss}");
}
}
}
ディレクトリの変更監視
直近N分で変更されたファイルを表示する。
static void MonitorRecentChanges(DirectoryInfo dir, int minutesBack = 60)
{
var cutoffTime = DateTime.Now.AddMinutes(-minutesBack);
var recentFiles = dir.GetFiles("*", SearchOption.AllDirectories)
.Where(f => f.LastWriteTime > cutoffTime)
.OrderByDescending(f => f.LastWriteTime);
Console.WriteLine($"過去 {minutesBack} 分で変更されたファイル:");
if (!recentFiles.Any())
{
Console.WriteLine("変更は見つかりませんでした。");
return;
}
foreach (var file in recentFiles)
{
var minutesAgo = (DateTime.Now - file.LastWriteTime).TotalMinutes;
Console.WriteLine($"{file.Name} - {minutesAgo:F0} 分前 ({FormatFileSize(file.Length)})");
}
}
7. 親ディレクトリの扱い
FileInfoのDirectoryプロパティやDirectoryInfoのParentで階層を上がれるよ。
var file = new FileInfo(@"C:\Projects\MyApp\src\Program.cs");
Console.WriteLine($"ファイル: {file.Name}");
Console.WriteLine($"フォルダ: {file.Directory.Name}");
Console.WriteLine($"親フォルダ: {file.Directory.Parent.Name}");
Console.WriteLine($"プロジェクトのルートフォルダ: {file.Directory.Parent.Parent.Name}");
// ルートまで上がることもできる
var currentDir = file.Directory;
while (currentDir.Parent != null)
{
Console.WriteLine($"レベル: {currentDir.Name}");
currentDir = currentDir.Parent;
}
Console.WriteLine($"ルート: {currentDir.Name}");
8. 落とし穴とよくあるミス
1. キャッシュ。オブジェクトのFileInfoやDirectoryInfoは値をキャッシュする。オブジェクトを作成した後に変更があったらデータが古くなることがある。最新化するにはRefresh()を使おう。
var file = new FileInfo("test.txt");
file.Refresh(); // メタデータを更新
2. アクセス例外。アクセスできないファイルやフォルダがある:UnauthorizedAccessExceptionや他のアクセスエラーを処理してね。
try
{
var files = new DirectoryInfo(path).GetFiles();
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("アクセスできません");
}
3. タイムスタンプ。コピーするとCreationTimeが変わり、LastWriteTimeは保持されることがある。レポートや同期アルゴリズムに影響する点だよ。
File.Copy("a.txt", "b.txt");
4. パフォーマンス。 GetFiles()は全部を一度に読み込むので大きなフォルダでは遅くなる。遅延列挙のEnumerateFiles()を優先しよう。
GO TO FULL VERSION