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); } }