145 lines
4.9 KiB
C#
145 lines
4.9 KiB
C#
using LiteDB;
|
||
using LehrerApp.Sync.Models;
|
||
|
||
namespace LehrerApp.Api;
|
||
|
||
/// <summary>
|
||
/// Server-seitiger Event-Speicher.
|
||
/// Speichert verschlüsselte Events pro User – versteht den Payload nicht.
|
||
/// Eine LiteDB-Datei pro User in dataPath/{userId}.db
|
||
/// </summary>
|
||
public class EventStore
|
||
{
|
||
private readonly string _dataPath;
|
||
private readonly Dictionary<string, LiteDatabase> _dbs = new();
|
||
private readonly Lock _lock = new();
|
||
|
||
public EventStore(string dataPath)
|
||
{
|
||
_dataPath = dataPath;
|
||
Directory.CreateDirectory(dataPath);
|
||
}
|
||
|
||
// ── Push: Events vom Client entgegennehmen ────────────────────────────────
|
||
|
||
public PushResponse Push(string userId, List<SyncEvent> events)
|
||
{
|
||
var db = GetDb(userId);
|
||
var col = db.GetCollection<ServerEvent>("events");
|
||
col.EnsureIndex(x => x.ServerSequenceNr);
|
||
|
||
var conflictIds = new List<Guid>();
|
||
long serverSeq = GetLastSequenceNr(col);
|
||
|
||
foreach (var evt in events.OrderBy(e => e.Timestamp))
|
||
{
|
||
// Konflikt: anderes Gerät hat dieselbe Entity kürzlich geändert
|
||
var recent = col.FindOne(e =>
|
||
e.EntityType == evt.EntityType &&
|
||
e.EntityId == evt.EntityId &&
|
||
e.DeviceId != evt.DeviceId &&
|
||
e.Timestamp > evt.Timestamp.AddSeconds(-30));
|
||
|
||
if (recent is not null)
|
||
{
|
||
conflictIds.Add(evt.EventId);
|
||
continue;
|
||
}
|
||
|
||
col.Insert(new ServerEvent
|
||
{
|
||
EventId = evt.EventId,
|
||
DeviceId = evt.DeviceId,
|
||
DeviceType = evt.DeviceType,
|
||
Timestamp = evt.Timestamp,
|
||
ClientSequenceNr = evt.SequenceNr,
|
||
ServerSequenceNr = ++serverSeq,
|
||
EntityType = evt.EntityType,
|
||
EntityId = evt.EntityId,
|
||
Operation = evt.Operation,
|
||
Payload = evt.Payload, // verschlüsselt – Server liest nicht
|
||
});
|
||
}
|
||
|
||
return new PushResponse
|
||
{
|
||
Success = true,
|
||
ServerSequenceNr = serverSeq,
|
||
ConflictingEventIds = conflictIds,
|
||
};
|
||
}
|
||
|
||
// ── Pull: neue Events für Client bereitstellen ────────────────────────────
|
||
|
||
public PullResponse Pull(string userId, long since, string requestingDeviceId)
|
||
{
|
||
var db = GetDb(userId);
|
||
var col = db.GetCollection<ServerEvent>("events");
|
||
|
||
// Nur Events anderer Geräte zurückgeben
|
||
var events = col
|
||
.Find(e => e.ServerSequenceNr > since && e.DeviceId != requestingDeviceId)
|
||
.OrderBy(e => e.ServerSequenceNr)
|
||
.Take(500)
|
||
.Select(e => new SyncEvent
|
||
{
|
||
EventId = e.EventId,
|
||
DeviceId = e.DeviceId,
|
||
DeviceType = e.DeviceType,
|
||
Timestamp = e.Timestamp,
|
||
SequenceNr = e.ServerSequenceNr,
|
||
EntityType = e.EntityType,
|
||
EntityId = e.EntityId,
|
||
Operation = e.Operation,
|
||
Payload = e.Payload,
|
||
})
|
||
.ToList();
|
||
|
||
return new PullResponse
|
||
{
|
||
Events = events,
|
||
ServerSequenceNr = GetLastSequenceNr(col),
|
||
};
|
||
}
|
||
|
||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||
|
||
private LiteDatabase GetDb(string userId)
|
||
{
|
||
lock (_lock)
|
||
{
|
||
if (!_dbs.TryGetValue(userId, out var db))
|
||
{
|
||
// Sanitize userId für Dateinamen
|
||
var safeName = string.Concat(userId
|
||
.Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_'));
|
||
var path = Path.Combine(_dataPath, $"{safeName}.db");
|
||
db = new LiteDatabase(path);
|
||
_dbs[userId] = db;
|
||
}
|
||
return db;
|
||
}
|
||
}
|
||
|
||
private static long GetLastSequenceNr(ILiteCollection<ServerEvent> col)
|
||
{
|
||
var last = col.FindOne(Query.All(nameof(ServerEvent.ServerSequenceNr), Query.Descending));
|
||
return last?.ServerSequenceNr ?? 0;
|
||
}
|
||
}
|
||
|
||
// Internes Dokument im Server-EventStore
|
||
internal class ServerEvent
|
||
{
|
||
public Guid EventId { get; set; }
|
||
public string DeviceId { get; set; } = "";
|
||
public DeviceType DeviceType { get; set; }
|
||
public DateTime Timestamp { get; set; }
|
||
public long ClientSequenceNr { get; set; }
|
||
public long ServerSequenceNr { get; set; }
|
||
public string EntityType { get; set; } = "";
|
||
public string EntityId { get; set; } = "";
|
||
public string Operation { get; set; } = "";
|
||
public string Payload { get; set; } = ""; // AES-256 – Server liest nicht
|
||
}
|