mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-22 04:41:14 +00:00
chore: cycle 2 step 9 task plan artifacts + step 10 state
Carries forward new-task research + solution drafts under
_docs/02_task_plans/uav-batch-upload/ that were not included in
the Step 9 task-spec commit (42a3cc7). Also marks the autodev
state as Step 10 in_progress for cycle 2 implementation.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
# Solution Draft 01 — UAV Batch Upload (AZ-485 family)
|
||||
|
||||
**Date**: 2026-05-11
|
||||
**Mode**: Research Mode A (Initial)
|
||||
**Output Class**: Technical-component selection
|
||||
**Inputs**: `_docs/02_task_plans/uav-batch-upload/problem.md`; `_docs/02_task_plans/uav-batch-upload/00_research/00_ac_assessment.md`
|
||||
**Status**: Draft 01 (no prior drafts to assess)
|
||||
|
||||
## Core conclusion
|
||||
|
||||
The four-task family (AZ-485/486/487/488) is **build-grade-tractable on top of the existing stack — no new dependencies required for AZ-485, AZ-486, or AZ-488; AZ-487 brings in one new built-in NuGet package** (`Microsoft.AspNetCore.Authentication.JwtBearer 8.0.x`, ships with the framework). The only architectural decision worth the user's attention is the **geofence shape (Q4)** — keep the existing bbox or move to polygon/NetTopologySuite — and that decision has a real SP impact on AZ-488 (2 → 4 SP).
|
||||
|
||||
## Key findings (5 points)
|
||||
|
||||
1. **JWT/AZ-487**: `Microsoft.AspNetCore.Authentication.JwtBearer 8.0.x` (ships with the framework, version-pinned to ASP.NET Core 8) handles HS256 + the user's "validate signature + `exp` only" requirement out of the box via `TokenValidationParameters` with `ValidateIssuer = false`, `ValidateAudience = false`, `ValidateLifetime = true`, and a `SymmetricSecurityKey`. No third-party library justified.
|
||||
2. **Multipart/AZ-485**: ASP.NET Core 8 minimal API supports `IFormFileCollection` + scalar `[FromForm]` parameters natively. Per-endpoint `MaxRequestBodySize` and `MultipartBodyLengthLimit` overrides apply via endpoint metadata (`IRequestSizeLimitMetadata`, `IFormOptionsMetadata`). `app.MapPost(...).DisableAntiforgery()` is required because antiforgery is enabled by default for forms but irrelevant to a JWT-protected non-browser endpoint.
|
||||
3. **ImageSharp/AZ-486**: ImageSharp 3.1.11 already-pinned API surface is sufficient — `Image.Identify` for dimensions + format without decoding, `Image.DetectFormat` for magic-bytes check, and `Image<L8>.ProcessPixelRows` for streaming-grade luminance stddev computation. No additional NuGet packages needed. Welford's online algorithm fits the per-row callback shape perfectly.
|
||||
4. **Geofence/AZ-488**: **Recommend Option A — reuse the existing `GeofencePolygon` bbox shape** (NorthWest + SouthEast corners). Rationale: the codebase already understands bbox; the user said "geofence" without specifying polygon; ray-casting is overkill for an internal whitelist that will likely have ≤ 5 axis-aligned regions. NetTopologySuite is **rejected** for this scope (heavy, +9 MB, includes a full topology engine when we need a 4-line containment check). Option B (true polygons) is **deferred** as a future task if real-world geofences turn out to need shapes beyond rectangles.
|
||||
5. **File+DB consistency/AZ-485**: **Recommend write-row-then-write-file with idempotent overwrite**. The per-source UPSERT (already in place from AZ-484) makes the row write idempotent; the file write is `File.WriteAllBytesAsync(tempPath); File.Move(tempPath, finalPath, overwrite: true)` for atomic replace. On file write failure: log + return `rejected: storage_failure` per tile in the response; the row stays as authoritative metadata pointing at the (now-missing) file path. Pre-existing GMaps download path has the same coupling (`TileService.DownloadAndStoreTilesAsync`) — match its behavior, not invent a new one.
|
||||
|
||||
---
|
||||
|
||||
## Project Constraint Matrix
|
||||
|
||||
| Constraint Source | Constraint | Impact on Component Selection |
|
||||
|--|--|--|
|
||||
| problem.md / user Step 1 | Sync 200 with mixed `[{tileId, status, reason?}]` | Rules out async/202 patterns; rules out HTTP 207 |
|
||||
| problem.md / user Step 1 | Minimum metadata only | No new request DTO complexity; ~5 fields per tile |
|
||||
| problem.md / user Step 1 | All 6 quality checks | AZ-486 is structurally a pipeline of 6 small validators |
|
||||
| problem.md / user Step 1 | JWT HS256, claims `sub`+`exp` only | Rules out IdP/OIDC integration; symmetric key in env |
|
||||
| problem.md / user Step 1 | Same `./tiles/{z}/{x}/{y}.jpg` path as GMaps | UAV file overwrites GMaps file when uploaded for the same cell — file-level "most recent wins" matches DB-level rule |
|
||||
| existing code | `GeofencePolygon` is a bbox in this codebase | Polygon would be a new concept; bbox is consistent |
|
||||
| existing code | `TileSourceConverter.ToWireValue(TileSource.Uav)` already exists from AZ-484 | UAV row writes are 1-line conversion at the boundary |
|
||||
| existing code | Per-source UPSERT in `TileRepository.InsertAsync` (AZ-484) | UAV InsertAsync = same call site as GMaps; no new repository method |
|
||||
| existing code | `TileService.DownloadAndStoreTilesAsync` writes file then row | Establish file-write/row-write order parity |
|
||||
| existing code | No auth middleware | AZ-487 must wire `app.UseAuthentication()` + `.UseAuthorization()` from scratch; tests must construct valid tokens |
|
||||
| existing code | No rate limiter | Out of scope for this family; recorded as Risk |
|
||||
| LESSONS L-001 | Dapper TypeHandler<T> bypassed for enum reads | No new persisted enums in this family; verdict stays response-only |
|
||||
| LESSONS ring-buffer/process | NFR + runner script must land same step | Any AZ-485 perf NFR (e.g., batch throughput) must come with the runner scenario |
|
||||
| LESSONS ring-buffer/estimation | Test-site counts need grep evidence | new-task Step 4 codebase analysis must grep, not estimate |
|
||||
| .NET 8.0 LTS only | Pin all package versions to 8.0.x major | `Microsoft.AspNetCore.Authentication.JwtBearer 8.0.x`, no 9.x |
|
||||
|
||||
## Component Fit Matrix
|
||||
|
||||
### Q1 — JWT auth (AZ-487)
|
||||
|
||||
| Candidate | Fit verdict | Notes |
|
||||
|--|--|--|
|
||||
| `Microsoft.AspNetCore.Authentication.JwtBearer 8.0.x` | **Selected** | Ships with ASP.NET Core 8; supports HS256 via `SymmetricSecurityKey`; `ValidateIssuer=false`, `ValidateAudience=false`, `ValidateLifetime=true` matches user's "claims = sub + exp only"; per-endpoint `RequireAuthorization()` works on minimal API |
|
||||
| `OpenIddict 5.x` | Rejected | Full OAuth2/OIDC server framework; orders of magnitude bigger than needed for HS256-validation-only |
|
||||
| Custom token middleware | Rejected | Reinvents token parsing/validation; security-critical code that should not be hand-rolled |
|
||||
| `IdentityServer / Duende` | Rejected | Issuer/server, not validator; commercial license |
|
||||
|
||||
**Mode pinned**: `JwtBearerOptions.TokenValidationParameters` with explicit symmetric-key shape. Mandatory MVE below.
|
||||
|
||||
### Q2 — Multipart batch upload (AZ-485)
|
||||
|
||||
| Candidate | Fit verdict | Notes |
|
||||
|--|--|--|
|
||||
| Minimal API `IFormFileCollection` + `[FromForm]` scalar fields | **Selected for batches up to ~50 MB** | Native to ASP.NET Core 8; no wire shape issue; FormOptions overridable per-endpoint |
|
||||
| `MultipartReader` streaming | Deferred | Required only if batches exceed in-memory budget. Not in initial scope; revisit if production telemetry shows batches >50 MB. |
|
||||
| Separate per-tile POST (no batch) | Rejected — user explicitly chose batch | Would simplify code but contradicts requirement |
|
||||
|
||||
**Mode pinned**: `app.MapPost("/api/satellite/uav/upload", (IFormFileCollection files, [FromForm] string metadata, ...) => ...).DisableAntiforgery().RequireAuthorization()`. Per-endpoint size cap via `.WithMetadata(new RequestSizeLimitMetadata { MaxRequestBodySize = 50_000_000 })` and `.WithMetadata(new FormOptionsMetadata { MultipartBodyLengthLimit = 50_000_000 })`.
|
||||
|
||||
### Q3 — Quality gate (AZ-486)
|
||||
|
||||
| Check | API | Performance |
|
||||
|--|--|--|
|
||||
| Format = JPEG | `Image.DetectFormat(stream)` then check `format.Name == "JPEG"` | Header-only read; ~1 ms per tile |
|
||||
| Dimensions = `MapConfig.TileSizePixels` (256) | `Image.Identify(stream)` returns `ImageInfo { Width, Height }` | Same header-only path; same ~1 ms |
|
||||
| File size 4 KB ≤ size ≤ 2 MB | `IFormFile.Length` direct check | O(1), no decode |
|
||||
| `captured_at` within last 365 days | `DateTime.UtcNow - capturedAt ≤ TimeSpan.FromDays(365)` | O(1) |
|
||||
| Blank/uniform: luminance stddev < 5 | `Image.Load<L8>(stream).ProcessPixelRows(accessor => ...)` Welford online stddev | ~5–8 ms per 256×256 tile (single-channel decode + one pass) |
|
||||
| Geofence containment | `GeofenceWhitelist.Contains(lat, lon)` (AZ-488) | O(M) per tile, M ≤ 10 polygons → negligible |
|
||||
|
||||
**Mode pinned**: `Image.Identify` first (cheap, gives format + dimensions in one call); only proceed to `Image.Load<L8>` for the blank-detection check after the cheap checks pass. Total per-tile budget under 15 ms.
|
||||
|
||||
| Candidate alternatives | Fit verdict |
|
||||
|--|--|
|
||||
| Magick.NET / ImageMagick | Rejected — second image library on the same stack; ImageSharp already pinned and sufficient |
|
||||
| `System.Drawing.Common` | Rejected — Microsoft has marked `System.Drawing.Common` non-cross-platform on .NET 6+; would break Linux container |
|
||||
| Pre-trained ML "is-this-blank" model | Rejected — overkill; Welford stddev is the canonical, fast baseline |
|
||||
|
||||
### Q4 — Geofence shape (AZ-488)
|
||||
|
||||
| Candidate | Fit verdict | Sizing impact |
|
||||
|--|--|--|
|
||||
| **Reuse `GeofencePolygon` bbox** (NW + SE corners, point-in-bbox check is a 4-comparison if-statement) | **Selected** | AZ-488 = 2 SP as originally scoped |
|
||||
| True polygon (lat/lon vertex list, ray-casting from scratch) | Deferred | Would push AZ-488 to ~4 SP (new DTO + ray-cast utility + tests for edge cases like point-on-edge, antimeridian crossing) |
|
||||
| `NetTopologySuite 2.5.x` | Rejected | +9 MB binary, full topology engine; not justified for ≤ 10 axis-aligned whitelisted regions |
|
||||
|
||||
Storage: `appsettings.json` array under `UavQualityGate.GeofenceWhitelist[]`, list of `{ northWest: {lat,lon}, southEast: {lat,lon} }`. Empty array = "allow all" (sentinel). Loaded via `IOptions<UavQualityGateConfig>` like every other config in this codebase.
|
||||
|
||||
### Q5 — File+DB consistency (AZ-485)
|
||||
|
||||
| Candidate | Fit verdict |
|
||||
|--|--|
|
||||
| **Write file with `.tmp`, then `InsertAsync` (UPSERT), then `File.Move(temp → final, overwrite: true)`** | **Selected** | Two-phase: file write is recoverable (delete .tmp on failure), DB write is idempotent (UPSERT), final atomic rename. |
|
||||
| Write row first, then file | Rejected | Race window: row points at non-existent file if process crashes between |
|
||||
| Distributed transaction across FS + DB (e.g., `TransactionScope`) | Rejected | `System.IO.File` is not a transactional resource manager on Linux — would silently no-op the FS rollback |
|
||||
| Outbox pattern with retry queue | Deferred | Worth considering if production traffic warrants exactly-once-delivery guarantees; out of scope for this cycle |
|
||||
|
||||
## Tech Stack Summary
|
||||
|
||||
| Layer | Choice | Version | Justification |
|
||||
|--|--|--|--|
|
||||
| HTTP framework | ASP.NET Core minimal API | 8.0 (existing) | Match existing endpoints |
|
||||
| Auth middleware | `Microsoft.AspNetCore.Authentication.JwtBearer` | 8.0.x | Built-in; HS256 supported |
|
||||
| Image inspection | `SixLabors.ImageSharp` | 3.1.11 (existing) | Already pinned; supports `Identify`, `Load<L8>`, `ProcessPixelRows` |
|
||||
| DB access | `Dapper` + `Npgsql` | 2.1.35 / 9.0.2 (existing) | UPSERT via `TileRepository.InsertAsync` (AZ-484) |
|
||||
| Geofence | Reuse `SatelliteProvider.Common.DTO.GeofencePolygon` | existing | Bbox shape, simple containment check |
|
||||
| Config | `Microsoft.Extensions.Options` `IOptions<T>` | 8.0.x (existing) | New `UavQualityGateConfig` registered same way as `MapConfig`, etc. |
|
||||
|
||||
**No new top-level dependencies on top of the existing stack** other than the JwtBearer package.
|
||||
|
||||
## Security analysis (concise)
|
||||
|
||||
| Threat | Control | Where |
|
||||
|--|--|--|
|
||||
| Forged tokens | HMAC SHA-256 signature validation against shared secret | AZ-487 — `JwtBearerOptions.TokenValidationParameters` |
|
||||
| Token replay | `exp` claim validation | AZ-487 — `ValidateLifetime = true` |
|
||||
| Signing key leak | env var, not in source; documented in `.env.example` (matches existing GMaps API key handling) | AZ-487 + AGENTS.md update |
|
||||
| Path traversal in upload | Tile path is computed from validated lat/lon/zoom server-side, not from any user-supplied filename | AZ-485 — `tile_x/tile_y` derived via existing `GeoUtils.WorldToTilePos` |
|
||||
| Image-decode attacks (zip bomb, malformed JPEG) | `Image.Identify` uses bounded reads; `Image.Load` wrapped in try/catch around `InvalidImageContentException` and `UnknownImageFormatException`; per-file size cap before decode | AZ-486 |
|
||||
| Disk fill DoS | Per-batch size cap (`MaxRequestBodySize = 50 MB`); per-file size cap (2 MB); rate limiter recommended but tracked separately from this family | AZ-485 — endpoint metadata; rate limiter is Security Audit I3 deferred |
|
||||
| JWT logged in logs | `Serilog` request logging configured today does not log Authorization header; verify Pii filter is on | AZ-487 verification step |
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
| Risk | Severity | Mitigation |
|
||||
|--|--|--|
|
||||
| File overwrite for same cell across sources causes data loss at the file system level (DB row carries source, file does not) | Medium | Re-confirm with user in new-task Step 5 — option to use `./tiles/uav/{z}/{x}/{y}.jpg` to keep both bytes addressable. Default is "yes, same path" per Step 1 answer. |
|
||||
| Quality-gate thresholds (blank stddev, age limit, byte size bounds) need real-world tuning | Low | Make every threshold configurable via `UavQualityGateConfig`; ship defaults; tune in production telemetry |
|
||||
| JWT signing-key rotation procedure | Medium | Out of scope for AZ-487; document the procedure ("change env var + restart") in `_docs/02_document/deployment/`. Shared secret rotation requires a coordinated upload-client + server change, accepted limitation |
|
||||
| Test infrastructure for JWT (integration tests must mint tokens) | Low | Use `JsonWebTokenHandler` from `System.IdentityModel.Tokens.Jwt` in test code with the same shared secret as the test config; pattern is well-trodden |
|
||||
| Adversarial UAV uploader sends `captured_at` in the future | Low | Add a 7th implicit check: `captured_at <= DateTime.UtcNow`. Cheap, catches one obvious abuse |
|
||||
|
||||
## Minimum Viable Examples (saved verbatim per per-mode API gate)
|
||||
|
||||
### MVE-1 — JWT validation (Q1 selected mode)
|
||||
|
||||
```csharp
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
var key = builder.Configuration["UavAuth:SigningKey"]
|
||||
?? throw new InvalidOperationException("UavAuth:SigningKey is required");
|
||||
|
||||
options.RequireHttpsMetadata = false;
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)),
|
||||
ClockSkew = TimeSpan.FromSeconds(30),
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
var app = builder.Build();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapPost("/api/satellite/uav/upload", (IFormFileCollection files, [FromForm] string metadata) =>
|
||||
{
|
||||
return Results.Ok(new { received = files.Count });
|
||||
})
|
||||
.DisableAntiforgery()
|
||||
.RequireAuthorization();
|
||||
```
|
||||
|
||||
### MVE-2 — Per-endpoint size limits (Q2 selected mode)
|
||||
|
||||
```csharp
|
||||
using Microsoft.AspNetCore.Http.Metadata;
|
||||
|
||||
const long MaxBatchBytes = 50L * 1024 * 1024;
|
||||
|
||||
app.MapPost("/api/satellite/uav/upload", handler)
|
||||
.DisableAntiforgery()
|
||||
.RequireAuthorization()
|
||||
.WithMetadata(
|
||||
new RequestSizeLimitMetadata { MaxRequestBodySize = MaxBatchBytes },
|
||||
new FormOptionsMetadata
|
||||
{
|
||||
MultipartBodyLengthLimit = MaxBatchBytes,
|
||||
ValueLengthLimit = 16 * 1024,
|
||||
MultipartBoundaryLengthLimit = 256,
|
||||
});
|
||||
```
|
||||
|
||||
### MVE-3 — Quality gate (Q3 selected mode)
|
||||
|
||||
```csharp
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
public sealed record QualityResult(bool Accepted, string? Reason);
|
||||
|
||||
public sealed class TileQualityGate
|
||||
{
|
||||
private readonly UavQualityGateConfig _cfg;
|
||||
|
||||
public QualityResult Check(IFormFile file, double lat, double lon, DateTime capturedAt, GeofenceWhitelist geofence)
|
||||
{
|
||||
if (file.Length < _cfg.MinFileBytes) return new(false, "file_too_small");
|
||||
if (file.Length > _cfg.MaxFileBytes) return new(false, "file_too_large");
|
||||
|
||||
var capturedAge = DateTime.UtcNow - capturedAt;
|
||||
if (capturedAge > TimeSpan.FromDays(_cfg.MaxAgeDays)) return new(false, "captured_at_too_old");
|
||||
if (capturedAge < TimeSpan.Zero) return new(false, "captured_at_in_future");
|
||||
|
||||
if (!geofence.Contains(lat, lon)) return new(false, "outside_geofence");
|
||||
|
||||
using var stream = file.OpenReadStream();
|
||||
|
||||
IImageFormat? detectedFormat = Image.DetectFormat(stream);
|
||||
if (detectedFormat is null || detectedFormat.Name != "JPEG") return new(false, "format_not_jpeg");
|
||||
|
||||
stream.Position = 0;
|
||||
ImageInfo info = Image.Identify(stream);
|
||||
if (info.Width != _cfg.ExpectedTilePixels || info.Height != _cfg.ExpectedTilePixels)
|
||||
return new(false, "wrong_dimensions");
|
||||
|
||||
stream.Position = 0;
|
||||
try
|
||||
{
|
||||
using var img = Image.Load<L8>(stream);
|
||||
double mean = 0, m2 = 0; long n = 0;
|
||||
img.ProcessPixelRows(accessor =>
|
||||
{
|
||||
for (int y = 0; y < accessor.Height; y++)
|
||||
{
|
||||
var row = accessor.GetRowSpan(y);
|
||||
for (int x = 0; x < row.Length; x++)
|
||||
{
|
||||
double v = row[x].PackedValue;
|
||||
n++;
|
||||
double delta = v - mean;
|
||||
mean += delta / n;
|
||||
m2 += delta * (v - mean);
|
||||
}
|
||||
}
|
||||
});
|
||||
double stddev = n > 1 ? Math.Sqrt(m2 / (n - 1)) : 0;
|
||||
if (stddev < _cfg.MinLuminanceStdDev) return new(false, "image_blank_or_uniform");
|
||||
}
|
||||
catch (InvalidImageContentException) { return new(false, "image_corrupt"); }
|
||||
catch (UnknownImageFormatException) { return new(false, "format_unknown"); }
|
||||
|
||||
return new(true, null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MVE-4 — Geofence whitelist (Q4 selected mode)
|
||||
|
||||
```csharp
|
||||
public sealed class GeofenceWhitelist
|
||||
{
|
||||
private readonly IReadOnlyList<GeofencePolygon> _polygons;
|
||||
|
||||
public GeofenceWhitelist(IReadOnlyList<GeofencePolygon> polygons) => _polygons = polygons;
|
||||
|
||||
public bool Contains(double lat, double lon)
|
||||
{
|
||||
if (_polygons.Count == 0) return true; // sentinel: empty list = allow all
|
||||
|
||||
foreach (var p in _polygons)
|
||||
{
|
||||
if (p.NorthWest is null || p.SouthEast is null) continue;
|
||||
if (lat <= p.NorthWest.Lat && lat >= p.SouthEast.Lat
|
||||
&& lon >= p.NorthWest.Lon && lon <= p.SouthEast.Lon)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MVE-5 — File+DB consistency (Q5 selected mode)
|
||||
|
||||
```csharp
|
||||
public sealed class UavTilePersister
|
||||
{
|
||||
private readonly ITileRepository _repo;
|
||||
private readonly StorageConfig _cfg;
|
||||
private readonly ILogger<UavTilePersister> _log;
|
||||
|
||||
public async Task<Guid?> PersistAsync(IFormFile file, double lat, double lon, int zoom,
|
||||
double tileSizeMeters, DateTime capturedAt, CancellationToken ct)
|
||||
{
|
||||
var (tileX, tileY) = GeoUtils.WorldToTilePos(new GeoPoint(lat, lon), zoom);
|
||||
var finalPath = Path.Combine(_cfg.TilesRoot, zoom.ToString(), tileX.ToString(), tileY + ".jpg");
|
||||
var tempPath = finalPath + ".uav.tmp";
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(finalPath)!);
|
||||
|
||||
await using (var dst = File.Create(tempPath))
|
||||
await file.CopyToAsync(dst, ct);
|
||||
|
||||
try
|
||||
{
|
||||
var entity = new TileEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TileZoom = zoom, TileX = tileX, TileY = tileY,
|
||||
Latitude = lat, Longitude = lon,
|
||||
TileSizeMeters = tileSizeMeters,
|
||||
TileSizePixels = _cfg.ExpectedTilePixels,
|
||||
ImageType = "jpg",
|
||||
Source = TileSourceConverter.ToWireValue(TileSource.Uav),
|
||||
CapturedAt = capturedAt,
|
||||
FilePath = Path.GetRelativePath(AppContext.BaseDirectory, finalPath),
|
||||
};
|
||||
|
||||
var id = await _repo.InsertAsync(entity);
|
||||
|
||||
File.Move(tempPath, finalPath, overwrite: true);
|
||||
return id;
|
||||
}
|
||||
catch
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sources
|
||||
|
||||
| Source | Tier | Citation |
|
||||
|--|--|--|
|
||||
| ASP.NET Core 8.0.21 source — JwtBearer public API | L1 | `github.com/dotnet/aspnetcore/blob/v8.0.21/src/Security/Authentication/JwtBearer/src/PublicAPI.Shipped.txt` (via context7 `/dotnet/aspnetcore/v8.0.21`) |
|
||||
| ASP.NET Core 8.0.21 source — FormOptions, IRequestSizeLimitMetadata | L1 | `github.com/dotnet/aspnetcore/blob/v8.0.21/src/Http/Http/src/PublicAPI.Shipped.txt` |
|
||||
| ASP.NET Core docs — JWT Bearer config sample | L1 | `learn.microsoft.com/aspnet/core/security/authentication/configure-jwt-bearer-authentication` (via context7 `/dotnet/aspnetcore`) |
|
||||
| SixLabors.ImageSharp docs — Image.Identify, ProcessPixelRows | L1 | `docs.sixlabors.com/articles/imagesharp/imageinfo` (via context7 `/sixlabors/imagesharp`) |
|
||||
| Codebase grep — `GeofencePolygon` shape, `TileRepository.InsertAsync` UPSERT | L1 (project source) | `SatelliteProvider.Common/DTO/GeofencePolygon.cs`, `SatelliteProvider.DataAccess/Repositories/TileRepository.cs` |
|
||||
| AZ-484 task family closure (this repo, cycle 1) | L1 (project history) | `_docs/03_implementation/implementation_report_multi_source_tile_storage_cycle1.md`; `_docs/02_document/contracts/data-access/tile-storage.md` |
|
||||
|
||||
## Quality checklist
|
||||
|
||||
- [x] Each component selection has an exact-fit verdict against the Project Constraint Matrix
|
||||
- [x] Per-mode API capability verification done for each library (JwtBearer, ASP.NET Core multipart, ImageSharp)
|
||||
- [x] MVE saved for every selected mode (5 of them)
|
||||
- [x] "Do not use" / Rejected list present per question
|
||||
- [x] L1 sources for every key claim
|
||||
- [x] Multi-perspective considered: implementer (MVE), security architect (threats table), domain (existing code constraints), contrarian (rejected NTS / OpenIddict / Magick.NET / async-202)
|
||||
- [x] No conclusions from "gut feel" — every selection has matrix evidence
|
||||
Reference in New Issue
Block a user