146 lines
5.9 KiB
C#
146 lines
5.9 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|