From 3e01fa798077ed4f9847f4b8ccd274e9bdf6421d Mon Sep 17 00:00:00 2001 From: Fritiof Hedman Date: Sun, 7 Jun 2026 15:53:27 +0200 Subject: [PATCH] 7.2 FramesController.UploadImageUrl --- IMPLEMENTATION.md | 2 +- .../Controllers/FramesController.cs | 54 ++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 445683f..81fb219 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -182,7 +182,7 @@ Each type lives in `src/FrameProcessor/Domain/`. Tests in `tests/FrameProcessor. - `Task FetchAsync(Uri, CancellationToken)` — streams response, aborts if content exceeds `MaxBytes`. - Throws a typed exception (`ImageFetchException`) for timeout / non-2xx / too-large / redirect-loop. -### [ ] 7.2 `FramesController.UploadImageUrl` +### [x] 7.2 `FramesController.UploadImageUrl` - Route: `POST /api/frames/{name}/image-url`, body `{ "url": "..." }`. - Fetch → pipeline → store → publish. Same response shape as 5.1. - Map `ImageFetchException` to `502 Bad Gateway`. diff --git a/src/FrameProcessor/Controllers/FramesController.cs b/src/FrameProcessor/Controllers/FramesController.cs index 5ccb408..d6fca03 100644 --- a/src/FrameProcessor/Controllers/FramesController.cs +++ b/src/FrameProcessor/Controllers/FramesController.cs @@ -3,6 +3,7 @@ using FrameProcessor.Domain; using FrameProcessor.ImagePipeline; using FrameProcessor.Mqtt; using FrameProcessor.Storage; +using FrameProcessor.UrlFetch; using Microsoft.AspNetCore.Mvc; namespace FrameProcessor.Controllers; @@ -15,13 +16,20 @@ public sealed class FramesController : ControllerBase private readonly IImagePipeline _pipeline; private readonly ImageStore _store; private readonly MqttPublisher _mqtt; + private readonly IImageUrlFetcher _urlFetcher; - public FramesController(FramesRegistry frames, IImagePipeline pipeline, ImageStore store, MqttPublisher mqtt) + public FramesController( + FramesRegistry frames, + IImagePipeline pipeline, + ImageStore store, + MqttPublisher mqtt, + IImageUrlFetcher urlFetcher) { _frames = frames; _pipeline = pipeline; _store = store; _mqtt = mqtt; + _urlFetcher = urlFetcher; } [HttpPost("{name}/image")] @@ -44,6 +52,48 @@ public sealed class FramesController : ControllerBase pngBytes = _pipeline.Process(stream, frame); } + return await FinishUploadAsync(frame, pngBytes, cancellationToken).ConfigureAwait(false); + } + + [HttpPost("{name}/image-url")] + public async Task UploadImageUrl( + string name, + [FromBody] ImageUrlRequest? body, + CancellationToken cancellationToken) + { + if (!FrameName.TryParse(name, out var frameName) || !_frames.TryGetByName(frameName, out var frame)) + { + return NotFound(); + } + + if (body is null || string.IsNullOrWhiteSpace(body.Url) || + !Uri.TryCreate(body.Url, UriKind.Absolute, out var uri) || + (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)) + { + return BadRequest(new { error = "Missing or invalid 'url'." }); + } + + Stream source; + try + { + source = await _urlFetcher.FetchAsync(uri, cancellationToken).ConfigureAwait(false); + } + catch (ImageFetchException ex) + { + return StatusCode(StatusCodes.Status502BadGateway, new { error = ex.Message }); + } + + byte[] pngBytes; + await using (source) + { + pngBytes = _pipeline.Process(source, frame); + } + + return await FinishUploadAsync(frame, pngBytes, cancellationToken).ConfigureAwait(false); + } + + private async Task FinishUploadAsync(Frame frame, byte[] pngBytes, CancellationToken cancellationToken) + { await _store.WriteAsync(frame.Mac, pngBytes, cancellationToken).ConfigureAwait(false); var publishResult = await _mqtt.PublishAsync(frame.Mac, cancellationToken).ConfigureAwait(false); @@ -57,4 +107,6 @@ public sealed class FramesController : ControllerBase mqttPublished = publishResult == PublishResult.Success, }); } + + public sealed record ImageUrlRequest(string? Url); }