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`.
|
- `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.
|
- 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": "..." }`.
|
- Route: `POST /api/frames/{name}/image-url`, body `{ "url": "..." }`.
|
||||||
- Fetch → pipeline → store → publish. Same response shape as 5.1.
|
- Fetch → pipeline → store → publish. Same response shape as 5.1.
|
||||||
- Map `ImageFetchException` to `502 Bad Gateway`.
|
- Map `ImageFetchException` to `502 Bad Gateway`.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using FrameProcessor.Domain;
|
|||||||
using FrameProcessor.ImagePipeline;
|
using FrameProcessor.ImagePipeline;
|
||||||
using FrameProcessor.Mqtt;
|
using FrameProcessor.Mqtt;
|
||||||
using FrameProcessor.Storage;
|
using FrameProcessor.Storage;
|
||||||
|
using FrameProcessor.UrlFetch;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace FrameProcessor.Controllers;
|
namespace FrameProcessor.Controllers;
|
||||||
@@ -15,13 +16,20 @@ public sealed class FramesController : ControllerBase
|
|||||||
private readonly IImagePipeline _pipeline;
|
private readonly IImagePipeline _pipeline;
|
||||||
private readonly ImageStore _store;
|
private readonly ImageStore _store;
|
||||||
private readonly MqttPublisher _mqtt;
|
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;
|
_frames = frames;
|
||||||
_pipeline = pipeline;
|
_pipeline = pipeline;
|
||||||
_store = store;
|
_store = store;
|
||||||
_mqtt = mqtt;
|
_mqtt = mqtt;
|
||||||
|
_urlFetcher = urlFetcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{name}/image")]
|
[HttpPost("{name}/image")]
|
||||||
@@ -44,6 +52,48 @@ public sealed class FramesController : ControllerBase
|
|||||||
pngBytes = _pipeline.Process(stream, frame);
|
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);
|
await _store.WriteAsync(frame.Mac, pngBytes, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var publishResult = await _mqtt.PublishAsync(frame.Mac, 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,
|
mqttPublished = publishResult == PublishResult.Success,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed record ImageUrlRequest(string? Url);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user