mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-06-30 00:05:58 -04:00
retried webhook malware scans while the torrent metadata is not yet available
This commit is contained in:
@@ -71,6 +71,7 @@ public class MalwareBlockerIntegrationTests : IDisposable
|
||||
.Returns(new BlockFilesResult
|
||||
{
|
||||
Found = true,
|
||||
MetadataFound = true,
|
||||
ShouldRemove = true,
|
||||
DeleteReason = DeleteReason.AllFilesBlocked,
|
||||
IsPrivate = false
|
||||
@@ -201,6 +202,7 @@ public class MalwareBlockerIntegrationTests : IDisposable
|
||||
.Returns(new BlockFilesResult
|
||||
{
|
||||
Found = true,
|
||||
MetadataFound = true,
|
||||
ShouldRemove = true,
|
||||
DeleteReason = DeleteReason.AllFilesBlocked,
|
||||
IsPrivate = true
|
||||
|
||||
@@ -278,7 +278,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<List<string>>()
|
||||
)
|
||||
.Returns(new BlockFilesResult { Found = true, ShouldRemove = false });
|
||||
.Returns(new BlockFilesResult { Found = true, MetadataFound = true, ShouldRemove = false });
|
||||
|
||||
_fixture.DownloadServiceFactory
|
||||
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
||||
@@ -341,6 +341,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
.Returns(new BlockFilesResult
|
||||
{
|
||||
Found = true,
|
||||
MetadataFound = true,
|
||||
ShouldRemove = true,
|
||||
IsPrivate = false,
|
||||
DeleteReason = DeleteReason.AllFilesBlocked
|
||||
@@ -399,7 +400,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
var mockDownloadService = _fixture.CreateMockDownloadService();
|
||||
mockDownloadService
|
||||
.BlockUnwantedFilesAsync(Arg.Any<string>(), Arg.Any<List<string>>())
|
||||
.Returns(new BlockFilesResult { Found = true, ShouldRemove = false });
|
||||
.Returns(new BlockFilesResult { Found = true, MetadataFound = true, ShouldRemove = false });
|
||||
|
||||
_fixture.DownloadServiceFactory
|
||||
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
||||
@@ -477,6 +478,56 @@ public class MalwareBlockerTests : IDisposable
|
||||
t.RetryIndex == 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteInternalAsync_WhenWebhookTargetMetadataMissing_SchedulesRetry()
|
||||
{
|
||||
// Arrange
|
||||
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
||||
EnableSonarrBlocklist();
|
||||
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
||||
|
||||
var mockArrClient = Substitute.For<IArrClient>();
|
||||
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
||||
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
||||
.Returns(mockArrClient);
|
||||
|
||||
var record = new QueueRecord { Id = 1, DownloadId = "metadl-hash", Title = "MetaDL", Protocol = "torrent", SeriesId = 7, EpisodeId = 1 };
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
.Iterate(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>(), Arg.Any<Func<IReadOnlyList<QueueRecord>, Task>>(), Arg.Any<long?>())
|
||||
.Returns(ci =>
|
||||
{
|
||||
var callback = ci.ArgAt<Func<IReadOnlyList<QueueRecord>, Task>>(2);
|
||||
return callback([record]);
|
||||
});
|
||||
|
||||
// Torrent found in the client, but its metadata/file list is not ready yet
|
||||
var mockDownloadService = _fixture.CreateMockDownloadService();
|
||||
mockDownloadService
|
||||
.BlockUnwantedFilesAsync(Arg.Any<string>(), Arg.Any<List<string>>())
|
||||
.Returns(new BlockFilesResult { Found = true });
|
||||
|
||||
_fixture.DownloadServiceFactory
|
||||
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
||||
.Returns(mockDownloadService);
|
||||
|
||||
Cleanuparr.Infrastructure.Features.Context.ContextProvider.Set(
|
||||
new Cleanuparr.Infrastructure.Features.Jobs.WebhookScanTarget(
|
||||
sonarrInstance.Id, "metadl-hash", 7, InstanceType.Sonarr, RetryIndex: 0));
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
await _fixture.JobManagementService.Received(1).ScheduleMalwareBlockerWebhookRetry(
|
||||
Arg.Is<WebhookScanTarget>(t => t.DownloadId == "metadl-hash" && t.RetryIndex == 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteInternalAsync_WhenWebhookTargetIsUsenet_DoesNotScanOrRetry()
|
||||
{
|
||||
@@ -570,6 +621,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
.Returns(new BlockFilesResult
|
||||
{
|
||||
Found = true,
|
||||
MetadataFound = true,
|
||||
ShouldRemove = true,
|
||||
IsPrivate = false,
|
||||
DeleteReason = DeleteReason.AtLeastOneFileBlocked
|
||||
@@ -645,6 +697,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
.Returns(new BlockFilesResult
|
||||
{
|
||||
Found = true,
|
||||
MetadataFound = true,
|
||||
ShouldRemove = true,
|
||||
IsPrivate = true,
|
||||
DeleteReason = DeleteReason.AllFilesBlocked
|
||||
@@ -826,6 +879,7 @@ public class MalwareBlockerTests : IDisposable
|
||||
.Returns(new BlockFilesResult
|
||||
{
|
||||
Found = true,
|
||||
MetadataFound = true,
|
||||
ShouldRemove = true,
|
||||
IsPrivate = false,
|
||||
DeleteReason = DeleteReason.AllFilesBlocked
|
||||
|
||||
@@ -15,6 +15,11 @@ public sealed record BlockFilesResult
|
||||
public bool IsPrivate { get; set; }
|
||||
|
||||
public bool Found { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// True when the torrent's file list (metadata) was available so the scan could complete (or was not needed).
|
||||
/// </summary>
|
||||
public bool MetadataFound { get; set; }
|
||||
|
||||
public DeleteReason DeleteReason { get; set; } = DeleteReason.None;
|
||||
}
|
||||
@@ -55,11 +55,14 @@ public partial class DelugeService
|
||||
_logger.LogDebug(exception, "failed to find files in the download client | {name}", download.Name);
|
||||
}
|
||||
|
||||
if (contents is null)
|
||||
if (contents is null || contents.Contents?.Count is null or 0)
|
||||
{
|
||||
_logger.LogDebug("torrent has no files | {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
result.MetadataFound = true;
|
||||
|
||||
Dictionary<int, int> priorities = [];
|
||||
bool hasPriorityUpdates = false;
|
||||
long totalFiles = 0;
|
||||
|
||||
@@ -66,6 +66,8 @@ public partial class QBitService
|
||||
return result;
|
||||
}
|
||||
|
||||
result.MetadataFound = true;
|
||||
|
||||
List<int> unwantedFiles = [];
|
||||
long totalFiles = 0;
|
||||
long totalUnwantedFiles = 0;
|
||||
|
||||
@@ -64,6 +64,8 @@ public partial class RTorrentService
|
||||
return result;
|
||||
}
|
||||
|
||||
result.MetadataFound = true;
|
||||
|
||||
bool hasPriorityUpdates = false;
|
||||
long totalFiles = 0;
|
||||
long totalUnwantedFiles = 0;
|
||||
|
||||
@@ -17,27 +17,30 @@ public partial class TransmissionService
|
||||
TorrentInfo? download = await GetTorrentAsync(hash);
|
||||
BlockFilesResult result = new();
|
||||
|
||||
if (download?.FileStats is null || download.FileStats.Length == 0)
|
||||
if (download is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the {name} download client", hash, _downloadClientConfig.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (download.Files is null)
|
||||
bool isPrivate = download.IsPrivate ?? false;
|
||||
result.IsPrivate = isPrivate;
|
||||
result.Found = true;
|
||||
|
||||
if (download.FileStats?.Length is null or 0 || download.Files?.Length is null or 0)
|
||||
{
|
||||
_logger.LogDebug("torrent {hash} has no files", hash);
|
||||
_logger.LogDebug("torrent has no files | {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
result.MetadataFound = true;
|
||||
|
||||
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||
{
|
||||
_logger.LogDebug("skip | download is ignored | {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isPrivate = download.IsPrivate ?? false;
|
||||
result.IsPrivate = isPrivate;
|
||||
result.Found = true;
|
||||
SetDownloadClientContext();
|
||||
|
||||
var malwareBlockerConfig = ContextProvider.Get<ContentBlockerConfig>();
|
||||
@@ -48,7 +51,7 @@ public partial class TransmissionService
|
||||
_logger.LogDebug("skip files check | download is private | {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
List<long> unwantedFiles = [];
|
||||
long totalFiles = 0;
|
||||
long totalUnwantedFiles = 0;
|
||||
|
||||
@@ -55,6 +55,8 @@ public partial class UTorrentService
|
||||
return result;
|
||||
}
|
||||
|
||||
result.MetadataFound = true;
|
||||
|
||||
List<int> fileIndexes = new(files.Count);
|
||||
long totalUnwantedFiles = 0;
|
||||
|
||||
|
||||
@@ -297,9 +297,9 @@ public sealed class MalwareBlocker : GenericHandler
|
||||
_logger.LogDebug("No torrent clients enabled");
|
||||
}
|
||||
|
||||
if (!result.Found)
|
||||
if (!result.Found || !result.MetadataFound)
|
||||
{
|
||||
// Torrent not present in any client yet (e.g. metadata not ready) — webhook scan should retry.
|
||||
// Retry while the torrent is not yet in a client, or is present but its file list/metadata isn't ready.
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user