< Summary

Line coverage
95%
Covered lines: 78
Uncovered lines: 4
Coverable lines: 82
Total lines: 532
Line coverage: 95.1%
Branch coverage
87%
Covered branches: 7
Total branches: 8
Branch coverage: 87.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: .cctor()100%210%
File 2: .ctor(...)100%11100%
File 2: get_Order()100%210%
File 2: CanInvoke(...)100%210%
File 2: InvokeAsync()100%22100%
File 2: BuildAsset(...)100%44100%
File 2: BuildSecondaryAsset(...)50%22100%
File 2: FallbackDate(...)100%11100%

File(s)

/home/runner/work/anichron/anichron/src/.artifacts/obj/Anichron.Worker/debug/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs

File '/home/runner/work/anichron/anichron/src/.artifacts/obj/Anichron.Worker/debug/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs' does not exist (any more).

/home/runner/work/anichron/anichron/src/Anichron.Worker/Ingestion/Middlewares/PersistenceMiddleware.cs

#LineLine coverage
 1using Anichron.Core;
 2using Anichron.Core.Data;
 3using Anichron.Core.Data.Repository;
 4using Anichron.Core.Domain;
 5using Anichron.Worker.Ingestion.Pipeline;
 6using NodaTime;
 7using System.IO.Abstractions;
 8
 9namespace Anichron.Worker.Ingestion.Middlewares;
 10
 1711internal sealed partial class PersistenceMiddleware(
 1712    IMediaAssetRepository repository,
 1713    IUnitOfWork unitOfWork,
 1714    IFileSystem fileSystem,
 1715    IClock clock,
 1716    IGuidFactory guidFactory,
 1717    ILogger<PersistenceMiddleware> logger) : IIngestionMiddleware
 18{
 019    public int Order => IngestionOrder.Persistence;
 020    public bool CanInvoke(IngestionContext context) => true;
 21
 22    public async Task InvokeAsync(IngestionContext context, IngestionDelegate next, CancellationToken ct)
 1723    {
 1724        var asset = BuildAsset(context);
 25
 1726        if (context.Item.SecondaryFile is { } secondary)
 527        {
 28            // Ordering guarantees ContentHashingMiddleware ran first and set SecondaryHash; suppression is safe.
 529            var secondaryAsset = BuildSecondaryAsset(secondary, context.SecondaryHash!, context);
 530            asset.PairedAssetId = secondaryAsset.Id;
 531            repository.Add(secondaryAsset);
 532        }
 33
 1734        repository.Add(asset);
 1735        await unitOfWork.SaveChangesAsync(ct);
 1736        context.Asset = asset;
 1737        Log.Persisted(logger, context.Item.RelativePath, asset.Id);
 1738        await next(context, ct);
 1739    }
 40
 41    private MediaAsset BuildAsset(IngestionContext context)
 1742    {
 1743        var relativePath = context.Item.RelativePath;
 1744        var mediaType = context.Item.PrimaryMediaType;
 1745        var exif = context.Exif!;
 1746        var dateCaptured = exif.DateCaptured ?? FallbackDate(context.Item.AbsolutePath);
 47
 1748        return new MediaAsset
 1749        {
 1750            Id = context.AssetId,
 1751            StorageConfigId = context.Config.Id,
 1752            FilePath = relativePath,
 1753            FileName = Path.GetFileName(relativePath),
 1754            ContentHash = context.ContentHash!,
 1755            DateCaptured = dateCaptured,
 1756            Month = dateCaptured.Month,
 1757            Day = dateCaptured.Day,
 1758            Year = dateCaptured.Year,
 1759            MediaType = mediaType,
 1760            IsSoftDeleted = false,
 1761            LastSeenOnNas = clock.GetCurrentInstant(),
 1762            ProxyFiles = [.. context.ProxyFiles],
 1763            Metadata = new Metadata
 1764            {
 1765                Width = exif.Width,
 1766                Height = exif.Height,
 1767                OrientationDegrees = exif.OrientationDegrees,
 1768                Latitude = exif.Latitude,
 1769                Longitude = exif.Longitude,
 1770                CameraMake = exif.CameraMake,
 1771                CameraModel = exif.CameraModel,
 1772                LensModel = exif.LensModel,
 1773                DurationInSeconds = exif.DurationInSeconds,
 1774            },
 1775        };
 1776    }
 77
 78    private MediaAsset BuildSecondaryAsset(SecondaryFileDescriptor secondary, string hash, IngestionContext context)
 579    {
 580        var exif = context.Exif!;
 581        var dateCaptured = exif.DateCaptured ?? FallbackDate(secondary.AbsolutePath);
 82
 583        return new MediaAsset
 584        {
 585            Id = guidFactory.NewGuid(),
 586            StorageConfigId = context.Config.Id,
 587            FilePath = secondary.RelativePath,
 588            FileName = Path.GetFileName(secondary.RelativePath),
 589            ContentHash = hash,
 590            DateCaptured = dateCaptured,
 591            Month = dateCaptured.Month,
 592            Day = dateCaptured.Day,
 593            Year = dateCaptured.Year,
 594            MediaType = secondary.MediaType,
 595            IsSoftDeleted = false,
 596            LastSeenOnNas = clock.GetCurrentInstant(),
 597        };
 598    }
 99
 100    private LocalDateTime FallbackDate(string absolutePath)
 1101    {
 1102        var lastWrite = fileSystem.FileInfo.New(absolutePath).LastWriteTime;
 1103        return LocalDateTime.FromDateTime(lastWrite);
 1104    }
 105
 106    private static partial class Log
 107    {
 108        [LoggerMessage(Level = LogLevel.Information, Message = "Persisted {RelativePath} as asset {AssetId}")]
 109        public static partial void Persisted(ILogger logger, string relativePath, Guid assetId);
 110    }
 111}