renamed vars; added root dir; fixed root dir file counts; fixed qbit flow

This commit is contained in:
Flaminel
2025-03-24 18:05:50 +02:00
parent a83809eef7
commit 4a1e0f6896
15 changed files with 306 additions and 306 deletions
@@ -1,4 +1,4 @@
using Common.Exceptions; using Common.Exceptions;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
namespace Common.Configuration.DownloadCleaner; namespace Common.Configuration.DownloadCleaner;
@@ -17,14 +17,14 @@ public sealed record DownloadCleanerConfig : IJobConfig, IIgnoredDownloadsConfig
[ConfigurationKeyName("IGNORED_DOWNLOADS_PATH")] [ConfigurationKeyName("IGNORED_DOWNLOADS_PATH")]
public string? IgnoredDownloadsPath { get; init; } public string? IgnoredDownloadsPath { get; init; }
[ConfigurationKeyName("NO_HL_CATEGORY")] [ConfigurationKeyName("UNLINKED_TARGET_CATEGORY")]
public string NoHardLinksCategory { get; init; } = ""; public string UnlinkedTargetCategory { get; init; } = "cleanuperr-unlinked";
[ConfigurationKeyName("NO_HL_IGNORE_ROOT_DIR")] [ConfigurationKeyName("UNLINKED_IGNORED_ROOT_DIR")]
public bool NoHardLinksIgnoreRootDir { get; init; } public string UnlinkedIgnoredRootDir { get; init; } = string.Empty;
[ConfigurationKeyName("NO_HL_CATEGORIES")] [ConfigurationKeyName("UNLINKED_CATEGORIES")]
public List<string>? NoHardLinksCategories { get; init; } public List<string>? UnlinkedCategories { get; init; }
public void Validate() public void Validate()
{ {
@@ -45,24 +45,29 @@ public sealed record DownloadCleanerConfig : IJobConfig, IIgnoredDownloadsConfig
Categories?.ForEach(x => x.Validate()); Categories?.ForEach(x => x.Validate());
if (string.IsNullOrEmpty(NoHardLinksCategory)) if (string.IsNullOrEmpty(UnlinkedTargetCategory))
{ {
return; return;
} }
if (NoHardLinksCategories?.Count is null or 0) if (UnlinkedCategories?.Count is null or 0)
{ {
throw new ValidationException("no categories configured"); throw new ValidationException("no unlinked categories configured");
} }
if (NoHardLinksCategories.Contains(NoHardLinksCategory)) if (UnlinkedCategories.Contains(UnlinkedTargetCategory))
{ {
throw new ValidationException("NO_HARDLINKS_CATEGORY is present in NO_HARDLINKS_CATEGORIES"); throw new ValidationException($"{SectionName.ToUpperInvariant()}__UNLINKED_TARGET_CATEGORY should not be present in {SectionName.ToUpperInvariant()}__UNLINKED_CATEGORIES");
} }
if (NoHardLinksCategories.Any(string.IsNullOrEmpty)) if (UnlinkedCategories.Any(string.IsNullOrEmpty))
{ {
throw new ValidationException("empty hardlink filter category found"); throw new ValidationException("empty unlinked category filter found");
}
if (!string.IsNullOrEmpty(UnlinkedIgnoredRootDir) && !Directory.Exists(UnlinkedIgnoredRootDir))
{
throw new ValidationException($"{UnlinkedIgnoredRootDir} root directory does not exist");
} }
} }
} }
+6 -6
View File
@@ -44,16 +44,16 @@
"Name": "tv-sonarr", "Name": "tv-sonarr",
"MAX_RATIO": -1, "MAX_RATIO": -1,
"MIN_SEED_TIME": 0, "MIN_SEED_TIME": 0,
"MAX_SEED_TIME": -1 "MAX_SEED_TIME": 240
} }
], ],
"IGNORED_DOWNLOADS_PATH": "../test/data/cleanuperr/ignored_downloads", "UNLINKED_TARGET_CATEGORY": "cleanuperr-unlinked",
"NO_HL_CATEGORY": "nohardlinks", "UNLINKED_IGNORED_ROOT_DIR": "../test/data/qbit-win",
"NO_HL_IGNORE_ROOT_DIR": false, "UNLINKED_CATEGORIES": [
"NO_HL_CATEGORIES": [
"tv-sonarr", "tv-sonarr",
"radarr" "radarr"
] ],
"IGNORED_DOWNLOADS_PATH": "../test/data/cleanuperr/ignored_downloads"
}, },
"DOWNLOAD_CLIENT": "qbittorrent", "DOWNLOAD_CLIENT": "qbittorrent",
"qBittorrent": { "qBittorrent": {
+3
View File
@@ -37,6 +37,9 @@
"Enabled": false, "Enabled": false,
"DELETE_PRIVATE": false, "DELETE_PRIVATE": false,
"CATEGORIES": [], "CATEGORIES": [],
"UNLINKED_TARGET_CATEGORY": "cleanuperr-unlinked",
"UNLINKED_IGNORED_ROOT_DIR": "",
"UNLINKED_CATEGORIES": [],
"IGNORED_DOWNLOADS_PATH": "" "IGNORED_DOWNLOADS_PATH": ""
}, },
"DOWNLOAD_CLIENT": "none", "DOWNLOAD_CLIENT": "none",
@@ -46,7 +46,7 @@ public class TestDownloadService : DownloadService
public override Task<List<object>?> GetSeedingDownloads() => Task.FromResult<List<object>?>(null); public override Task<List<object>?> GetSeedingDownloads() => Task.FromResult<List<object>?>(null);
public override List<object>? FilterDownloadsToBeCleanedAsync(List<object>? downloads, List<CleanCategory> categories) => null; public override List<object>? FilterDownloadsToBeCleanedAsync(List<object>? downloads, List<CleanCategory> categories) => null;
public override List<object>? FilterDownloadsToChangeCategoryAsync(List<object>? downloads, List<string> categories) => null; public override List<object>? FilterDownloadsToChangeCategoryAsync(List<object>? downloads, List<string> categories) => null;
public override Task CleanDownloadsAsync(List<object>? downloads, List<CleanCategory> categoriesToClean, HashSet<string> excludedHashes) => Task.CompletedTask; public override Task CleanDownloadsAsync(List<object>? downloads, List<CleanCategory> categoriesToClean, HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads) => Task.CompletedTask;
public override Task ChangeCategoryForNoHardLinksAsync(List<object>? downloads, HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads) => Task.CompletedTask; public override Task ChangeCategoryForNoHardLinksAsync(List<object>? downloads, HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads) => Task.CompletedTask;
// Expose protected methods for testing // Expose protected methods for testing
public new void ResetStrikesOnProgress(string hash, long downloaded) => base.ResetStrikesOnProgress(hash, downloaded); public new void ResetStrikesOnProgress(string hash, long downloaded) => base.ResetStrikesOnProgress(hash, downloaded);
@@ -70,17 +70,17 @@ public sealed class DownloadCleaner : GenericHandler
List<object>? downloads = await _downloadService.GetSeedingDownloads(); List<object>? downloads = await _downloadService.GetSeedingDownloads();
List<object>? downloadsToChangeCategory = null; List<object>? downloadsToChangeCategory = null;
if (!string.IsNullOrEmpty(_config.NoHardLinksCategory) && _config.NoHardLinksCategories?.Count > 0) if (!string.IsNullOrEmpty(_config.UnlinkedTargetCategory) && _config.UnlinkedCategories?.Count > 0)
{ {
if (!_hardLinkCategoryCreated) if (!_hardLinkCategoryCreated)
{ {
_logger.LogDebug("creating category {cat}", _config.NoHardLinksCategory); _logger.LogDebug("creating category {cat}", _config.UnlinkedTargetCategory);
await _downloadService.CreateCategoryAsync(_config.NoHardLinksCategory); await _downloadService.CreateCategoryAsync(_config.UnlinkedTargetCategory);
_hardLinkCategoryCreated = true; _hardLinkCategoryCreated = true;
} }
downloadsToChangeCategory = _downloadService.FilterDownloadsToChangeCategoryAsync(downloads, _config.NoHardLinksCategories); downloadsToChangeCategory = _downloadService.FilterDownloadsToChangeCategoryAsync(downloads, _config.UnlinkedCategories);
} }
// wait for the downloads to appear in the arr queue // wait for the downloads to appear in the arr queue
@@ -91,7 +91,7 @@ public sealed class DownloadCleaner : GenericHandler
await ProcessArrConfigAsync(_lidarrConfig, InstanceType.Lidarr, true); await ProcessArrConfigAsync(_lidarrConfig, InstanceType.Lidarr, true);
_logger.LogTrace("looking for downloads to change category"); _logger.LogTrace("looking for downloads to change category");
await _downloadService.ChangeCategoryForNoHardLinksAsync(downloadsToChangeCategory, _excludedHashes); await _downloadService.ChangeCategoryForNoHardLinksAsync(downloadsToChangeCategory, _excludedHashes, ignoredDownloads);
List<object>? downloadsToClean = _downloadService.FilterDownloadsToBeCleanedAsync(downloads, _config.Categories); List<object>? downloadsToClean = _downloadService.FilterDownloadsToBeCleanedAsync(downloads, _config.Categories);
@@ -99,7 +99,7 @@ public sealed class DownloadCleaner : GenericHandler
downloads = null; downloads = null;
_logger.LogTrace("looking for downloads to clean"); _logger.LogTrace("looking for downloads to clean");
await _downloadService.CleanDownloadsAsync(downloadsToClean, _config.Categories, _excludedHashes); await _downloadService.CleanDownloadsAsync(downloadsToClean, _config.Categories, _excludedHashes, ignoredDownloads);
} }
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType) protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
@@ -193,4 +193,10 @@ public sealed class DelugeClient
return webResponse.Result; return webResponse.Result;
} }
public async Task SetTorrentLabel(string hash, string newLabel)
{
// TODO
throw new NotImplementedException();
}
} }
@@ -301,110 +301,112 @@ public class DelugeService : DownloadService, IDelugeService
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override async Task ChangeCategoryForNoHardLinksAsync(List<object>? downloads, HashSet<string> excludedHashes) public override async Task ChangeCategoryForNoHardLinksAsync(List<object>? downloads, HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads)
{ {
if (downloads?.Count is null or 0) if (downloads?.Count is null or 0)
{ {
return; return;
} }
if (_downloadCleanerConfig.NoHardLinksIgnoreRootDir) throw new NotImplementedException();
{
downloads
.Cast<TorrentStatus>()
.Select(x =>
{
string? firstDir = GetRootWithFirstDirectory(x.DownloadPath);
if (string.IsNullOrEmpty(firstDir)) // if (_downloadCleanerConfig.NoHardLinksIgnoreRootDir)
{ // {
return string.Empty; // downloads
} // .Cast<TorrentStatus>()
// .Select(x =>
if (firstDir == Path.GetPathRoot(x.DownloadPath)) // {
{ // string? firstDir = GetRootWithFirstDirectory(x.DownloadPath);
return string.Empty; //
} // if (string.IsNullOrEmpty(firstDir))
// {
return firstDir; // return string.Empty;
}) // }
.Where(x => !string.IsNullOrEmpty(x)) //
.Distinct() // if (firstDir == Path.GetPathRoot(x.DownloadPath))
.ToList() // {
.ForEach(x => // return string.Empty;
{ // }
_logger.LogTrace("populating file counts from {dir}", x); //
// return firstDir;
if (!Directory.Exists(x)) // })
{ // .Where(x => !string.IsNullOrEmpty(x))
throw new ValidationException($"directory \"{x}\" does not exist"); // .Distinct()
} // .ToList()
// .ForEach(x =>
_hardLinkFileService.PopulateFileCounts(x); // {
}); // _logger.LogTrace("populating file counts from {dir}", x);
} //
// if (!Directory.Exists(x))
foreach (TorrentStatus download in downloads.Cast<TorrentStatus>()) // {
{ // throw new ValidationException($"directory \"{x}\" does not exist");
if (string.IsNullOrEmpty(download.Hash)) // }
{ //
_logger.LogDebug("skip | download hash is null for {name}", download.Name); // _hardLinkFileService.PopulateFileCounts(x);
continue; // });
} // }
//
if (excludedHashes.Any(x => x.Equals(download.Hash, StringComparison.InvariantCultureIgnoreCase))) // foreach (TorrentStatus download in downloads.Cast<TorrentStatus>())
{ // {
_logger.LogDebug("skip | download is used by an arr | {name}", download.Name); // if (string.IsNullOrEmpty(download.Hash))
continue; // {
} // _logger.LogDebug("skip | download hash is null for {name}", download.Name);
// continue;
ContextProvider.Set("downloadName", download.Name); // }
ContextProvider.Set("hash", download.Hash); //
// if (excludedHashes.Any(x => x.Equals(download.Hash, StringComparison.InvariantCultureIgnoreCase)))
DelugeContents? contents = null; // {
try // _logger.LogDebug("skip | download is used by an arr | {name}", download.Name);
{ // continue;
contents = await _client.GetTorrentFiles(download.Hash); // }
} //
catch (Exception exception) // ContextProvider.Set("downloadName", download.Name);
{ // ContextProvider.Set("hash", download.Hash);
_logger.LogDebug(exception, "failed to find torrent files for {name}", download.Name); //
continue; // DelugeContents? contents = null;
} // try
// {
bool hasHardlinks = false; // contents = await _client.GetTorrentFiles(download.Hash);
// }
ProcessFiles(contents?.Contents, (name, file) => // catch (Exception exception)
{ // {
string filePath = Path.Combine(download.DownloadPath, file.Path); // _logger.LogDebug(exception, "failed to find torrent files for {name}", download.Name);
// continue;
long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, _downloadCleanerConfig.NoHardLinksIgnoreRootDir); // }
//
if (hardlinkCount < 0) // bool hasHardlinks = false;
{ //
_logger.LogDebug("skip | could not get file properties | {name}", download.Name); // ProcessFiles(contents?.Contents, (name, file) =>
hasHardlinks = true; // {
return; // string filePath = Path.Combine(download.DownloadPath, file.Path);
} //
// long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, _downloadCleanerConfig.NoHardLinksIgnoreRootDir);
if (hardlinkCount > 0) //
{ // if (hardlinkCount < 0)
hasHardlinks = true; // {
} // _logger.LogDebug("skip | could not get file properties | {name}", download.Name);
}); // hasHardlinks = true;
// return;
if (hasHardlinks) // }
{ //
_logger.LogDebug("skip | download has hardlinks | {name}", download.Name); // if (hardlinkCount > 0)
continue; // {
} // hasHardlinks = true;
// }
await _dryRunInterceptor.InterceptAsync(ChangeLabel, download.Hash, _downloadCleanerConfig.NoHardLinksCategory); // });
//
_logger.LogInformation("category changed for {name}", download.Name); // if (hasHardlinks)
// {
await _notifier.NotifyCategoryChanged(download.Label, _downloadCleanerConfig.NoHardLinksCategory); // _logger.LogDebug("skip | download has hardlinks | {name}", download.Name);
} // continue;
// }
//
// await _dryRunInterceptor.InterceptAsync(ChangeLabel, download.Hash, _downloadCleanerConfig.NoHardLinksCategory);
//
// _logger.LogInformation("category changed for {name}", download.Name);
//
// await _notifier.NotifyCategoryChanged(download.Label, _downloadCleanerConfig.NoHardLinksCategory);
// }
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -88,7 +88,7 @@ public abstract class DownloadService : IDownloadService
public abstract Task CleanDownloadsAsync(List<object>? downloads, List<CleanCategory> categoriesToClean, HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads); public abstract Task CleanDownloadsAsync(List<object>? downloads, List<CleanCategory> categoriesToClean, HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads);
/// <inheritdoc/> /// <inheritdoc/>
public abstract Task ChangeCategoryForNoHardLinksAsync(List<object>? downloads, HashSet<string> excludedHashes); public abstract Task ChangeCategoryForNoHardLinksAsync(List<object>? downloads, HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads);
/// <inheritdoc/> /// <inheritdoc/>
public abstract Task CreateCategoryAsync(string name); public abstract Task CreateCategoryAsync(string name);
@@ -74,7 +74,7 @@ public class DummyDownloadService : DownloadService
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override Task ChangeCategoryForNoHardLinksAsync(List<object>? downloads, HashSet<string> excludedHashes) public override Task ChangeCategoryForNoHardLinksAsync(List<object>? downloads, HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@@ -61,14 +61,16 @@ public interface IDownloadService : IDisposable
/// <param name="downloads">The downloads to clean.</param> /// <param name="downloads">The downloads to clean.</param>
/// <param name="categoriesToClean">The categories that should be cleaned.</param> /// <param name="categoriesToClean">The categories that should be cleaned.</param>
/// <param name="excludedHashes">The hashes that should not be cleaned.</param> /// <param name="excludedHashes">The hashes that should not be cleaned.</param>
/// <param name="ignoredDownloads">The downloads to ignore from processing.</param>
Task CleanDownloadsAsync(List<object>? downloads, List<CleanCategory> categoriesToClean, HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads); Task CleanDownloadsAsync(List<object>? downloads, List<CleanCategory> categoriesToClean, HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads);
/// <summary> /// <summary>
/// Changes the category for downloads that have no hardlinks. /// Changes the category for downloads that have no hardlinks.
/// </summary> /// </summary>
/// <param name="downloads">The downloads to change.</param> /// <param name="downloads">The downloads to change.</param>
/// <param name="excludedHashes"></param> /// <param name="excludedHashes">The hashes that should not be cleaned.</param>
Task ChangeCategoryForNoHardLinksAsync(List<object>? downloads, HashSet<string> excludedHashes); /// <param name="ignoredDownloads">The downloads to ignore from processing.</param>
Task ChangeCategoryForNoHardLinksAsync(List<object>? downloads, HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads);
/// <summary> /// <summary>
/// Deletes a download item. /// Deletes a download item.
@@ -271,10 +271,16 @@ public class QBitService : DownloadService, IQBitService
continue; continue;
} }
if (excludedHashes.Any(x => x.Equals(download.Hash, StringComparison.InvariantCultureIgnoreCase)))
{
_logger.LogDebug("skip | download is used by an arr | {name}", download.Name);
continue;
}
IReadOnlyList<TorrentTracker> trackers = await GetTrackersAsync(download.Hash); IReadOnlyList<TorrentTracker> trackers = await GetTrackersAsync(download.Hash);
if (ignoredDownloads.Count > 0 && if (ignoredDownloads.Count > 0 &&
(download.ShouldIgnore(ignoredDownloads) || trackers.Any(x => x.ShouldIgnore(ignoredDownloads)) is true)) (download.ShouldIgnore(ignoredDownloads) || trackers.Any(x => x.ShouldIgnore(ignoredDownloads))))
{ {
_logger.LogInformation("skip | download is ignored | {name}", download.Name); _logger.LogInformation("skip | download is ignored | {name}", download.Name);
continue; continue;
@@ -288,12 +294,6 @@ public class QBitService : DownloadService, IQBitService
continue; continue;
} }
if (excludedHashes.Any(x => x.Equals(download.Hash, StringComparison.InvariantCultureIgnoreCase)))
{
_logger.LogDebug("skip | download is used by an arr | {name}", download.Name);
continue;
}
if (!_downloadCleanerConfig.DeletePrivate) if (!_downloadCleanerConfig.DeletePrivate)
{ {
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(download.Hash); TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(download.Hash);
@@ -351,57 +351,23 @@ public class QBitService : DownloadService, IQBitService
await _client.AddCategoryAsync(name); await _client.AddCategoryAsync(name);
} }
public override async Task ChangeCategoryForNoHardLinksAsync(List<object>? downloads, HashSet<string> excludedHashes) public override async Task ChangeCategoryForNoHardLinksAsync(List<object>? downloads, HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads)
{ {
if (downloads?.Count is null or 0) if (downloads?.Count is null or 0)
{ {
return; return;
} }
if (_downloadCleanerConfig.NoHardLinksIgnoreRootDir) if (!string.IsNullOrEmpty(_downloadCleanerConfig.UnlinkedIgnoredRootDir))
{ {
downloads _hardLinkFileService.PopulateFileCounts(_downloadCleanerConfig.UnlinkedIgnoredRootDir);
.Cast<TorrentInfo>()
.Select(x =>
{
string? firstDir = GetRootWithFirstDirectory(x.SavePath);
if (string.IsNullOrEmpty(firstDir))
{
return string.Empty;
}
if (firstDir == Path.GetPathRoot(x.SavePath))
{
return string.Empty;
}
return firstDir;
})
.Where(x => !string.IsNullOrEmpty(x))
.Distinct()
.ToList()
.ForEach(x =>
{
_logger.LogTrace("populating file counts from {dir}", x);
if (!Directory.Exists(x))
{
throw new ValidationException($"directory \"{x}\" does not exist");
}
_hardLinkFileService.PopulateFileCounts(x);
});
} }
foreach (TorrentInfo download in downloads) foreach (TorrentInfo download in downloads)
{ {
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(download.Hash); if (string.IsNullOrEmpty(download.Hash))
if (files is null)
{ {
_logger.LogDebug("failed to find files for {name}", download.Name); continue;
return;
} }
if (excludedHashes.Any(x => x.Equals(download.Hash, StringComparison.InvariantCultureIgnoreCase))) if (excludedHashes.Any(x => x.Equals(download.Hash, StringComparison.InvariantCultureIgnoreCase)))
@@ -410,6 +376,23 @@ public class QBitService : DownloadService, IQBitService
continue; continue;
} }
IReadOnlyList<TorrentTracker> trackers = await GetTrackersAsync(download.Hash);
if (ignoredDownloads.Count > 0 &&
(download.ShouldIgnore(ignoredDownloads) || trackers.Any(x => x.ShouldIgnore(ignoredDownloads))))
{
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
continue;
}
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(download.Hash);
if (files is null)
{
_logger.LogDebug("failed to find files for {name}", download.Name);
continue;
}
ContextProvider.Set("downloadName", download.Name); ContextProvider.Set("downloadName", download.Name);
ContextProvider.Set("hash", download.Hash); ContextProvider.Set("hash", download.Hash);
bool hasHardlinks = false; bool hasHardlinks = false;
@@ -419,20 +402,17 @@ public class QBitService : DownloadService, IQBitService
if (!file.Index.HasValue) if (!file.Index.HasValue)
{ {
_logger.LogDebug("skip | file index is null for {name}", download.Name); _logger.LogDebug("skip | file index is null for {name}", download.Name);
return; hasHardlinks = true;
break;
} }
var ceva = Path.Combine(download.ContentPath, file.Name); // string filePath = Path.Combine(Directory.Exists(download.ContentPath)
var ceva2 = Path.Combine(download.SavePath, file.Name); // ? download.ContentPath
// : download.SavePath, file.Name
// );
string filePath = string.Join(Path.DirectorySeparatorChar, Path.Combine(download.SavePath, file.Name).Split(['\\', '/'])); // TODO
string filePath = Path.Combine(Directory.Exists(download.ContentPath) long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, !string.IsNullOrEmpty(_downloadCleanerConfig.UnlinkedIgnoredRootDir));
? download.ContentPath
: download.SavePath, file.Name
);
filePath = string.Join(Path.DirectorySeparatorChar, Path.Combine(download.SavePath, file.Name).Split(['\\', '/'])); // TODO
// TODO add config for root directory
long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, _downloadCleanerConfig.NoHardLinksIgnoreRootDir);
if (hardlinkCount < 0) if (hardlinkCount < 0)
{ {
@@ -444,6 +424,7 @@ public class QBitService : DownloadService, IQBitService
if (hardlinkCount > 0) if (hardlinkCount > 0)
{ {
hasHardlinks = true; hasHardlinks = true;
break;
} }
} }
@@ -453,13 +434,13 @@ public class QBitService : DownloadService, IQBitService
continue; continue;
} }
await _dryRunInterceptor.InterceptAsync(ChangeCategory, download.Hash, _downloadCleanerConfig.NoHardLinksCategory); await _dryRunInterceptor.InterceptAsync(ChangeCategory, download.Hash, _downloadCleanerConfig.UnlinkedTargetCategory);
_logger.LogInformation("category changed for {name}", download.Name); _logger.LogInformation("category changed for {name}", download.Name);
await _notifier.NotifyCategoryChanged(download.Category, _downloadCleanerConfig.NoHardLinksCategory); await _notifier.NotifyCategoryChanged(download.Category, _downloadCleanerConfig.UnlinkedTargetCategory);
download.Category = _downloadCleanerConfig.NoHardLinksCategory; download.Category = _downloadCleanerConfig.UnlinkedTargetCategory;
} }
} }
@@ -269,6 +269,11 @@ public class TransmissionService : DownloadService, ITransmissionService
public override async Task CleanDownloadsAsync(List<object>? downloads, List<CleanCategory> categoriesToClean, public override async Task CleanDownloadsAsync(List<object>? downloads, List<CleanCategory> categoriesToClean,
HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads) HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads)
{ {
if (downloads?.Count is null or 0)
{
return;
}
foreach (TorrentInfo download in downloads) foreach (TorrentInfo download in downloads)
{ {
if (string.IsNullOrEmpty(download.HashString)) if (string.IsNullOrEmpty(download.HashString))
@@ -341,117 +346,120 @@ public class TransmissionService : DownloadService, ITransmissionService
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override async Task ChangeCategoryForNoHardLinksAsync(List<object>? downloads, HashSet<string> excludedHashes) public override async Task ChangeCategoryForNoHardLinksAsync(List<object>? downloads, HashSet<string> excludedHashes, IReadOnlyList<string> ignoredDownloads)
{ {
if (downloads?.Count is null or 0) if (downloads?.Count is null or 0)
{ {
return; return;
} }
if (_downloadCleanerConfig.NoHardLinksIgnoreRootDir) // TODO ignored downloads
{ throw new NotImplementedException();
downloads
.Cast<TorrentInfo>()
.Select(x =>
{
if (x.DownloadDir == null)
{
return string.Empty;
}
string? firstDir = GetRootWithFirstDirectory(x.DownloadDir); // if (_downloadCleanerConfig.NoHardLinksIgnoreRootDir)
// {
if (string.IsNullOrEmpty(firstDir)) // downloads
{ // .Cast<TorrentInfo>()
return string.Empty; // .Select(x =>
} // {
// if (x.DownloadDir == null)
if (firstDir == Path.GetPathRoot(x.DownloadDir)) // {
{ // return string.Empty;
return string.Empty; // }
} //
// string? firstDir = GetRootWithFirstDirectory(x.DownloadDir);
return firstDir; //
}) // if (string.IsNullOrEmpty(firstDir))
.Where(x => !string.IsNullOrEmpty(x)) // {
.Distinct() // return string.Empty;
.ToList() // }
.ForEach(x => //
{ // if (firstDir == Path.GetPathRoot(x.DownloadDir))
_logger.LogTrace("populating file counts from {dir}", x); // {
// return string.Empty;
if (!Directory.Exists(x)) // }
{ //
throw new ValidationException($"directory \"{x}\" does not exist"); // return firstDir;
} // })
// .Where(x => !string.IsNullOrEmpty(x))
_hardLinkFileService.PopulateFileCounts(x); // .Distinct()
}); // .ToList()
} // .ForEach(x =>
// {
foreach (TorrentInfo download in downloads.Cast<TorrentInfo>()) // _logger.LogTrace("populating file counts from {dir}", x);
{ //
if (string.IsNullOrEmpty(download.HashString) || download.DownloadDir == null) // if (!Directory.Exists(x))
{ // {
_logger.LogDebug("skip | download hash or download directory is null for {name}", download.Name); // throw new ValidationException($"directory \"{x}\" does not exist");
continue; // }
} //
// _hardLinkFileService.PopulateFileCounts(x);
if (excludedHashes.Any(x => x.Equals(download.HashString, StringComparison.InvariantCultureIgnoreCase))) // });
{ // }
_logger.LogDebug("skip | download is used by an arr | {name}", download.Name); //
continue; // foreach (TorrentInfo download in downloads.Cast<TorrentInfo>())
} // {
// if (string.IsNullOrEmpty(download.HashString) || download.DownloadDir == null)
ContextProvider.Set("downloadName", download.Name); // {
ContextProvider.Set("hash", download.HashString); // _logger.LogDebug("skip | download hash or download directory is null for {name}", download.Name);
// continue;
bool hasHardlinks = false; // }
//
if (download.Files != null) // if (excludedHashes.Any(x => x.Equals(download.HashString, StringComparison.InvariantCultureIgnoreCase)))
{ // {
foreach (TransmissionTorrentFiles file in download.Files) // _logger.LogDebug("skip | download is used by an arr | {name}", download.Name);
{ // continue;
string filePath = Path.Combine(download.DownloadDir, file.Name); // }
//
long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, _downloadCleanerConfig.NoHardLinksIgnoreRootDir); // ContextProvider.Set("downloadName", download.Name);
// ContextProvider.Set("hash", download.HashString);
if (hardlinkCount < 0) //
{ // bool hasHardlinks = false;
_logger.LogDebug("skip | could not get file properties | {name}", download.Name); //
hasHardlinks = true; // if (download.Files != null)
break; // {
} // foreach (TransmissionTorrentFiles file in download.Files)
// {
if (hardlinkCount > 0) // string filePath = Path.Combine(download.DownloadDir, file.Name);
{ //
hasHardlinks = true; // long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, _downloadCleanerConfig.NoHardLinksIgnoreRootDir);
break; //
} // if (hardlinkCount < 0)
} // {
} // _logger.LogDebug("skip | could not get file properties | {name}", download.Name);
// hasHardlinks = true;
if (hasHardlinks) // break;
{ // }
_logger.LogDebug("skip | download has hardlinks | {name}", download.Name); //
continue; // if (hardlinkCount > 0)
} // {
// hasHardlinks = true;
// Get the current category (directory name) // break;
string currentCategory = Path.GetFileName(Path.TrimEndingDirectorySeparator(download.DownloadDir)); // }
// }
// Create the new location path // }
string newLocation = Path.Combine( //
Path.GetDirectoryName(Path.TrimEndingDirectorySeparator(download.DownloadDir)) ?? string.Empty, // if (hasHardlinks)
_downloadCleanerConfig.NoHardLinksCategory // {
); // _logger.LogDebug("skip | download has hardlinks | {name}", download.Name);
// continue;
await _dryRunInterceptor.InterceptAsync(MoveDownload, download.Id, newLocation); // }
//
_logger.LogInformation("category changed for {name}", download.Name); // // Get the current category (directory name)
// string currentCategory = Path.GetFileName(Path.TrimEndingDirectorySeparator(download.DownloadDir));
await _notifier.NotifyCategoryChanged(currentCategory, _downloadCleanerConfig.NoHardLinksCategory); //
} // // Create the new location path
// string newLocation = Path.Combine(
// Path.GetDirectoryName(Path.TrimEndingDirectorySeparator(download.DownloadDir)) ?? string.Empty,
// _downloadCleanerConfig.NoHardLinksCategory
// );
//
// await _dryRunInterceptor.InterceptAsync(MoveDownload, download.Id, newLocation);
//
// _logger.LogInformation("category changed for {name}", download.Name);
//
// await _notifier.NotifyCategoryChanged(currentCategory, _downloadCleanerConfig.NoHardLinksCategory);
// }
} }
[DryRunSafeguard] [DryRunSafeguard]
@@ -22,6 +22,8 @@ public class HardLinkFileService : IHardLinkFileService
public void PopulateFileCounts(string directoryPath) public void PopulateFileCounts(string directoryPath)
{ {
_logger.LogTrace("populating file counts from {dir}", directoryPath);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
_windowsHardLinkFileService.PopulateFileCounts(directoryPath); _windowsHardLinkFileService.PopulateFileCounts(directoryPath);
@@ -51,15 +51,11 @@ public class UnixHardLinkFileService : IHardLinkFileService, IDisposable
{ {
try try
{ {
foreach (var file in Directory.EnumerateFiles(directoryPath, "*", SearchOption.AllDirectories)) // traverse all files in the ignored path and subdirectories
foreach (string file in Directory.EnumerateFiles(directoryPath, "*", SearchOption.AllDirectories))
{ {
AddInodeToCount(file); AddInodeToCount(file);
} }
foreach (var dir in Directory.EnumerateDirectories(directoryPath, "*", SearchOption.AllDirectories))
{
AddInodeToCount(dir);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -57,16 +57,11 @@ public class WindowsHardLinkFileService : IHardLinkFileService, IDisposable
{ {
try try
{ {
// Traverse all files and directories in the ignored path // traverse all files in the ignored path and subdirectories
foreach (var file in Directory.EnumerateFiles(directoryPath, "*", SearchOption.AllDirectories)) foreach (string file in Directory.EnumerateFiles(directoryPath, "*", SearchOption.AllDirectories))
{ {
AddFileIndexToCount(file); AddFileIndexToCount(file);
} }
foreach (var dir in Directory.EnumerateDirectories(directoryPath, "*", SearchOption.AllDirectories))
{
AddFileIndexToCount(dir);
}
} }
catch (Exception ex) catch (Exception ex)
{ {