diff --git a/DeepDrftAPI/Controllers/TrackController.cs b/DeepDrftAPI/Controllers/TrackController.cs index 9b4a372..eade8f9 100644 --- a/DeepDrftAPI/Controllers/TrackController.cs +++ b/DeepDrftAPI/Controllers/TrackController.cs @@ -198,14 +198,15 @@ public class TrackController : ControllerBase // proxies the upload here so it never touches the vault disk path or SQL directly. // UnifiedTrackService owns the two-database write. // - // RequestSizeLimit/MultipartBodyLengthLimit set to 1 GB: audio 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. + // RequestSizeLimit/MultipartBodyLengthLimit set to ~1.86 GB: audio uploads can be tens to + // hundreds of MB (or over a GB for high-res WAVs); 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. 2_000_000_000 stays below + // int.MaxValue (2,147,483,647) so it is safe where limits are int-typed. [ApiKeyAuthorize] [HttpPost("upload")] - [RequestSizeLimit(1_073_741_824)] - [RequestFormLimits(MultipartBodyLengthLimit = 1_073_741_824)] + [RequestSizeLimit(2_000_000_000)] + [RequestFormLimits(MultipartBodyLengthLimit = 2_000_000_000)] public async Task> UploadTrack( [FromForm] IFormFile? audioFile, [FromForm] string? trackName, @@ -503,13 +504,13 @@ public class TrackController : ControllerBase // Swap an existing track's audio bytes from a raw upload, preserving the track's id, EntryKey, // release membership, position, and metadata. UnifiedTrackService.ReplaceAudioAsync owns the // vault swap + waveform regen; nothing in SQL is written. Mirrors the upload endpoint's temp-file - // streaming and 1 GB ceiling (a WAV replace is a large-body upload like the original). The + // streaming and ~1.86 GB ceiling (a WAV replace is a large-body upload like the original). The // literal "{id:long}/replace-audio" segment is declared in the literal-route block so it never // resolves to the parameterized "{trackId}" GET. [ApiKeyAuthorize] [HttpPost("{id:long}/replace-audio")] - [RequestSizeLimit(1_073_741_824)] - [RequestFormLimits(MultipartBodyLengthLimit = 1_073_741_824)] + [RequestSizeLimit(2_000_000_000)] + [RequestFormLimits(MultipartBodyLengthLimit = 2_000_000_000)] public async Task ReplaceAudio( long id, [FromForm] IFormFile? audioFile, diff --git a/DeepDrftManager/Components/Pages/Tracks/BatchEdit.razor b/DeepDrftManager/Components/Pages/Tracks/BatchEdit.razor index f6d4442..fc4b7a8 100644 --- a/DeepDrftManager/Components/Pages/Tracks/BatchEdit.razor +++ b/DeepDrftManager/Components/Pages/Tracks/BatchEdit.razor @@ -114,8 +114,8 @@ @code { - // 1 GB ceiling matches DeepDrftAPI's per-request limit on api/track/upload. - private const long MaxUploadBytes = 1_073_741_824L; + // ~1.86 GB ceiling matches DeepDrftAPI's per-request limit on api/track/upload. + private const long MaxUploadBytes = 2_000_000_000L; // Release-title addressing (Album-mode batch Edit): loads the whole release by title. [Parameter] public string AlbumName { get; set; } = string.Empty; diff --git a/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor b/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor index a82085e..b3d9092 100644 --- a/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor +++ b/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor @@ -108,9 +108,9 @@ @code { - // 1 GB ceiling matches DeepDrftAPI's per-request limit on api/track/upload; the + // ~1.86 GB ceiling matches DeepDrftAPI's per-request limit on api/track/upload; the // streaming path means the limit caps the request, not in-memory buffering. - private const long MaxUploadBytes = 1_073_741_824L; + private const long MaxUploadBytes = 2_000_000_000L; private List _tracks = new(); private int _selectedIndex = -1; diff --git a/DeepDrftManager/Services/CmsTrackService.cs b/DeepDrftManager/Services/CmsTrackService.cs index c404832..80f54c3 100644 --- a/DeepDrftManager/Services/CmsTrackService.cs +++ b/DeepDrftManager/Services/CmsTrackService.cs @@ -28,11 +28,11 @@ public class CmsTrackService : ICmsTrackService private const int DefaultIdleTimeoutSeconds = 90; // Response-wait budget: once the request body is fully on the wire the server runs AudioProcessor - // decode → vault write → SQL persist. For a several-hundred-MB WAV this can take many minutes. + // decode → vault write → SQL persist. For a multi-GB WAV this can exceed 10 minutes. // The idle heartbeat goes silent after the last byte, so a separate, larger deadline governs the // response-wait phase so a fully-uploaded file is never killed mid-persist. // Operator-tunable via Upload:ResponseTimeoutSeconds. - private const int DefaultResponseTimeoutSeconds = 600; // 10 minutes + private const int DefaultResponseTimeoutSeconds = 1200; // 20 minutes private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; diff --git a/DeepDrftManager/appsettings.json b/DeepDrftManager/appsettings.json index e27d077..f1dc861 100644 --- a/DeepDrftManager/appsettings.json +++ b/DeepDrftManager/appsettings.json @@ -8,5 +8,9 @@ "AllowedHosts": "*", "ForwardedHeaders": { "DisableHttpsRedirection": false + }, + "Upload": { + "IdleTimeoutSeconds": 90, + "ResponseTimeoutSeconds": 1200 } } diff --git a/deploy/nginx/deepdrft-manager.conf b/deploy/nginx/deepdrft-manager.conf index 02e7bfe..ff20c2b 100644 --- a/deploy/nginx/deepdrft-manager.conf +++ b/deploy/nginx/deepdrft-manager.conf @@ -3,6 +3,10 @@ server { listen [::]:80; server_name __DOMAIN_APP__; + # Allow audio file uploads up to ~1.86 GB (matches the per-request ceiling on api/track/upload + # and api/track/{id}/replace-audio). nginx default is 1 MB, which would 413 any large upload. + client_max_body_size 2000m; + location / { proxy_pass http://localhost:__PORT_MANAGER__; proxy_http_version 1.1; @@ -15,5 +19,12 @@ server { # WebSocket support (Blazor InteractiveServer SignalR circuits) proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $http_connection; + + # Large audio uploads stream over the SignalR WebSocket circuit for several minutes. + # nginx's 60 s default would drop the connection mid-transfer and silently kill the + # Blazor circuit — the app never sees an error, so nothing is logged. 1200 s matches + # the Upload:ResponseTimeoutSeconds budget already configured in the application. + proxy_read_timeout 1200s; + proxy_send_timeout 1200s; } } diff --git a/deploy/nginx/deepdrft-public.conf b/deploy/nginx/deepdrft-public.conf index a0fdaaa..905f4de 100644 --- a/deploy/nginx/deepdrft-public.conf +++ b/deploy/nginx/deepdrft-public.conf @@ -15,5 +15,11 @@ server { # WebSocket support (Blazor Server SignalR circuits + WASM interop) proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $http_connection; + + # Blazor Server SignalR circuits can be long-lived. Raise timeouts above the 60 s + # nginx default so idle-but-active circuits (e.g. during audio streaming) are not + # silently dropped. + proxy_read_timeout 1200s; + proxy_send_timeout 1200s; } }