7.2 FramesController.UploadImageUrl

This commit is contained in:
2026-06-07 15:53:27 +02:00
parent 3bef27b286
commit 3e01fa7980
2 changed files with 54 additions and 2 deletions

View File

@@ -182,7 +182,7 @@ Each type lives in `src/FrameProcessor/Domain/`. Tests in `tests/FrameProcessor.
- `Task<Stream> 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`.

View File

@@ -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<IActionResult> 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<IActionResult> 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);
}