125 lines
5.0 KiB
C#
125 lines
5.0 KiB
C#
namespace TinfoilVibeServer.Utilities;
|
||
|
||
public static class FileSystemExtensions
|
||
{
|
||
/// <summary>
|
||
/// 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,
|
||
/// <c>null</c> is returned.
|
||
/// </summary>
|
||
/// <param name="rootDirectories">
|
||
/// A collection of absolute paths that must point to existing directories.
|
||
/// Paths that do not exist or are inaccessible are silently skipped.
|
||
/// </param>
|
||
/// <returns>
|
||
/// The UTC <see cref="DateTime"/> of the newest file, or <c>null</c> if there are none.
|
||
/// </returns>
|
||
public static DateTime? GetLatestModifiedUtc(IEnumerable<string> 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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Parallelised version that may be faster on very large directory trees.
|
||
/// </summary>
|
||
public static DateTime? GetLatestModifiedUtcParallel(IEnumerable<string> 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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates the directory (and all missing parent directories) if it does not already exist.
|
||
/// </summary>
|
||
/// <param name="path">Absolute or relative path to the directory to create.</param>
|
||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="path"/> is null.</exception>
|
||
/// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is empty or contains only whitespace.</exception>
|
||
/// <exception cref="UnauthorizedAccessException">Thrown if the caller does not have permission.</exception>
|
||
/// <exception cref="IOException">Thrown if a file exists at the target path or the directory cannot be created.</exception>
|
||
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);
|
||
}
|
||
}
|