Eerste commit

This commit is contained in:
jf
2026-09-27 14:21:31 +00:00
commit de4ff128cb
295 changed files with 47374 additions and 0 deletions
@@ -0,0 +1,145 @@
using System.Collections.Concurrent;
using MijnBesluit.Application.Abstractions;
namespace MijnBesluit.Server;
/// <summary>
/// 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.
/// </summary>
public record Bijlagedoel(long? KluisItemId = null, long? PersoonId = null, long? BerichtId = null);
/// <summary>
/// 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.
/// </summary>
public class Bestandsoverdracht
{
/// <summary>Hoeveel bytes er per stuk over de lijn gaan. Base64 maakt er ruim 680 kB van.</summary>
public const int StukGrootte = 512 * 1024;
/// <summary>Zo lang blijft een halve overdracht bewaard voordat hij wordt weggegooid.</summary>
public static readonly TimeSpan Verlooptijd = TimeSpan.FromMinutes(30);
/// <summary>Zoveel overdrachten tegelijk per gebruiker; meer is geen gebruik maar misbruik.</summary>
public const int MaxPerGebruiker = 3;
private readonly ConcurrentDictionary<string, Overdracht> _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; }
}
/// <summary>Begint een nieuwe overdracht en geeft het kenmerk terug waarmee de stukken komen.</summary>
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;
}
/// <summary>Neemt één stuk aan. Het volgnummer moet kloppen, anders is er iets kwijtgeraakt.</summary>
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++;
}
/// <summary>Sluit de overdracht af en geeft de volledige inhoud terug.</summary>
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());
}
/// <summary>Breekt een overdracht af, bijvoorbeeld als de gebruiker annuleert.</summary>
public void Afbreken(string id, long gebruikerId)
{
if (_lopend.TryGetValue(id, out var overdracht) && overdracht.GebruikerId == gebruikerId)
{
_lopend.TryRemove(id, out _);
overdracht.Inhoud.Dispose();
}
}
/// <summary>Aantal overdrachten dat nu loopt. Alleen om te kunnen controleren.</summary>
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();
}
}
}