refactored names; fixed return values for hard links service

This commit is contained in:
Flaminel
2025-02-22 00:30:42 +02:00
parent 9b68792ea9
commit 1ad07b1f51
15 changed files with 115 additions and 95 deletions
@@ -86,7 +86,7 @@ public sealed class DownloadCleaner : GenericHandler
if (hasDownloadsToChange)
{
await _downloadService.ChangeCategoryForNoHardlinksAsync(downloadsToChangeCategory, _excludedHashes);
await _downloadService.ChangeCategoryForNoHardLinksAsync(downloadsToChangeCategory, _excludedHashes);
}
else
{
@@ -266,7 +266,7 @@ public class DelugeService : DownloadService, IDelugeService
}
}
public override Task ChangeCategoryForNoHardlinksAsync(List<object> downloads, HashSet<string> excludedHashes)
public override Task ChangeCategoryForNoHardLinksAsync(List<object> downloads, HashSet<string> excludedHashes)
{
throw new NotImplementedException();
}
@@ -87,7 +87,7 @@ public abstract class DownloadService : IDownloadService
public abstract Task CleanDownloads(List<object> downloads, List<CleanCategory> categoriesToClean, HashSet<string> excludedHashes);
/// <inheritdoc/>
public abstract Task ChangeCategoryForNoHardlinksAsync(List<object> downloads, HashSet<string> excludedHashes);
public abstract Task ChangeCategoryForNoHardLinksAsync(List<object> downloads, HashSet<string> excludedHashes);
protected void ResetStrikesOnProgress(string hash, long downloaded)
{
@@ -68,7 +68,7 @@ public class DummyDownloadService : DownloadService
throw new NotImplementedException();
}
public override Task ChangeCategoryForNoHardlinksAsync(List<object> downloads, HashSet<string> excludedHashes)
public override Task ChangeCategoryForNoHardLinksAsync(List<object> downloads, HashSet<string> excludedHashes)
{
throw new NotImplementedException();
}
@@ -53,7 +53,7 @@ public interface IDownloadService : IDisposable
/// </summary>
/// <param name="downloads">The downloads to change.</param>
/// <param name="excludedHashes"></param>
Task ChangeCategoryForNoHardlinksAsync(List<object> downloads, HashSet<string> excludedHashes);
Task ChangeCategoryForNoHardLinksAsync(List<object> downloads, HashSet<string> excludedHashes);
/// <summary>
/// Deletes a download item.
@@ -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<object> downloads, HashSet<string> excludedHashes)
public override async Task ChangeCategoryForNoHardLinksAsync(List<object> downloads, HashSet<string> excludedHashes)
{
if (_downloadCleanerConfig.IgnoreRootDir)
{
// TODO call this only if Unix
downloads
.Cast<TorrentInfo>()
.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<TorrentContent>? 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);
@@ -289,7 +289,7 @@ public class TransmissionService : DownloadService, ITransmissionService
}
}
public override Task ChangeCategoryForNoHardlinksAsync(List<object> downloads, HashSet<string> excludedHashes)
public override Task ChangeCategoryForNoHardLinksAsync(List<object> downloads, HashSet<string> excludedHashes)
{
throw new NotImplementedException();
}
@@ -0,0 +1,49 @@
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
namespace Infrastructure.Verticals.Files;
public class HardLinkFileService : IHardLinkFileService
{
private readonly ILogger<HardLinkFileService> _logger;
private readonly UnixHardLinkFileService _unixHardLinkFileService;
private readonly WindowsHardLinkFileService _windowsHardLinkFileService;
public HardLinkFileService(
ILogger<HardLinkFileService> 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);
}
}
@@ -1,49 +0,0 @@
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
namespace Infrastructure.Verticals.Files;
public class HardlinkFileService : IHardlinkFileService
{
private readonly ILogger<HardlinkFileService> _logger;
private readonly UnixHardlinkFileService _unixHardlinkFileService;
private readonly WindowsHardlinkFileService _windowsHardlinkFileService;
public HardlinkFileService(
ILogger<HardlinkFileService> 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);
}
}
@@ -0,0 +1,19 @@
namespace Infrastructure.Verticals.Files;
public interface IHardLinkFileService
{
/// <summary>
/// Populates the inode counts for Unix and the file index counts for Windows.
/// Needs to be called before <see cref="GetHardLinkCount"/> to populate the inode counts.
/// </summary>
/// <param name="directoryPath">The root directory where to search for hardlinks.</param>
void PopulateFileCounts(string directoryPath);
/// <summary>
/// Get the hardlink count of a file.
/// </summary>
/// <param name="filePath">File path.</param>
/// <param name="ignoreRootDir">Whether to ignore hardlinks found in the same root dir.</param>
/// <returns>-1 on error, 0 if there are no hardlinks and 1 otherwise.</returns>
long GetHardLinkCount(string filePath, bool ignoreRootDir);
}
@@ -1,7 +0,0 @@
namespace Infrastructure.Verticals.Files;
public interface IHardlinkFileService
{
void PopulateInodeCounts(string directoryPath);
long GetHardLinkCount(string filePath, bool ignoreRootDir);
}
@@ -4,18 +4,18 @@ using Mono.Unix.Native;
namespace Infrastructure.Verticals.Files;
public class UnixHardlinkFileService
public class UnixHardLinkFileService : IHardLinkFileService
{
private readonly ILogger<UnixHardlinkFileService> _logger;
// Track inode counts in the ignored directory (e.g., root directory)
private readonly ILogger<UnixHardLinkFileService> _logger;
private readonly ConcurrentDictionary<ulong, int> _inodeCounts = new();
public UnixHardlinkFileService(ILogger<UnixHardlinkFileService> logger)
public UnixHardLinkFileService(ILogger<UnixHardLinkFileService> logger)
{
_logger = logger;
}
public long GetHardlinkCount(string filePath, bool ignoreRootDir)
/// <inheritdoc/>
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)
/// <inheritdoc/>
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;
}
}
}
@@ -5,18 +5,18 @@ using Microsoft.Win32.SafeHandles;
namespace Infrastructure.Verticals.Files;
public class WindowsHardlinkFileService
public class WindowsHardLinkFileService : IHardLinkFileService
{
private readonly ILogger<WindowsHardlinkFileService> _logger;
// Track file indices in the ignored directory (e.g., root directory)
private readonly ILogger<WindowsHardLinkFileService> _logger;
private readonly ConcurrentDictionary<ulong, int> _fileIndexCounts = new();
public WindowsHardlinkFileService(ILogger<WindowsHardlinkFileService> logger)
public WindowsHardLinkFileService(ILogger<WindowsHardLinkFileService> logger)
{
_logger = logger;
}
public long GetWindowsHardLinkCount(string filePath, bool ignoreRootDir)
/// <inheritdoc/>
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)
/// <inheritdoc/>
public void PopulateFileCounts(string directoryPath)
{
try
{