Baustein 6: Datei-Anhaenge synchronisieren (Kapitel 10)
Anhaenge laufen bewusst NICHT ueber den JSON-Ereigniskanal (wuerde ihn
fuer Fotos/Scans stark aufblaehen), sondern ueber einen eigenen
verschluesselten Binaerkanal - analog zum bereits bestehenden Muster
in SnapshotService.
- Neue Endpunkte POST/GET /api/sync/attachments/{storageId} in
LehrerApp.Api (AttachmentStore, dateibasiert je Nutzer)
- EventQueue: neue, vom JSON-Ereignis getrennte Warteliste fuer
ausstehende Uploads (SyncEventPublisher traegt Anhaenge einer
gespeicherten Documentation dort ein)
- AttachmentSyncer laedt ausstehende Anhaenge hoch (in
SyncEngine.SyncNowAsync nach dem Event-Push)
- EventApplier laedt fehlende Anhaenge nach dem Anwenden eines
Documentation-Ereignisses nach - ueber die rohe Collection statt
IAttachmentStorage.Upload, da dieses immer eine neue Id vergaebe und
hier die Original-StorageId erhalten bleiben muss
Round-Trip-Tests belegen byteidentische Uebertragung.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ public class SyncEngine : IDisposable
|
||||
private readonly EventQueue _queue;
|
||||
private readonly ConflictResolver _resolver;
|
||||
private readonly EventApplier _applier;
|
||||
private readonly AttachmentSyncer _attachments;
|
||||
private readonly HttpClient _http;
|
||||
private readonly SyncConfig _config;
|
||||
private readonly Timer _timer;
|
||||
@@ -20,13 +21,14 @@ public class SyncEngine : IDisposable
|
||||
public event Action<SyncStatus>? StatusChanged;
|
||||
|
||||
public SyncEngine(EventQueue queue, ConflictResolver resolver, EventApplier applier,
|
||||
HttpClient http, SyncConfig config)
|
||||
AttachmentSyncer attachments, HttpClient http, SyncConfig config)
|
||||
{
|
||||
_queue = queue;
|
||||
_resolver = resolver;
|
||||
_applier = applier;
|
||||
_http = http;
|
||||
_config = config;
|
||||
_queue = queue;
|
||||
_resolver = resolver;
|
||||
_applier = applier;
|
||||
_attachments = attachments;
|
||||
_http = http;
|
||||
_config = config;
|
||||
_timer = new Timer(
|
||||
async _ => await SyncNowAsync(true), null,
|
||||
TimeSpan.FromMinutes(config.AutoSyncIntervalMinutes),
|
||||
@@ -42,6 +44,7 @@ public class SyncEngine : IDisposable
|
||||
try
|
||||
{
|
||||
var (pushed, _) = await PushAsync();
|
||||
await _attachments.UploadPendingAsync(_queue);
|
||||
var (pulled, conflicts) = await PullAsync();
|
||||
_queue.SetLastSyncAt(DateTime.UtcNow);
|
||||
SetState(SyncState.Idle);
|
||||
@@ -77,10 +80,10 @@ public class SyncEngine : IDisposable
|
||||
foreach (var evt in resp.Events)
|
||||
{
|
||||
var c = _resolver.TryResolve(evt, _config.DeviceId);
|
||||
if (c is null) { _applier.Apply(evt); continue; }
|
||||
if (c is null) { await _applier.ApplyAsync(evt); continue; }
|
||||
_queue.AddConflict(c);
|
||||
conflicts++;
|
||||
if (c.Resolution == "RemoteWon") _applier.Apply(evt);
|
||||
if (c.Resolution == "RemoteWon") await _applier.ApplyAsync(evt);
|
||||
}
|
||||
_queue.SetLastServerSeq(resp.ServerSequenceNr);
|
||||
return (resp.Events.Count, conflicts);
|
||||
|
||||
Reference in New Issue
Block a user