Files
TinfoilVibeServer/TinfoilVibeServer/Utilities/FileSystemExtensions.cs
T
ecenshu 33a724a796
ci / build_linux (push) Failing after 1m49s
Consolidate config files for both local dev and docker
Consolidate data into separate mapped volume for docker and local
2025-11-13 18:54:57 +10:30

125 lines
5.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace TinfoilVibeServer.Utilities;
public static class FileSystemExtensions
{
/// <summary>
/// Returns the most recent lastwrite time (UTC) of any file under the supplied
/// root directories, traversing all subdirectories. 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 its 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 cant 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 threadsafe).
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);
}
}