fix(api): stream audio store path to eliminate whole-file buffering (OOM)

Processors now emit a ProcessedAudio plan with a streamed writer instead of a whole-file AudioBinary; vault writes stream via RegisterResourceStreamingAsync. Header parsing is bounded. Wave 2 (waveform/Opus) still re-reads the full file by design.
This commit is contained in:
daniel-c-harvey
2026-06-25 15:27:28 -04:00
parent 1e063d95f4
commit 79bbbd4956
13 changed files with 920 additions and 168 deletions
@@ -56,6 +56,37 @@ public abstract class MediaVault : VaultIndexDirectory
await FileUtils.PutFileAsync(mediaPath, buffer);
}
/// <summary>
/// Streams an entry's bytes into the vault without ever materializing the whole file in memory:
/// records the supplied <paramref name="metaData"/> in the index, then invokes
/// <paramref name="writeContent"/> to emit bytes directly to the backing <see cref="FileStream"/>.
/// The metadata is supplied by the caller (there is no in-memory <see cref="FileBinary"/> to infer
/// it from) — the store path (upload / replace-audio) sources its bytes from a staging file, not a
/// buffer. Returns the number of bytes written, for the caller to log.
///
/// Index-then-file ordering matches <see cref="AddEntryAsync"/>; a mid-write failure therefore
/// leaves an index entry over a partial/missing file, the same exposure the buffered path has on
/// an I/O fault. The caller treats a thrown exception as a failed register.
/// </summary>
public async Task<long> AddEntryStreamingAsync(
string entryId,
MetaData metaData,
Func<Stream, CancellationToken, Task> writeContent,
CancellationToken cancellationToken = default)
{
var mediaPath = GetMediaPathFromEntryKey(entryId, metaData.Extension);
await AddToIndexAsync(entryId, metaData);
await using var fileStream = new FileStream(
mediaPath, FileMode.Create, FileAccess.Write, FileShare.None,
bufferSize: 81920, useAsync: true);
await writeContent(fileStream, cancellationToken);
await fileStream.FlushAsync(cancellationToken);
return fileStream.Length;
}
/// <summary>
/// Retrieves an entry from the vault (MediaVaultType inferred from T)
/// </summary>