7.2 FramesController.UploadImageUrl
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user