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:
@@ -23,17 +23,19 @@ namespace LehrerApp.Sync;
|
||||
/// Pfad bewusst NICHT geprüft (v1-Einschränkung, siehe TODO.md 10.3) — nur harte LiteDB-Unique-
|
||||
/// Constraints greifen noch und führen zum Überspringen des einzelnen Ereignisses.
|
||||
/// </summary>
|
||||
public class EventApplier(LiteDbContext db, byte[] syncKey)
|
||||
public class EventApplier(LiteDbContext db, byte[] syncKey, HttpClient? http = null)
|
||||
{
|
||||
private static readonly Dictionary<string, EntityHandler> Handlers = BuildHandlers();
|
||||
|
||||
public void Apply(SyncEvent evt)
|
||||
public async Task ApplyAsync(SyncEvent evt)
|
||||
{
|
||||
if (!Handlers.TryGetValue(evt.EntityType, out var handler)) return;
|
||||
try
|
||||
{
|
||||
var json = evt.Payload.Length == 0 ? "" : Decrypt(evt.Payload);
|
||||
handler(db, evt.Operation, evt.EntityId, json);
|
||||
if (evt.EntityType == nameof(Documentation) && evt.Operation != "Delete" && http is not null)
|
||||
await DownloadMissingAttachmentsAsync(json);
|
||||
}
|
||||
catch (LiteException)
|
||||
{
|
||||
@@ -42,6 +44,27 @@ public class EventApplier(LiteDbContext db, byte[] syncKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Anhang-Bytes reisen nicht im JSON-Ereignis mit (siehe SyncEventPublisher) - nach dem
|
||||
// Anwenden der Documentation-Metadaten fehlende, lokal noch nicht vorhandene Anhänge einzeln
|
||||
// nachladen. Gegenstück zum Upload in AttachmentSyncer.
|
||||
private async Task DownloadMissingAttachmentsAsync(string json)
|
||||
{
|
||||
var doc = JsonSerializer.Deserialize<Documentation>(json);
|
||||
if (doc is null) return;
|
||||
foreach (var attachment in doc.Attachments)
|
||||
{
|
||||
if (db.Attachments.Exists(attachment.StorageId)) continue;
|
||||
var resp = await http!.GetAsync($"/api/sync/attachments/{attachment.StorageId}");
|
||||
if (!resp.IsSuccessStatusCode) continue;
|
||||
var encrypted = await resp.Content.ReadAsByteArrayAsync();
|
||||
var decrypted = SyncCrypto.Decrypt(encrypted, syncKey);
|
||||
using var stream = new MemoryStream(decrypted);
|
||||
// Über die rohe Collection statt IAttachmentStorage.Upload, da dieses immer eine
|
||||
// neue Id vergibt - hier muss die Original-StorageId erhalten bleiben.
|
||||
db.Attachments.Upload(attachment.StorageId, attachment.FileName, stream);
|
||||
}
|
||||
}
|
||||
|
||||
private string Decrypt(string payloadBase64) =>
|
||||
Encoding.UTF8.GetString(SyncCrypto.Decrypt(Convert.FromBase64String(payloadBase64), syncKey));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user