using Api.SeaHavenIndustries.Observability; using Data.SeaHavenIndustries; using Microsoft.EntityFrameworkCore; using Sentry; namespace Api.SeaHavenIndustries.Helper { public class VendorDocumentScanWorker : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private readonly IWebHostEnvironment _environment; private readonly ILogger _logger; private readonly IHub _sentryHub; public VendorDocumentScanWorker( IServiceScopeFactory scopeFactory, IWebHostEnvironment environment, ILogger logger, IHub sentryHub) { _scopeFactory = scopeFactory; _environment = environment; _logger = logger; _sentryHub = sentryHub; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await ScanPendingAsync(stoppingToken); await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); } } private async Task ScanPendingAsync(CancellationToken cancellationToken) { using var transaction = SentryObservability.BeginBackgroundTransaction( _sentryHub, "vendor-documents.scan-pending", $"{nameof(VendorDocumentScanWorker)}.{nameof(ScanPendingAsync)}"); try { using var scope = _scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var scanner = scope.ServiceProvider.GetRequiredService(); var pending = await db.VendorCompletionDocuments .Where(document => document.ScanStatus == "Pending") .OrderBy(document => document.CreatedDate) .Take(20) .ToListAsync(cancellationToken); foreach (var document in pending) { var path = VendorDocumentStorage.ResolvePath(_environment.ContentRootPath, document); var result = await scanner.ScanAsync(path, cancellationToken); if (result == DocumentScanResult.Unavailable) { _logger.LogWarning("Vendor document scan service unavailable; document {DocumentId} remains quarantined", document.Id); continue; } document.ScanStatus = result == DocumentScanResult.Passed ? "Passed" : "Rejected"; document.ReviewStatus = result == DocumentScanResult.Passed ? "Processing" : "Rejected"; document.RejectionReason = result == DocumentScanResult.Infected ? "The upload failed malware scanning." : document.RejectionReason; document.ScannedAt = DateTime.UtcNow; document.LastModificationTime = DateTime.UtcNow; await db.SaveChangesAsync(cancellationToken); if (result == DocumentScanResult.Infected && File.Exists(path)) File.Delete(path); } transaction.FinishOk(); } catch (OperationCanceledException) { transaction.FinishCancelled(); throw; } catch (Exception ex) { transaction.FinishError(ex); throw; } } } public static class VendorDocumentStorage { public static string Root(string contentRoot) => Path.Combine(contentRoot, "App_Data", "vendor-documents"); public static string ResolvePath(string contentRoot, int vendorId, int dispatchId, string storedFileName) => Path.Combine(Root(contentRoot), vendorId.ToString(), dispatchId.ToString(), storedFileName); public static string ResolvePath(string contentRoot, VendorCompletionDocument document) => ResolvePath(contentRoot, document.VendorId, document.DispatchId, document.StoredFileName); } }