From 8c8d412ef102f84ebfebad542f3a75a318a43571 Mon Sep 17 00:00:00 2001 From: Flaminel Date: Fri, 21 Feb 2025 23:57:43 +0200 Subject: [PATCH] refactored hardlink service --- .../DependencyInjection/ServicesDI.cs | 2 + .../DownloadClient/QBittorrent/QBitService.cs | 4 +- .../Verticals/Files/HardlinkFileService.cs | 153 ++++-------------- .../Verticals/Files/IHardlinkFileService.cs | 2 +- .../Files/UnixHardlinkFileService.cs | 88 ++++++++++ .../Files/WindowsHardlinkFileService.cs | 55 +++++++ 6 files changed, 175 insertions(+), 129 deletions(-) create mode 100644 code/Infrastructure/Verticals/Files/UnixHardlinkFileService.cs create mode 100644 code/Infrastructure/Verticals/Files/WindowsHardlinkFileService.cs diff --git a/code/Executable/DependencyInjection/ServicesDI.cs b/code/Executable/DependencyInjection/ServicesDI.cs index b737dd8..4a5752d 100644 --- a/code/Executable/DependencyInjection/ServicesDI.cs +++ b/code/Executable/DependencyInjection/ServicesDI.cs @@ -25,6 +25,8 @@ public static class ServicesDI .AddTransient() .AddTransient() .AddTransient() + .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs b/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs index 0ffa788..1c82718 100644 --- a/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs @@ -343,9 +343,9 @@ public class QBitService : DownloadService, IQBitService : download.SavePath, file.Name ); - ulong hardlinkCount = _hardlinkFileService.GetHardLinkCount(filePath, _downloadCleanerConfig.IgnoreRootDir); + long hardlinkCount = _hardlinkFileService.GetHardLinkCount(filePath, _downloadCleanerConfig.IgnoreRootDir); - if (hardlinkCount is 0) + if (hardlinkCount < 0) { _logger.LogDebug("skip | could not get file properties | {name}", download.Name); hasHardlinks = true; diff --git a/code/Infrastructure/Verticals/Files/HardlinkFileService.cs b/code/Infrastructure/Verticals/Files/HardlinkFileService.cs index 4ffb870..c442d14 100644 --- a/code/Infrastructure/Verticals/Files/HardlinkFileService.cs +++ b/code/Infrastructure/Verticals/Files/HardlinkFileService.cs @@ -1,148 +1,49 @@ -using System.Collections.Concurrent; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; -using Microsoft.Win32.SafeHandles; -using Mono.Unix.Native; namespace Infrastructure.Verticals.Files; public class HardlinkFileService : IHardlinkFileService { private readonly ILogger _logger; - // Track inode counts in the ignored directory (e.g., root directory) - private readonly ConcurrentDictionary _inodeCounts = new(); - - public HardlinkFileService(ILogger logger) + private readonly UnixHardlinkFileService _unixHardlinkFileService; + private readonly WindowsHardlinkFileService _windowsHardlinkFileService; + + public HardlinkFileService( + ILogger logger, + UnixHardlinkFileService unixHardlinkFileService, + WindowsHardlinkFileService windowsHardlinkFileService + ) { _logger = logger; + _unixHardlinkFileService = unixHardlinkFileService; + _windowsHardlinkFileService = windowsHardlinkFileService; } - - public ulong GetHardLinkCount(string filePath, bool ignoreRootDir) + + public void PopulateInodeCounts(string directoryPath) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // TODO + return; + } + + _unixHardlinkFileService.PopulateInodeCounts(directoryPath); + } + + public long GetHardLinkCount(string filePath, bool ignoreRootDir) { if (!File.Exists(filePath)) { _logger.LogDebug("file {file} does not exist", filePath); - return default; + return -1; } if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - _logger.LogDebug("Windows platform detected"); - return GetWindowsHardLinkCount(filePath); + return _windowsHardlinkFileService.GetWindowsHardLinkCount(filePath); } - return GetUnixHardLinkCount(filePath, ignoreRootDir); - } - - private uint GetWindowsHardLinkCount(string filePath) - { - try - { - using SafeFileHandle fileStream = File.OpenHandle(filePath); - - if (GetFileInformationByHandle(fileStream, out var file)) - { - return file.NumberOfLinks; - } - } - catch (Exception exception) - { - // TODO log download name? - _logger.LogError(exception, "failed to stat Windows file {file}", filePath); - } - - return default; - } - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool GetFileInformationByHandle( - SafeFileHandle hFile, - out BY_HANDLE_FILE_INFORMATION lpFileInformation - ); - - private struct BY_HANDLE_FILE_INFORMATION - { - public uint FileAttributes; - public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime; - public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime; - public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime; - public uint VolumeSerialNumber; - public uint FileSizeHigh; - public uint FileSizeLow; - public uint NumberOfLinks; - public uint FileIndexHigh; - public uint FileIndexLow; - } - - // Call this first to populate inode counts from the directory you want to ignore - public void PopulateInodeCounts(string directoryPath) - { - try - { - // Traverse all files and directories in the ignored path - foreach (var file in Directory.EnumerateFiles(directoryPath, "*", SearchOption.AllDirectories)) - { - AddInodeToCount(file); - } - - foreach (var dir in Directory.EnumerateDirectories(directoryPath, "*", SearchOption.AllDirectories)) - { - AddInodeToCount(dir); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to populate inode counts from {dir}", directoryPath); - } - } - - private void AddInodeToCount(string path) - { - try - { - if (Syscall.stat(path, out Stat stat) == 0) - { - _inodeCounts.AddOrUpdate(stat.st_ino, 1, (_, count) => count + 1); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Couldn't stat {path} during inode counting", path); - } - } - - // Modified GetUnixHardLinkCount with ignore logic - public ulong GetUnixHardLinkCount(string filePath, bool ignoreRootDir) - { - try - { - if (Syscall.stat(filePath, out Stat stat) != 0) - { - _logger.LogDebug("failed to stat file {file}", filePath); - return 0; - } - - if (!ignoreRootDir) - { - // Simple case: Just check if >1 hardlink exists - _logger.LogDebug("stat file {file} | nlink: {nlink}", filePath, stat.st_nlink); - return stat.st_nlink; - } - - // Adjusted case: Subtract links from the ignored directory - int linksInIgnoredDir = _inodeCounts.TryGetValue(stat.st_ino, out int count) - ? count - : 1; // Default to 1 if not found - - _logger.LogDebug("stat file {file} | nlink: {nlink} | ignored: {ignored}", filePath, stat.st_nlink, linksInIgnoredDir); - - long adjustedCount = (long)stat.st_nlink - linksInIgnoredDir; - return (ulong)Math.Max(adjustedCount, 0); - } - catch (Exception exception) - { - _logger.LogError(exception, "failed to stat file {file}", filePath); - return 0; - } + return _unixHardlinkFileService.GetHardlinkCount(filePath, ignoreRootDir); } } \ No newline at end of file diff --git a/code/Infrastructure/Verticals/Files/IHardlinkFileService.cs b/code/Infrastructure/Verticals/Files/IHardlinkFileService.cs index 4b50cc3..0c68d02 100644 --- a/code/Infrastructure/Verticals/Files/IHardlinkFileService.cs +++ b/code/Infrastructure/Verticals/Files/IHardlinkFileService.cs @@ -3,5 +3,5 @@ public interface IHardlinkFileService { void PopulateInodeCounts(string directoryPath); - ulong GetHardLinkCount(string filePath, bool ignoreRootDir); + long GetHardLinkCount(string filePath, bool ignoreRootDir); } \ No newline at end of file diff --git a/code/Infrastructure/Verticals/Files/UnixHardlinkFileService.cs b/code/Infrastructure/Verticals/Files/UnixHardlinkFileService.cs new file mode 100644 index 0000000..90b8341 --- /dev/null +++ b/code/Infrastructure/Verticals/Files/UnixHardlinkFileService.cs @@ -0,0 +1,88 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using Mono.Unix.Native; + +namespace Infrastructure.Verticals.Files; + +public class UnixHardlinkFileService +{ + private readonly ILogger _logger; + // Track inode counts in the ignored directory (e.g., root directory) + private readonly ConcurrentDictionary _inodeCounts = new(); + + public UnixHardlinkFileService(ILogger logger) + { + _logger = logger; + } + + public long GetHardlinkCount(string filePath, bool ignoreRootDir) + { + try + { + if (Syscall.stat(filePath, out Stat stat) != 0) + { + _logger.LogDebug("failed to stat file {file}", filePath); + return -1; + } + + if (!ignoreRootDir) + { + // Simple case: Just check if >1 hardlink exists + _logger.LogDebug("stat file | hardlinks: {nlink} | {file}", stat.st_nlink, filePath); + return (long)stat.st_nlink; + } + + // Adjusted case: Subtract links from the ignored directory + int linksInIgnoredDir = _inodeCounts.TryGetValue(stat.st_ino, out int count) + ? count + : 1; // Default to 1 if not found + + _logger.LogDebug("stat file | hardlinks: {nlink} | ignored: {ignored} | {file}", stat.st_nlink, linksInIgnoredDir, filePath); + + long adjustedCount = (long)stat.st_nlink - linksInIgnoredDir; + return Math.Max(adjustedCount, 0); + } + catch (Exception exception) + { + _logger.LogError(exception, "failed to stat file {file}", filePath); + return -1; + } + } + + // Call this first to populate inode counts from the directory you want to ignore + public void PopulateInodeCounts(string directoryPath) + { + try + { + // Traverse all files and directories in the ignored path + foreach (var file in Directory.EnumerateFiles(directoryPath, "*", SearchOption.AllDirectories)) + { + AddInodeToCount(file); + } + + foreach (var dir in Directory.EnumerateDirectories(directoryPath, "*", SearchOption.AllDirectories)) + { + AddInodeToCount(dir); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to populate inode counts from {dir}", directoryPath); + } + } + + private void AddInodeToCount(string path) + { + try + { + if (Syscall.stat(path, out Stat stat) == 0) + { + _inodeCounts.AddOrUpdate(stat.st_ino, 1, (_, count) => count + 1); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Couldn't stat {path} during inode counting", path); + } + } +} \ No newline at end of file diff --git a/code/Infrastructure/Verticals/Files/WindowsHardlinkFileService.cs b/code/Infrastructure/Verticals/Files/WindowsHardlinkFileService.cs new file mode 100644 index 0000000..b0ae2be --- /dev/null +++ b/code/Infrastructure/Verticals/Files/WindowsHardlinkFileService.cs @@ -0,0 +1,55 @@ +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; +using Microsoft.Win32.SafeHandles; + +namespace Infrastructure.Verticals.Files; + +public class WindowsHardlinkFileService +{ + private readonly ILogger _logger; + + public WindowsHardlinkFileService(ILogger logger) + { + _logger = logger; + } + + public long GetWindowsHardLinkCount(string filePath) + { + try + { + using SafeFileHandle fileStream = File.OpenHandle(filePath); + + if (GetFileInformationByHandle(fileStream, out var file)) + { + return file.NumberOfLinks; + } + } + catch (Exception exception) + { + // TODO log download name? + _logger.LogError(exception, "failed to stat Windows file {file}", filePath); + } + + return -1; + } + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool GetFileInformationByHandle( + SafeFileHandle hFile, + out BY_HANDLE_FILE_INFORMATION lpFileInformation + ); + + private struct BY_HANDLE_FILE_INFORMATION + { + public uint FileAttributes; + public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime; + public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime; + public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime; + public uint VolumeSerialNumber; + public uint FileSizeHigh; + public uint FileSizeLow; + public uint NumberOfLinks; + public uint FileIndexHigh; + public uint FileIndexLow; + } +} \ No newline at end of file