< Summary

Line coverage
89%
Covered lines: 34
Uncovered lines: 4
Coverable lines: 38
Total lines: 158
Line coverage: 89.4%
Branch coverage
100%
Covered branches: 4
Total branches: 4
Branch coverage: 100%
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: TemporaryPathFor(...)100%11100%
File 2: WriteAsync()100%22100%
File 2: DiscardTemporaryFile(...)100%22100%

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/Proxy/ProxyStagingWriter.cs

#LineLine coverage
 1using Anichron.Core;
 2using Anichron.Core.Domain;
 3using Anichron.Worker.Ingestion.Pipeline;
 4using Anichron.Worker.Settings;
 5using Microsoft.Extensions.Options;
 6using NodaTime;
 7using System.IO.Abstractions;
 8
 9namespace Anichron.Worker.Ingestion.Proxy;
 10
 4711internal sealed partial class ProxyStagingWriter(
 4712    IFileSystem fileSystem,
 4713    IOptions<WorkerSettings> settings,
 4714    IClock clock,
 4715    IGuidFactory guidFactory,
 4716    ILogger<ProxyStagingWriter> logger)
 17{
 18    // A proxy is written under this suffix and renamed into place once complete, so a crash or a
 19    // killed transcode can never leave a truncated file wearing a valid proxy's name. Leftovers
 20    // are recognisable by this suffix, both to an operator and to the orphan sweeper (#174).
 21    internal const string TemporaryFileSuffix = ".tmp";
 22
 23    // Naming a leftover and finding one are the same rule, so they live together: a consumer that
 24    // hunts for staging artifacts — compensation here, the orphan sweeper in #174 — must never
 25    // re-express how this writer names them.
 26    internal const string TemporaryFileSearchPattern = "*" + TemporaryFileSuffix;
 27
 28    internal static string TemporaryPathFor(string proxyAbsolutePath)
 7929        => proxyAbsolutePath + TemporaryFileSuffix;
 30
 31    // The one place a proxy reaches its final name: produceAsync writes the bytes to the temporary
 32    // path it is handed, and only a complete file is renamed into place and registered on the
 33    // context. The rename stays within one directory, so it is atomic.
 34    internal async Task WriteAsync(
 35        IngestionContext context,
 36        string relativePath,
 37        ProxyType proxyType,
 38        Func<string, CancellationToken, Task> produceAsync,
 39        CancellationToken ct)
 7840    {
 7841        var proxyAbsolutePath = fileSystem.Path.Combine(settings.Value.ProxyPath, relativePath);
 7842        var temporaryAbsolutePath = TemporaryPathFor(proxyAbsolutePath);
 43        try
 7844        {
 7845            if (fileSystem.Path.GetDirectoryName(proxyAbsolutePath) is { } directory)
 7846                fileSystem.Directory.CreateDirectory(directory);
 47
 7848            await produceAsync(temporaryAbsolutePath, ct);
 7349            var sizeBytes = fileSystem.FileInfo.New(temporaryAbsolutePath).Length;
 7350            fileSystem.File.Move(temporaryAbsolutePath, proxyAbsolutePath, overwrite: true);
 51
 52            // Registered at the moment it lands, so the context always reflects what is on disk
 53            // and compensation can delete precisely the files this attempt wrote.
 7354            context.AddProxyFile(
 7355                ProxyFileBuilder.Build(context, relativePath, proxyType, sizeBytes, guidFactory, clock));
 7356            Log.ProxyWritten(logger, proxyType, sizeBytes, context.Item.RelativePath);
 7357        }
 58        finally
 7859        {
 7860            DiscardTemporaryFile(temporaryAbsolutePath);
 7861        }
 7362    }
 63
 64    // Runs while an exception may already be in flight — including a shutdown cancellation, where
 65    // the half-written file is dropped but completed proxies are kept. A failure to delete must
 66    // never replace the error that caused the cleanup.
 67    private void DiscardTemporaryFile(string temporaryPath)
 7868    {
 69        try
 7870        {
 7871            if (fileSystem.File.Exists(temporaryPath))
 472                fileSystem.File.Delete(temporaryPath);
 7773        }
 174        catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
 175        {
 176            Log.DiscardFailed(logger, temporaryPath, ex);
 177        }
 7878    }
 79
 80    private static partial class Log
 81    {
 82        [LoggerMessage(Level = LogLevel.Debug, Message = "Wrote {ProxyType} proxy ({SizeBytes} B) for {RelativePath}.")]
 83        public static partial void ProxyWritten(ILogger logger, ProxyType proxyType, long sizeBytes, string relativePath
 84
 85        [LoggerMessage(Level = LogLevel.Warning, Message = "Could not delete temporary proxy file {Path}.")]
 86        public static partial void DiscardFailed(ILogger logger, string path, Exception ex);
 87    }
 88}