Initial
This commit is contained in:
144
LehrerApp.Api/EventStore.cs
Normal file
144
LehrerApp.Api/EventStore.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user