Eerste commit
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user