retried webhook malware scans while the torrent metadata is not yet available

This commit is contained in:
Flaminel
2026-06-17 14:54:27 +03:00
parent 96823adcc3
commit d1bd9fddcc
9 changed files with 87 additions and 14 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -66,6 +66,8 @@ public partial class QBitService
return result;
}
result.MetadataFound = true;
List<int> unwantedFiles = [];
long totalFiles = 0;
long totalUnwantedFiles = 0;

View File

@@ -64,6 +64,8 @@ public partial class RTorrentService
return result;
}
result.MetadataFound = true;
bool hasPriorityUpdates = false;
long totalFiles = 0;
long totalUnwantedFiles = 0;

View File

@@ -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;

View File

@@ -55,6 +55,8 @@ public partial class UTorrentService
return result;
}
result.MetadataFound = true;
List<int> fileIndexes = new(files.Count);
long totalUnwantedFiles = 0;

View File

@@ -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;
}