Files
TinfoilVibeServer/TinfoilVibeServer/Utilities/FileSystemExtensions.cs
T
ecenshu 0e2fec8c01
Build & Push Docker image / build-and-push (push) Successful in 14m38s
ci / build_linux (push) Successful in 4m43s
Skip hashed and same location files
Explicit usings
Multipart rar handling
2025-11-23 21:05:58 +10:30

130 lines
5.1 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.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
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)
{
ArgumentNullException.ThrowIfNull(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);
}
}