Merge cms-w3-t2-upload: WAV upload flow — POST api/track/upload, POST api/cms/track, /cms/tracks/new
# Conflicts: # DeepDrftContent/CLAUDE.md
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
@page "/cms/tracks/new"
|
||||
@rendermode InteractiveServer
|
||||
@using System.Net.Http.Headers
|
||||
@using AuthBlocksWeb.HierarchicalAuthorize
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.Extensions.Logging
|
||||
@attribute [HierarchicalRoleAuthorize("Admin")]
|
||||
|
||||
@inject IHttpClientFactory HttpClientFactory
|
||||
@inject NavigationManager Navigation
|
||||
@inject ISnackbar Snackbar
|
||||
@inject ILogger<TrackNew> Logger
|
||||
|
||||
<PageTitle>Add Track — DeepDrft CMS</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Medium" Class="mt-8">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">Add Track</MudText>
|
||||
|
||||
<MudPaper Class="pa-6" Elevation="2">
|
||||
<MudStack Spacing="4">
|
||||
<MudText Typo="Typo.subtitle1">WAV file</MudText>
|
||||
<InputFile OnChange="OnFileSelected" accept=".wav,audio/wav,audio/x-wav" />
|
||||
@if (_selectedFile is not null)
|
||||
{
|
||||
<MudText Typo="Typo.body2">
|
||||
Selected: @_selectedFile.Name (@FormatBytes(_selectedFile.Size))
|
||||
</MudText>
|
||||
}
|
||||
|
||||
<MudTextField @bind-Value="_trackName" Label="Track Name" Required="true" RequiredError="Track Name is required" Variant="Variant.Outlined" />
|
||||
<MudTextField @bind-Value="_artist" Label="Artist" Required="true" RequiredError="Artist is required" Variant="Variant.Outlined" />
|
||||
<MudTextField @bind-Value="_album" Label="Album" Variant="Variant.Outlined" />
|
||||
<MudTextField @bind-Value="_genre" Label="Genre" Variant="Variant.Outlined" />
|
||||
<MudTextField @bind-Value="_releaseDate" Label="Release Date (YYYY-MM-DD)" Placeholder="2024-01-15" Variant="Variant.Outlined" />
|
||||
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error">@_errorMessage</MudAlert>
|
||||
}
|
||||
|
||||
<MudStack Row="true" Spacing="2" Justify="Justify.FlexEnd">
|
||||
<MudButton Variant="Variant.Text"
|
||||
OnClick="Cancel"
|
||||
Disabled="_isUploading">
|
||||
Cancel
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="SubmitAsync"
|
||||
Disabled="_isUploading">
|
||||
@if (_isUploading)
|
||||
{
|
||||
<MudProgressCircular Indeterminate="true" Size="Size.Small" Class="mr-2" />
|
||||
<text>Uploading…</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>Upload</text>
|
||||
}
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
// 1 GB ceiling matches the proxy controller's RequestSizeLimit; the actual streaming
|
||||
// path means the limit caps the request, not in-memory buffering.
|
||||
private const long MaxUploadBytes = 1_073_741_824L;
|
||||
|
||||
private IBrowserFile? _selectedFile;
|
||||
private string _trackName = string.Empty;
|
||||
private string _artist = string.Empty;
|
||||
private string _album = string.Empty;
|
||||
private string _genre = string.Empty;
|
||||
private string _releaseDate = string.Empty;
|
||||
private string? _errorMessage;
|
||||
private bool _isUploading;
|
||||
|
||||
private void OnFileSelected(InputFileChangeEventArgs e)
|
||||
{
|
||||
_selectedFile = e.File;
|
||||
_errorMessage = null;
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
_errorMessage = null;
|
||||
|
||||
if (_selectedFile is null)
|
||||
{
|
||||
_errorMessage = "Please select a WAV file.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_selectedFile.Name.EndsWith(".wav", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_errorMessage = "Selected file must be a .wav file.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_trackName))
|
||||
{
|
||||
_errorMessage = "Track Name is required.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_artist))
|
||||
{
|
||||
_errorMessage = "Artist is required.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_releaseDate)
|
||||
&& !DateOnly.TryParseExact(_releaseDate, "yyyy-MM-dd", out _))
|
||||
{
|
||||
_errorMessage = "Release Date must be in YYYY-MM-DD format.";
|
||||
return;
|
||||
}
|
||||
|
||||
_isUploading = true;
|
||||
try
|
||||
{
|
||||
using var multipart = new MultipartFormDataContent();
|
||||
|
||||
// OpenReadStream streams chunks from the browser via the SignalR circuit;
|
||||
// wrapping in StreamContent avoids materialising the whole file in memory
|
||||
// before the proxy controller receives it.
|
||||
await using var fileStream = _selectedFile.OpenReadStream(MaxUploadBytes);
|
||||
var fileContent = new StreamContent(fileStream);
|
||||
fileContent.Headers.ContentType = new MediaTypeHeaderValue(
|
||||
string.IsNullOrWhiteSpace(_selectedFile.ContentType) ? "audio/wav" : _selectedFile.ContentType);
|
||||
multipart.Add(fileContent, "wav", _selectedFile.Name);
|
||||
multipart.Add(new StringContent(_trackName), "trackName");
|
||||
multipart.Add(new StringContent(_artist), "artist");
|
||||
if (!string.IsNullOrWhiteSpace(_album)) multipart.Add(new StringContent(_album), "album");
|
||||
if (!string.IsNullOrWhiteSpace(_genre)) multipart.Add(new StringContent(_genre), "genre");
|
||||
if (!string.IsNullOrWhiteSpace(_releaseDate)) multipart.Add(new StringContent(_releaseDate), "releaseDate");
|
||||
|
||||
var client = HttpClientFactory.CreateClient("DeepDrft.API");
|
||||
using var response = await client.PostAsync("api/cms/track", multipart);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
Snackbar.Add($"Uploaded '{_trackName}'.", Severity.Success);
|
||||
Navigation.NavigateTo("/cms/tracks");
|
||||
return;
|
||||
}
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
_errorMessage = string.IsNullOrWhiteSpace(body)
|
||||
? $"Upload failed ({(int)response.StatusCode})."
|
||||
: $"Upload failed ({(int)response.StatusCode}): {body}";
|
||||
Logger.LogWarning("CMS upload rejected: {Status} {Body}", (int)response.StatusCode, body);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Upload failed in TrackNew");
|
||||
_errorMessage = "Upload failed. Please try again.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
Navigation.NavigateTo("/cms/tracks");
|
||||
}
|
||||
|
||||
private static string FormatBytes(long bytes)
|
||||
{
|
||||
const long KB = 1024;
|
||||
const long MB = KB * 1024;
|
||||
const long GB = MB * 1024;
|
||||
if (bytes >= GB) return $"{bytes / (double)GB:F2} GB";
|
||||
if (bytes >= MB) return $"{bytes / (double)MB:F2} MB";
|
||||
if (bytes >= KB) return $"{bytes / (double)KB:F2} KB";
|
||||
return $"{bytes} bytes";
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ The binary content API host. ApiKey middleware, CORS, forwarded headers. Returns
|
||||
## What lives here now (only)
|
||||
|
||||
- `Program.cs`, `Startup.cs`: HTTP host config, DI wiring, middleware setup, port binding.
|
||||
- `Controllers/TrackController.cs`: Three endpoints (see below).
|
||||
- `Controllers/TrackController.cs`: Four endpoints (see below).
|
||||
- `Middleware/ApiKeyAuthenticationMiddleware.cs`, `Middleware/ApiKeyAuthorizeAttribute.cs`: ApiKey validation logic.
|
||||
- `Models/`: Settings POCOs only (`ApiKeySettings`, `CorsSettings`, `FileDatabaseSettings`). No domain code.
|
||||
- `environment/filedatabase.json`: FileDatabase vault path config (required).
|
||||
@@ -24,7 +24,7 @@ The binary content API host. ApiKey middleware, CORS, forwarded headers. Returns
|
||||
- `WavOffsetService` — in `DeepDrftContent.Services`.
|
||||
- Don't add new domain code to this project.
|
||||
|
||||
## The endpoint surface (exactly two endpoints)
|
||||
## The endpoint surface (four endpoints)
|
||||
|
||||
### GET api/track/{trackId}?offset=0 (unauthenticated)
|
||||
|
||||
@@ -57,7 +57,24 @@ Returns the WAV bytes from the `tracks` vault.
|
||||
- `true` → entry removed → 200.
|
||||
- Added in CMS Wave 3 (W1.5) so the CMS delete endpoint on `DeepDrftWeb` (`DELETE api/cms/track/{id}`) can clean up the vault after the SQL row is gone. Wave 3 product approval covers this — the "do not add a third endpoint without product approval" rule from prior waves is satisfied.
|
||||
|
||||
The endpoint surface is now intentionally **three** endpoints. Do not add a fourth without product approval.
|
||||
### POST api/track/upload ([ApiKeyAuthorize])
|
||||
|
||||
**Authenticated endpoint.** Accepts a raw WAV upload + metadata as `multipart/form-data`, processes the WAV via `DeepDrftContent.Services.TrackService.AddTrackFromWavAsync`, and returns an unpersisted `TrackEntity` (no `Id` assigned). The caller (the CMS controller on `DeepDrftWeb`) is responsible for then saving that entity to SQL.
|
||||
|
||||
- **Header `ApiKey`**: required. Validated by `ApiKeyAuthenticationMiddleware`.
|
||||
- **Form fields**:
|
||||
- `wav` (`IFormFile`, required): the WAV bytes. File name must end in `.wav`.
|
||||
- `trackName` (string, required)
|
||||
- `artist` (string, required)
|
||||
- `album` (string, optional)
|
||||
- `genre` (string, optional)
|
||||
- `releaseDate` (string, optional, format `YYYY-MM-DD`)
|
||||
- The upload stream is copied to a `.wav`-suffixed temp file under `Path.GetTempPath()` (the audio processor requires that extension and reads from disk). The temp file is always deleted in a `finally` block — success or failure.
|
||||
- `[RequestSizeLimit(1 GB)]` + `[RequestFormLimits(MultipartBodyLengthLimit = 1 GB)]` lift the per-request ceiling above the framework default (~28 MB) so production-sized WAVs are accepted. The body is streamed to the temp file, not buffered in memory.
|
||||
- Returns 200 with the unpersisted `TrackEntity` JSON on success. Returns 400 for missing/invalid form fields (no WAV, missing required strings, wrong extension, bad date). Returns 500 if `AddTrackFromWavAsync` returns null or throws.
|
||||
- **Wave 3 product approval covers this addition** (CMS-PLAN §5, Option B): `DeepDrftWeb` proxies CMS uploads here so it never touches the vault disk path. Do not extend the endpoint surface further without a fresh approval.
|
||||
|
||||
The endpoint surface is now intentionally **four** endpoints. Do not add a fifth without product approval.
|
||||
|
||||
## ApiKey middleware behaviour
|
||||
|
||||
|
||||
@@ -120,6 +120,112 @@ public class TrackController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
// POST api/track/upload: raw WAV in (multipart/form-data) + metadata → unpersisted TrackEntity out.
|
||||
// Used by the CMS upload flow on DeepDrftWeb; that host proxies the upload here so it never
|
||||
// touches the vault disk path directly (Option B in CMS-PLAN §5).
|
||||
//
|
||||
// RequestSizeLimit/MultipartBodyLengthLimit set to 1 GB: WAV uploads can be tens to hundreds
|
||||
// of MB and the framework defaults (~28 MB) reject them outright. The IFormFile path streams
|
||||
// the body to a temp file once Kestrel surfaces it, so the limit is the per-request ceiling,
|
||||
// not a buffered allocation.
|
||||
[ApiKeyAuthorize]
|
||||
[HttpPost("upload")]
|
||||
[RequestSizeLimit(1_073_741_824)]
|
||||
[RequestFormLimits(MultipartBodyLengthLimit = 1_073_741_824)]
|
||||
public async Task<ActionResult<DeepDrftModels.Entities.TrackEntity>> UploadTrack(
|
||||
[FromForm] IFormFile? wav,
|
||||
[FromForm] string? trackName,
|
||||
[FromForm] string? artist,
|
||||
[FromForm] string? album,
|
||||
[FromForm] string? genre,
|
||||
[FromForm] string? releaseDate,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("UploadTrack called: trackName={TrackName}, artist={Artist}, size={Size}",
|
||||
trackName, artist, wav?.Length);
|
||||
|
||||
if (wav is null || wav.Length == 0)
|
||||
{
|
||||
return BadRequest("WAV file is required");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(trackName))
|
||||
{
|
||||
return BadRequest("trackName is required");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(artist))
|
||||
{
|
||||
return BadRequest("artist is required");
|
||||
}
|
||||
|
||||
if (!string.Equals(Path.GetExtension(wav.FileName), ".wav", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return BadRequest("Uploaded file must have a .wav extension");
|
||||
}
|
||||
|
||||
DateOnly? parsedReleaseDate = null;
|
||||
if (!string.IsNullOrWhiteSpace(releaseDate))
|
||||
{
|
||||
if (!DateOnly.TryParseExact(releaseDate, "yyyy-MM-dd", out var parsed))
|
||||
{
|
||||
return BadRequest("releaseDate must be in YYYY-MM-DD format");
|
||||
}
|
||||
parsedReleaseDate = parsed;
|
||||
}
|
||||
|
||||
// AudioProcessor.ProcessWavFileAsync requires a path ending in .wav and reads from disk.
|
||||
// Path.GetTempFileName() yields .tmp, which fails that check — generate our own .wav path.
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".wav");
|
||||
|
||||
try
|
||||
{
|
||||
await using (var tempStream = new FileStream(
|
||||
tempPath, FileMode.CreateNew, FileAccess.Write, FileShare.None,
|
||||
bufferSize: 81920, useAsync: true))
|
||||
await using (var uploadStream = wav.OpenReadStream())
|
||||
{
|
||||
await uploadStream.CopyToAsync(tempStream, cancellationToken);
|
||||
}
|
||||
|
||||
var entity = await _trackService.AddTrackFromWavAsync(
|
||||
tempPath,
|
||||
trackName,
|
||||
artist,
|
||||
string.IsNullOrWhiteSpace(album) ? null : album,
|
||||
string.IsNullOrWhiteSpace(genre) ? null : genre,
|
||||
parsedReleaseDate);
|
||||
|
||||
if (entity is null)
|
||||
{
|
||||
_logger.LogWarning("UploadTrack: TrackService returned null for {TrackName}", trackName);
|
||||
return StatusCode(500, "Failed to process and store WAV");
|
||||
}
|
||||
|
||||
_logger.LogInformation("UploadTrack succeeded: entryKey={EntryKey}", entity.EntryKey);
|
||||
return Ok(entity);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "UploadTrack failed for {TrackName}", trackName);
|
||||
return StatusCode(500, "Internal server error");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (System.IO.File.Exists(tempPath))
|
||||
{
|
||||
System.IO.File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "UploadTrack: failed to delete temp file {TempPath}", tempPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ApiKeyAuthorize]
|
||||
[HttpPut("{trackId}")]
|
||||
public async Task<ActionResult> PutTrack(string trackId, [FromBody] AudioBinaryDto track)
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Claims;
|
||||
using DeepDrftModels.Entities;
|
||||
using DeepDrftWeb.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DeepDrftWeb.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// CMS upload surface. Proxies a WAV + metadata multipart form to DeepDrftContent's
|
||||
/// POST api/track/upload, then persists the returned unpersisted TrackEntity to SQL via
|
||||
/// ITrackService.Create. DeepDrftWeb intentionally does not reference DeepDrftContent.Services
|
||||
/// (CMS-PLAN §5, Option B) — all vault access is over HTTP.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize(Roles = "Admin")]
|
||||
[Route("api/cms")]
|
||||
public class CmsUploadController : ControllerBase
|
||||
{
|
||||
private const string ContentClientName = "DeepDrft.Content";
|
||||
private const string UploadPath = "api/track/upload";
|
||||
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ITrackService _trackService;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<CmsUploadController> _logger;
|
||||
|
||||
public CmsUploadController(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ITrackService trackService,
|
||||
IConfiguration configuration,
|
||||
ILogger<CmsUploadController> logger)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_trackService = trackService;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// Match DeepDrftContent's per-request ceiling so the proxy itself does not reject
|
||||
// a payload the downstream endpoint would accept.
|
||||
[HttpPost("track")]
|
||||
[RequestSizeLimit(1_073_741_824)]
|
||||
[RequestFormLimits(MultipartBodyLengthLimit = 1_073_741_824)]
|
||||
public async Task<ActionResult<TrackEntity>> UploadTrack(
|
||||
[FromForm] IFormFile? wav,
|
||||
[FromForm] string? trackName,
|
||||
[FromForm] string? artist,
|
||||
[FromForm] string? album,
|
||||
[FromForm] string? genre,
|
||||
[FromForm] string? releaseDate,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (wav is null || wav.Length == 0)
|
||||
{
|
||||
return BadRequest("WAV file is required");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(trackName))
|
||||
{
|
||||
return BadRequest("trackName is required");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(artist))
|
||||
{
|
||||
return BadRequest("artist is required");
|
||||
}
|
||||
|
||||
var apiKey = _configuration["DeepDrftContent:ApiKey"];
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
_logger.LogError("DeepDrftContent:ApiKey is not configured");
|
||||
return StatusCode(500, "Content API key is not configured");
|
||||
}
|
||||
|
||||
var userIdValue = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (!long.TryParse(userIdValue, out var userId))
|
||||
{
|
||||
// [Authorize(Roles = "Admin")] gates upstream, so a missing/unparseable
|
||||
// user id here is a configuration bug, not a normal client state.
|
||||
_logger.LogError("Authenticated user has no parseable NameIdentifier claim: {Value}", userIdValue);
|
||||
return StatusCode(500, "Authenticated user is missing a valid identifier");
|
||||
}
|
||||
|
||||
// Forward the upload to DeepDrftContent. We rebuild the multipart container rather
|
||||
// than relaying Request.Body so the boundary is owned by HttpClient and IFormFile's
|
||||
// already-buffered stream (memory + temp-file backed by Kestrel) is the source.
|
||||
using var multipart = new MultipartFormDataContent();
|
||||
await using var wavStream = wav.OpenReadStream();
|
||||
var wavContent = new StreamContent(wavStream);
|
||||
wavContent.Headers.ContentType = new MediaTypeHeaderValue(
|
||||
string.IsNullOrWhiteSpace(wav.ContentType) ? "audio/wav" : wav.ContentType);
|
||||
multipart.Add(wavContent, "wav", wav.FileName);
|
||||
multipart.Add(new StringContent(trackName), "trackName");
|
||||
multipart.Add(new StringContent(artist), "artist");
|
||||
if (!string.IsNullOrWhiteSpace(album)) multipart.Add(new StringContent(album), "album");
|
||||
if (!string.IsNullOrWhiteSpace(genre)) multipart.Add(new StringContent(genre), "genre");
|
||||
if (!string.IsNullOrWhiteSpace(releaseDate)) multipart.Add(new StringContent(releaseDate), "releaseDate");
|
||||
|
||||
var client = _httpClientFactory.CreateClient(ContentClientName);
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, UploadPath) { Content = multipart };
|
||||
request.Headers.Add("ApiKey", apiKey);
|
||||
|
||||
HttpResponseMessage response;
|
||||
try
|
||||
{
|
||||
response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Content API call failed for upload of {TrackName}", trackName);
|
||||
return StatusCode(502, "Content API is unreachable");
|
||||
}
|
||||
|
||||
using (response)
|
||||
{
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
var statusCode = (int)response.StatusCode;
|
||||
if (statusCode >= 500)
|
||||
{
|
||||
_logger.LogError("Content API returned {Status} for upload of {TrackName}: {Body}", statusCode, trackName, body);
|
||||
return StatusCode(statusCode, "Upload failed on the content server. Please try again.");
|
||||
}
|
||||
|
||||
// 4xx: body is user-friendly validation text from DeepDrftContent — relay as-is.
|
||||
_logger.LogWarning("Content API rejected upload: {Status} {Body}", statusCode, body);
|
||||
return StatusCode(statusCode, body);
|
||||
}
|
||||
|
||||
TrackEntity? unpersisted;
|
||||
try
|
||||
{
|
||||
unpersisted = await response.Content.ReadFromJsonAsync<TrackEntity>(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to deserialize TrackEntity from Content API response");
|
||||
return StatusCode(502, "Content API returned an unexpected response");
|
||||
}
|
||||
|
||||
if (unpersisted is null)
|
||||
{
|
||||
_logger.LogError("Content API returned a null TrackEntity");
|
||||
return StatusCode(502, "Content API returned an empty response");
|
||||
}
|
||||
|
||||
unpersisted.CreatedByUserId = userId;
|
||||
|
||||
var saveResult = await _trackService.Create(unpersisted);
|
||||
if (!saveResult.Success || saveResult.Value is null)
|
||||
{
|
||||
// The vault write succeeded but the SQL persist failed — audio is now orphaned
|
||||
// in the tracks vault under EntryKey. CMS-PLAN W2.4 covers the dead-letter
|
||||
// mechanism; until then we log loudly so the orphan is recoverable manually.
|
||||
var error = saveResult.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
||||
_logger.LogError(
|
||||
"Track persisted to vault but SQL save failed. Orphaned entry: {EntryKey}. Error: {Error}",
|
||||
unpersisted.EntryKey, error);
|
||||
return StatusCode(500, $"Track was uploaded but could not be saved: {error}");
|
||||
}
|
||||
|
||||
return Ok(saveResult.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,9 @@
|
||||
"ApiUrls": {
|
||||
"ContentApi": "http://localhost:12777/"
|
||||
},
|
||||
"DeepDrftContent": {
|
||||
"ApiKey": "REPLACE_IN_ENV"
|
||||
},
|
||||
"ForwardedHeaders": {
|
||||
"DisableHttpsRedirection": "true"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user