diff --git a/code/Executable/DependencyInjection/ServicesDI.cs b/code/Executable/DependencyInjection/ServicesDI.cs index 4a5752d..a3294c2 100644 --- a/code/Executable/DependencyInjection/ServicesDI.cs +++ b/code/Executable/DependencyInjection/ServicesDI.cs @@ -24,9 +24,9 @@ public static class ServicesDI .AddTransient() .AddTransient() .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/code/Infrastructure.Tests/Verticals/DownloadClient/TestDownloadService.cs b/code/Infrastructure.Tests/Verticals/DownloadClient/TestDownloadService.cs index d37282b..e3bbd3a 100644 --- a/code/Infrastructure.Tests/Verticals/DownloadClient/TestDownloadService.cs +++ b/code/Infrastructure.Tests/Verticals/DownloadClient/TestDownloadService.cs @@ -45,7 +45,7 @@ public class TestDownloadService : DownloadService public override Task?> GetDownloadsToBeCleaned(List categories) => Task.FromResult?>(null); public override Task?> GetDownloadsToChangeCategory(List categories) => Task.FromResult?>(null); public override Task CleanDownloads(List downloads, List categoriesToClean, HashSet excludedHashes) => Task.CompletedTask; - public override Task ChangeCategoryForNoHardlinksAsync(List downloads, HashSet excludedHashes) => Task.CompletedTask; + public override Task ChangeCategoryForNoHardLinksAsync(List downloads, HashSet excludedHashes) => Task.CompletedTask; // Expose protected methods for testing public new void ResetStrikesOnProgress(string hash, long downloaded) => base.ResetStrikesOnProgress(hash, downloaded); diff --git a/code/Infrastructure/Verticals/DownloadCleaner/DownloadCleaner.cs b/code/Infrastructure/Verticals/DownloadCleaner/DownloadCleaner.cs index b922da4..ea48b89 100644 --- a/code/Infrastructure/Verticals/DownloadCleaner/DownloadCleaner.cs +++ b/code/Infrastructure/Verticals/DownloadCleaner/DownloadCleaner.cs @@ -86,7 +86,7 @@ public sealed class DownloadCleaner : GenericHandler if (hasDownloadsToChange) { - await _downloadService.ChangeCategoryForNoHardlinksAsync(downloadsToChangeCategory, _excludedHashes); + await _downloadService.ChangeCategoryForNoHardLinksAsync(downloadsToChangeCategory, _excludedHashes); } else { diff --git a/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs b/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs index ca75243..db9ad08 100644 --- a/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs @@ -266,7 +266,7 @@ public class DelugeService : DownloadService, IDelugeService } } - public override Task ChangeCategoryForNoHardlinksAsync(List downloads, HashSet excludedHashes) + public override Task ChangeCategoryForNoHardLinksAsync(List downloads, HashSet excludedHashes) { throw new NotImplementedException(); } diff --git a/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs b/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs index fe76c16..25468d2 100644 --- a/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs @@ -87,7 +87,7 @@ public abstract class DownloadService : IDownloadService public abstract Task CleanDownloads(List downloads, List categoriesToClean, HashSet excludedHashes); /// - public abstract Task ChangeCategoryForNoHardlinksAsync(List downloads, HashSet excludedHashes); + public abstract Task ChangeCategoryForNoHardLinksAsync(List downloads, HashSet excludedHashes); protected void ResetStrikesOnProgress(string hash, long downloaded) { diff --git a/code/Infrastructure/Verticals/DownloadClient/DummyDownloadService.cs b/code/Infrastructure/Verticals/DownloadClient/DummyDownloadService.cs index 3fecdb2..a340bbc 100644 --- a/code/Infrastructure/Verticals/DownloadClient/DummyDownloadService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/DummyDownloadService.cs @@ -68,7 +68,7 @@ public class DummyDownloadService : DownloadService throw new NotImplementedException(); } - public override Task ChangeCategoryForNoHardlinksAsync(List downloads, HashSet excludedHashes) + public override Task ChangeCategoryForNoHardLinksAsync(List downloads, HashSet excludedHashes) { throw new NotImplementedException(); } diff --git a/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs b/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs index e5c9d2f..0b90c4c 100644 --- a/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs @@ -53,7 +53,7 @@ public interface IDownloadService : IDisposable /// /// The downloads to change. /// - Task ChangeCategoryForNoHardlinksAsync(List downloads, HashSet excludedHashes); + Task ChangeCategoryForNoHardLinksAsync(List downloads, HashSet excludedHashes); /// /// Deletes a download item. diff --git a/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs b/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs index 1c82718..2753363 100644 --- a/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs @@ -5,6 +5,7 @@ using Common.Configuration.ContentBlocker; using Common.Configuration.DownloadCleaner; using Common.Configuration.DownloadClient; using Common.Configuration.QueueCleaner; +using Common.Exceptions; using Common.Helpers; using Domain.Enums; using Infrastructure.Interceptors; @@ -298,20 +299,26 @@ public class QBitService : DownloadService, IQBitService } } - public override async Task ChangeCategoryForNoHardlinksAsync(List downloads, HashSet excludedHashes) + public override async Task ChangeCategoryForNoHardLinksAsync(List downloads, HashSet excludedHashes) { if (_downloadCleanerConfig.IgnoreRootDir) { - // TODO call this only if Unix downloads .Cast() - .GroupBy(x => x.SavePath) + .GroupBy(x => Path.GetPathRoot(x.SavePath)) .Select(x => x.Key) .ToList() - .ForEach(x => _hardlinkFileService.PopulateInodeCounts(x)); + .ForEach(x => + { + if (!Directory.Exists(x)) + { + throw new ValidationException($"directory \"{x}\" does not exist"); + } + + _hardLinkFileService.PopulateFileCounts(x); + }); } - // TODO account for cross-seed foreach (TorrentInfo download in downloads) { IReadOnlyList? files = await _client.GetTorrentContentsAsync(download.Hash); @@ -343,7 +350,7 @@ public class QBitService : DownloadService, IQBitService : download.SavePath, file.Name ); - long hardlinkCount = _hardlinkFileService.GetHardLinkCount(filePath, _downloadCleanerConfig.IgnoreRootDir); + long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, _downloadCleanerConfig.IgnoreRootDir); if (hardlinkCount < 0) { @@ -364,7 +371,7 @@ public class QBitService : DownloadService, IQBitService continue; } - _logger.LogInformation("no hardlinks found | changing category for {name}", download.Name); + _logger.LogInformation("changing category for {name}", download.Name); await ((QBitService)Proxy).ChangeCategory(download.Hash, _downloadCleanerConfig.NoHardlinksCategory); await _notifier.NotifyCategoryChanged(download.Category, _downloadCleanerConfig.NoHardlinksCategory); diff --git a/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs b/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs index 568a67b..ab1608d 100644 --- a/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs @@ -289,7 +289,7 @@ public class TransmissionService : DownloadService, ITransmissionService } } - public override Task ChangeCategoryForNoHardlinksAsync(List downloads, HashSet excludedHashes) + public override Task ChangeCategoryForNoHardLinksAsync(List downloads, HashSet excludedHashes) { throw new NotImplementedException(); } diff --git a/code/Infrastructure/Verticals/Files/HardLinkFileService.cs b/code/Infrastructure/Verticals/Files/HardLinkFileService.cs new file mode 100644 index 0000000..c20da36 --- /dev/null +++ b/code/Infrastructure/Verticals/Files/HardLinkFileService.cs @@ -0,0 +1,49 @@ +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; + +namespace Infrastructure.Verticals.Files; + +public class HardLinkFileService : IHardLinkFileService +{ + private readonly 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 void PopulateFileCounts(string directoryPath) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _windowsHardLinkFileService.PopulateFileCounts(directoryPath); + return; + } + + _unixHardLinkFileService.PopulateFileCounts(directoryPath); + } + + public long GetHardLinkCount(string filePath, bool ignoreRootDir) + { + if (!File.Exists(filePath)) + { + _logger.LogDebug("file {file} does not exist", filePath); + return -1; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return _windowsHardLinkFileService.GetHardLinkCount(filePath, ignoreRootDir); + } + + return _unixHardLinkFileService.GetHardLinkCount(filePath, ignoreRootDir); + } +} \ No newline at end of file diff --git a/code/Infrastructure/Verticals/Files/HardlinkFileService.cs b/code/Infrastructure/Verticals/Files/HardlinkFileService.cs deleted file mode 100644 index fa137e0..0000000 --- a/code/Infrastructure/Verticals/Files/HardlinkFileService.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Runtime.InteropServices; -using Microsoft.Extensions.Logging; - -namespace Infrastructure.Verticals.Files; - -public class HardlinkFileService : IHardlinkFileService -{ - private readonly 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 void PopulateInodeCounts(string directoryPath) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - _windowsHardlinkFileService.PopulateFileIndexCounts(directoryPath); - return; - } - - _unixHardlinkFileService.PopulateInodeCounts(directoryPath); - } - - public long GetHardLinkCount(string filePath, bool ignoreRootDir) - { - if (!File.Exists(filePath)) - { - _logger.LogDebug("file {file} does not exist", filePath); - return -1; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return _windowsHardlinkFileService.GetWindowsHardLinkCount(filePath, ignoreRootDir); - } - - 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 new file mode 100644 index 0000000..1639c57 --- /dev/null +++ b/code/Infrastructure/Verticals/Files/IHardLinkFileService.cs @@ -0,0 +1,19 @@ +namespace Infrastructure.Verticals.Files; + +public interface IHardLinkFileService +{ + /// + /// Populates the inode counts for Unix and the file index counts for Windows. + /// Needs to be called before to populate the inode counts. + /// + /// The root directory where to search for hardlinks. + void PopulateFileCounts(string directoryPath); + + /// + /// Get the hardlink count of a file. + /// + /// File path. + /// Whether to ignore hardlinks found in the same root dir. + /// -1 on error, 0 if there are no hardlinks and 1 otherwise. + long GetHardLinkCount(string filePath, bool ignoreRootDir); +} \ No newline at end of file diff --git a/code/Infrastructure/Verticals/Files/IHardlinkFileService.cs b/code/Infrastructure/Verticals/Files/IHardlinkFileService.cs deleted file mode 100644 index 0c68d02..0000000 --- a/code/Infrastructure/Verticals/Files/IHardlinkFileService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Infrastructure.Verticals.Files; - -public interface IHardlinkFileService -{ - void PopulateInodeCounts(string directoryPath); - 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 similarity index 67% rename from code/Infrastructure/Verticals/Files/UnixHardlinkFileService.cs rename to code/Infrastructure/Verticals/Files/UnixHardLinkFileService.cs index 639c51e..ddd0026 100644 --- a/code/Infrastructure/Verticals/Files/UnixHardlinkFileService.cs +++ b/code/Infrastructure/Verticals/Files/UnixHardLinkFileService.cs @@ -4,18 +4,18 @@ using Mono.Unix.Native; namespace Infrastructure.Verticals.Files; -public class UnixHardlinkFileService +public class UnixHardLinkFileService : IHardLinkFileService { - private readonly ILogger _logger; - // Track inode counts in the ignored directory (e.g., root directory) + private readonly ILogger _logger; private readonly ConcurrentDictionary _inodeCounts = new(); - public UnixHardlinkFileService(ILogger logger) + public UnixHardLinkFileService(ILogger logger) { _logger = logger; } - public long GetHardlinkCount(string filePath, bool ignoreRootDir) + /// + public long GetHardLinkCount(string filePath, bool ignoreRootDir) { try { @@ -27,15 +27,14 @@ public class UnixHardlinkFileService 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; + return (long)stat.st_nlink == 1 ? 0 : 1; } - // Adjusted case: Subtract links from the ignored directory + // subtract the number of hardlinks in the same root directory int linksInIgnoredDir = _inodeCounts.TryGetValue(stat.st_ino, out int count) - ? count - : 1; // Default to 1 if not found + ? count + : 1; // default to 1 if not found _logger.LogDebug("stat file | hardlinks: {nlink} | ignored: {ignored} | {file}", stat.st_nlink, linksInIgnoredDir, filePath); return (long)stat.st_nlink - linksInIgnoredDir; @@ -47,12 +46,11 @@ public class UnixHardlinkFileService } } - // Call this first to populate inode counts from the directory you want to ignore - public void PopulateInodeCounts(string directoryPath) + /// + public void PopulateFileCounts(string directoryPath) { try { - // Traverse all files and directories in the ignored path foreach (var file in Directory.EnumerateFiles(directoryPath, "*", SearchOption.AllDirectories)) { AddInodeToCount(file); @@ -65,7 +63,8 @@ public class UnixHardlinkFileService } catch (Exception ex) { - _logger.LogError(ex, "Failed to populate inode counts from {dir}", directoryPath); + _logger.LogError(ex, "failed to populate inode counts from {dir}", directoryPath); + throw; } } @@ -80,7 +79,8 @@ public class UnixHardlinkFileService } catch (Exception ex) { - _logger.LogWarning(ex, "Couldn't stat {path} during inode counting", path); + _logger.LogWarning(ex, "could not stat {path} during inode counting", path); + throw; } } } \ No newline at end of file diff --git a/code/Infrastructure/Verticals/Files/WindowsHardlinkFileService.cs b/code/Infrastructure/Verticals/Files/WindowsHardLinkFileService.cs similarity index 89% rename from code/Infrastructure/Verticals/Files/WindowsHardlinkFileService.cs rename to code/Infrastructure/Verticals/Files/WindowsHardLinkFileService.cs index 4e6efb0..47ff71e 100644 --- a/code/Infrastructure/Verticals/Files/WindowsHardlinkFileService.cs +++ b/code/Infrastructure/Verticals/Files/WindowsHardLinkFileService.cs @@ -5,18 +5,18 @@ using Microsoft.Win32.SafeHandles; namespace Infrastructure.Verticals.Files; -public class WindowsHardlinkFileService +public class WindowsHardLinkFileService : IHardLinkFileService { - private readonly ILogger _logger; - // Track file indices in the ignored directory (e.g., root directory) + private readonly ILogger _logger; private readonly ConcurrentDictionary _fileIndexCounts = new(); - public WindowsHardlinkFileService(ILogger logger) + public WindowsHardLinkFileService(ILogger logger) { _logger = logger; } - public long GetWindowsHardLinkCount(string filePath, bool ignoreRootDir) + /// + public long GetHardLinkCount(string filePath, bool ignoreRootDir) { try { @@ -31,7 +31,7 @@ public class WindowsHardlinkFileService if (!ignoreRootDir) { _logger.LogDebug("stat file | hardlinks: {nlink} | {file}", file.NumberOfLinks, filePath); - return file.NumberOfLinks; + return file.NumberOfLinks == 1 ? 0 : 1; } // Get unique file ID (combination of high and low indices) @@ -52,7 +52,8 @@ public class WindowsHardlinkFileService } } - public void PopulateFileIndexCounts(string directoryPath) + /// + public void PopulateFileCounts(string directoryPath) { try {