using System.Collections.Concurrent; using MijnBesluit.Application.Abstractions; namespace MijnBesluit.Server; /// /// Waar een bijlage bij hoort. Hoogstens één van de drie is gevuld; alle drie leeg mag ook, /// dan staat het bestand op zichzelf - zoals een video-opname die pas bij het opslaan aan een /// bericht wordt gehangen. /// public record Bijlagedoel(long? KluisItemId = null, long? PersoonId = null, long? BerichtId = null); /// /// Houdt bestanden bij die in stukken binnenkomen of weggaan. /// /// Waarom in stukken: één socketbericht is maximaal 16 MB, en de inhoud gaat als tekst /// (base64) mee, wat er nog een derde bij optelt. Een foto van de telefoon past daar vaak net /// in, een video nooit. Dus knippen we het op, met een volgnummer erbij zodat de ontvanger /// merkt als er een stuk mist. /// /// Onvoltooide overdrachten worden na een half uur opgeruimd. Zonder dat zou een app die /// halverwege wegvalt geheugen laten staan tot de service herstart. /// public class Bestandsoverdracht { /// Hoeveel bytes er per stuk over de lijn gaan. Base64 maakt er ruim 680 kB van. public const int StukGrootte = 512 * 1024; /// Zo lang blijft een halve overdracht bewaard voordat hij wordt weggegooid. public static readonly TimeSpan Verlooptijd = TimeSpan.FromMinutes(30); /// Zoveel overdrachten tegelijk per gebruiker; meer is geen gebruik maar misbruik. public const int MaxPerGebruiker = 3; private readonly ConcurrentDictionary _lopend = new(); private readonly IClock _clock; public Bestandsoverdracht(IClock clock) => _clock = clock; private sealed class Overdracht { public required Bijlagedoel? Doel { get; init; } public required string Id { get; init; } public required long GebruikerId { get; init; } public required string Bestandsnaam { get; init; } public required string Soort { get; init; } public required long TotaalBytes { get; init; } public required MemoryStream Inhoud { get; init; } public int VolgendStuk { get; set; } public DateTime Begonnen { get; set; } } /// Begint een nieuwe overdracht en geeft het kenmerk terug waarmee de stukken komen. public string Begin(long gebruikerId, string bestandsnaam, string soort, Bijlagedoel? doel, long totaalBytes) { RuimOp(); if (_lopend.Values.Count(o => o.GebruikerId == gebruikerId) >= MaxPerGebruiker) throw new InvalidOperationException("Er lopen al te veel uploads tegelijk. Maak er eerst een af."); var id = Guid.NewGuid().ToString("N"); _lopend[id] = new Overdracht { Id = id, GebruikerId = gebruikerId, Bestandsnaam = bestandsnaam, Soort = soort, Doel = doel, TotaalBytes = totaalBytes, Inhoud = new MemoryStream(capacity: (int)Math.Min(totaalBytes, int.MaxValue)), Begonnen = _clock.UtcNow }; return id; } /// Neemt één stuk aan. Het volgnummer moet kloppen, anders is er iets kwijtgeraakt. public void Stuk(string id, long gebruikerId, int volgnummer, byte[] inhoud) { var overdracht = Zoek(id, gebruikerId); if (volgnummer != overdracht.VolgendStuk) throw new InvalidOperationException( $"Stuk {volgnummer} kwam binnen terwijl {overdracht.VolgendStuk} aan de beurt was. Begin opnieuw."); if (overdracht.Inhoud.Length + inhoud.Length > overdracht.TotaalBytes) throw new InvalidOperationException("Er komt meer binnen dan aangekondigd."); overdracht.Inhoud.Write(inhoud, 0, inhoud.Length); overdracht.VolgendStuk++; } /// Sluit de overdracht af en geeft de volledige inhoud terug. public (string Bestandsnaam, string Soort, Bijlagedoel? Doel, byte[] Inhoud) Klaar(string id, long gebruikerId) { var overdracht = Zoek(id, gebruikerId); if (overdracht.Inhoud.Length != overdracht.TotaalBytes) throw new InvalidOperationException( $"Er kwam {overdracht.Inhoud.Length} byte binnen van de aangekondigde {overdracht.TotaalBytes}."); _lopend.TryRemove(id, out _); return (overdracht.Bestandsnaam, overdracht.Soort, overdracht.Doel, overdracht.Inhoud.ToArray()); } /// Breekt een overdracht af, bijvoorbeeld als de gebruiker annuleert. public void Afbreken(string id, long gebruikerId) { if (_lopend.TryGetValue(id, out var overdracht) && overdracht.GebruikerId == gebruikerId) { _lopend.TryRemove(id, out _); overdracht.Inhoud.Dispose(); } } /// Aantal overdrachten dat nu loopt. Alleen om te kunnen controleren. public int Lopend => _lopend.Count; private Overdracht Zoek(string id, long gebruikerId) { if (!_lopend.TryGetValue(id, out var overdracht)) throw new InvalidOperationException("Deze upload is niet (meer) bekend. Begin opnieuw."); // Van iemand anders? Dan bestaat hij voor jou niet. if (overdracht.GebruikerId != gebruikerId) throw new InvalidOperationException("Deze upload is niet (meer) bekend. Begin opnieuw."); if (_clock.UtcNow - overdracht.Begonnen > Verlooptijd) { _lopend.TryRemove(id, out _); throw new InvalidOperationException("Deze upload heeft te lang stilgelegen. Begin opnieuw."); } return overdracht; } private void RuimOp() { var nu = _clock.UtcNow; foreach (var (id, overdracht) in _lopend) { if (nu - overdracht.Begonnen <= Verlooptijd) continue; if (_lopend.TryRemove(id, out var weg)) weg.Inhoud.Dispose(); } } }