From abe896a7abb30a7a07e2ba7d44d088e9d19f8e73 Mon Sep 17 00:00:00 2001 From: Fritiof Hedman Date: Sun, 7 Jun 2026 14:46:00 +0200 Subject: [PATCH] 5.1 FramesController.UploadImage (multipart) Co-Authored-By: Claude Opus 4.7 (1M context) --- IMPLEMENTATION.md | 2 +- .../Controllers/FramesController.cs | 55 +++++++++++++++++++ src/FrameProcessor/Program.cs | 4 ++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/FrameProcessor/Controllers/FramesController.cs diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index a4dc15a..fbab822 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -129,7 +129,7 @@ Each type lives in `src/FrameProcessor/Domain/`. Tests in `tests/FrameProcessor. ## Phase 5 — First end-to-end happy path (no MQTT yet) -### [ ] 5.1 `FramesController.UploadImage` (multipart) +### [x] 5.1 `FramesController.UploadImage` (multipart) - Route: `POST /api/frames/{name}/image`. - Resolve frame by name → 404 if unknown. - Read multipart file part `image` (return 400 if missing). diff --git a/src/FrameProcessor/Controllers/FramesController.cs b/src/FrameProcessor/Controllers/FramesController.cs new file mode 100644 index 0000000..dafeb82 --- /dev/null +++ b/src/FrameProcessor/Controllers/FramesController.cs @@ -0,0 +1,55 @@ +using FrameProcessor.Configuration; +using FrameProcessor.Domain; +using FrameProcessor.ImagePipeline; +using FrameProcessor.Storage; +using Microsoft.AspNetCore.Mvc; + +namespace FrameProcessor.Controllers; + +[ApiController] +[Route("api/frames")] +public sealed class FramesController : ControllerBase +{ + private readonly FramesRegistry _frames; + private readonly IImagePipeline _pipeline; + private readonly ImageStore _store; + + public FramesController(FramesRegistry frames, IImagePipeline pipeline, ImageStore store) + { + _frames = frames; + _pipeline = pipeline; + _store = store; + } + + [HttpPost("{name}/image")] + public async Task UploadImage(string name, CancellationToken cancellationToken) + { + if (!FrameName.TryParse(name, out var frameName) || !_frames.TryGetByName(frameName, out var frame)) + { + return NotFound(); + } + + var file = Request.Form.Files.GetFile("image"); + if (file is null || file.Length == 0) + { + return BadRequest(new { error = "Missing 'image' file part." }); + } + + byte[] pngBytes; + await using (var stream = file.OpenReadStream()) + { + pngBytes = _pipeline.Process(stream, frame); + } + + await _store.WriteAsync(frame.Mac, pngBytes, cancellationToken).ConfigureAwait(false); + + return Ok(new + { + frame = frame.Name.Value, + mac = frame.Mac.ToString(), + url = $"/i/{frame.Mac}.png", + processedAt = DateTimeOffset.UtcNow, + mqttPublished = false, + }); + } +} diff --git a/src/FrameProcessor/Program.cs b/src/FrameProcessor/Program.cs index 76852f5..be676dc 100644 --- a/src/FrameProcessor/Program.cs +++ b/src/FrameProcessor/Program.cs @@ -1,4 +1,6 @@ using FrameProcessor.Configuration; +using FrameProcessor.ImagePipeline; +using FrameProcessor.Storage; var builder = WebApplication.CreateBuilder(args); @@ -34,6 +36,8 @@ builder.Services.AddOptions() .Bind(builder.Configuration); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); var app = builder.Build();