4.1 ImageStore

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 14:43:28 +02:00
parent ad62ce00a5
commit a0fa0205e5
3 changed files with 166 additions and 1 deletions

View File

@@ -0,0 +1,52 @@
using System.Diagnostics.CodeAnalysis;
using FrameProcessor.Configuration;
using FrameProcessor.Domain;
using Microsoft.Extensions.Options;
namespace FrameProcessor.Storage;
/// <summary>
/// Persists the latest processed PNG for each frame to <see cref="StorageOptions.ImageDirectory"/>.
/// Writes are atomic via a temp file + <see cref="File.Move(string, string, bool)"/> rename so the
/// fetch endpoint never observes a partial file (see SPEC.md §7.2, CLAUDE.md "Atomic writes only").
/// </summary>
public sealed class ImageStore
{
private readonly string _directory;
public ImageStore(IOptions<StorageOptions> options)
{
ArgumentNullException.ThrowIfNull(options);
_directory = options.Value.ImageDirectory;
Directory.CreateDirectory(_directory);
}
public async Task WriteAsync(MacAddress mac, ReadOnlyMemory<byte> bytes, CancellationToken cancellationToken)
{
var finalPath = GetPath(mac);
var tmpPath = finalPath + ".tmp";
await using (var stream = new FileStream(tmpPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
await stream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);
await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
}
File.Move(tmpPath, finalPath, overwrite: true);
}
public bool TryGetPath(MacAddress mac, [NotNullWhen(true)] out string? path)
{
var candidate = GetPath(mac);
if (File.Exists(candidate))
{
path = candidate;
return true;
}
path = null;
return false;
}
private string GetPath(MacAddress mac) => Path.Combine(_directory, $"{mac}.png");
}