namespace TinfoilVibeServer.Utilities;
public static class FileSystemExtensions
{
///
/// Returns the most recent last‑write time (UTC) of any file under the supplied
/// root directories, traversing all sub‑directories. If no files are found,
/// null is returned.
///
///
/// A collection of absolute paths that must point to existing directories.
/// Paths that do not exist or are inaccessible are silently skipped.
///
///
/// The UTC of the newest file, or null if there are none.
///
public static DateTime? GetLatestModifiedUtc(IEnumerable rootDirectories)
{
if (rootDirectories == null) throw new ArgumentNullException(nameof(rootDirectories));
// We keep a mutable variable because we don't want to materialise the entire
// sequence into memory.
DateTime? latest = null;
foreach (var root in rootDirectories)
{
if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root))
continue; // skip bad paths
try
{
// Enumerate lazily and process each file as soon as it’s yielded.
foreach (var filePath in Directory.EnumerateFiles(
root,
"*",
SearchOption.AllDirectories))
{
try
{
// Using FileSystemInfo to fetch only the property we need.
var fsi = new FileInfo(filePath);
var lastWrite = fsi.LastWriteTimeUtc;
if (!latest.HasValue || lastWrite > latest.Value)
latest = lastWrite;
}
catch (FileNotFoundException) // file vanished while we were enumerating
{
// ignore and keep going
}
catch (UnauthorizedAccessException)
{
// file exists but we can’t read its attributes – skip it
}
}
}
catch (UnauthorizedAccessException)
{
// the root directory itself is inaccessible – skip it
}
}
return latest;
}
///
/// Parallelised version that may be faster on very large directory trees.
///
public static DateTime? GetLatestModifiedUtcParallel(IEnumerable rootDirectories)
{
if (rootDirectories == null) throw new ArgumentNullException(nameof(rootDirectories));
// Flatten all file paths into a single stream first (this is the only
// part that needs to be thread‑safe).
var allFiles = rootDirectories
.Where(r => !string.IsNullOrWhiteSpace(r) && Directory.Exists(r))
.SelectMany(r => Directory.EnumerateFiles(r, "*", SearchOption.AllDirectories))
.ToArray(); // materialise once, then parallelise
// Now fetch the dates in parallel. The LINQ overload of Max() that takes
// an async selector is not available, so we just use Parallel.ForEach.
DateTime? latest = null;
var lockObj = new object();
Parallel.ForEach(allFiles, filePath =>
{
try
{
var lastWrite = new FileInfo(filePath).LastWriteTimeUtc;
lock (lockObj)
{
if (!latest.HasValue || lastWrite > latest.Value)
latest = lastWrite;
}
}
catch (Exception)
{
// swallow all exceptions – the caller only cares about the max date
}
});
return latest;
}
///
/// Creates the directory (and all missing parent directories) if it does not already exist.
///
/// Absolute or relative path to the directory to create.
/// Thrown if is null.
/// Thrown if is empty or contains only whitespace.
/// Thrown if the caller does not have permission.
/// Thrown if a file exists at the target path or the directory cannot be created.
public static void EnsureDirectoryExists(string? path)
{
if (path is null)
throw new ArgumentNullException(nameof(path));
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException("Path must not be empty or whitespace.", nameof(path));
// Directory.CreateDirectory is already idempotent – it only creates missing parts.
Directory.CreateDirectory(path);
}
}