From ccacbf12c2ee80d7d3f43ba0e710d305bd13181f Mon Sep 17 00:00:00 2001 From: patrick Date: Sun, 20 Sep 2026 00:10:15 +0200 Subject: [PATCH] storage: DerivativeKey neben statt unter dem Revisionsobjekt (Vorschau und PDF-Derivat scheiterten mit lokalem Treiber) Mit dem lokalen Treiber ist der Revisionsschluessel eine Datei; der Schluessel /thumbnail.png liess sich nie anlegen. Gefunden beim Ende-zu-Ende-Test der Worker-Verdrahtung. Co-Authored-By: Claude Sonnet 5 --- dms/internal/pdfconvert/convert.go | 2 +- dms/internal/storage/driver.go | 8 ++++++++ dms/internal/storage/service_test.go | 18 ++++++++++++++++++ dms/internal/thumbnail/generate.go | 2 +- .../workerruntime/workerruntime_test.go | 6 +++--- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/dms/internal/pdfconvert/convert.go b/dms/internal/pdfconvert/convert.go index 860a8d1c..82bf4575 100644 --- a/dms/internal/pdfconvert/convert.go +++ b/dms/internal/pdfconvert/convert.go @@ -118,7 +118,7 @@ func (g *Generator) process(ctx context.Context, payload jobPayload) error { return fmt.Errorf("pdfconvert: chiffretext lesen: %w", err) } - pdfKey := storage.ObjectKey(payload.DocumentID, payload.RevisionID) + "/office.pdf" + pdfKey := storage.DerivativeKey(payload.DocumentID, payload.RevisionID, "office.pdf") if _, err := g.Storage.Put(ctx, pdfKey, bytes.NewReader(ciphertext), int64(len(ciphertext)), "application/octet-stream"); err != nil { return fmt.Errorf("pdfconvert: im storage ablegen: %w", err) } diff --git a/dms/internal/storage/driver.go b/dms/internal/storage/driver.go index f4a289ad..4191fb42 100644 --- a/dms/internal/storage/driver.go +++ b/dms/internal/storage/driver.go @@ -43,3 +43,11 @@ type Driver interface { func ObjectKey(documentID, revisionID string) string { return "documents/" + documentID + "/revisions/" + revisionID } + +// DerivativeKey liefert den Schluessel eines abgeleiteten Objekts (Vorschau, +// PDF-Derivat) einer Revision. Er liegt NEBEN dem Revisionsobjekt, nie darunter: +// mit dem lokalen Treiber ist ObjectKey eine Datei, ein Schluessel darunter +// liesse sich nicht anlegen. +func DerivativeKey(documentID, revisionID, name string) string { + return ObjectKey(documentID, revisionID) + "." + name +} diff --git a/dms/internal/storage/service_test.go b/dms/internal/storage/service_test.go index 946794dd..7bcdb441 100644 --- a/dms/internal/storage/service_test.go +++ b/dms/internal/storage/service_test.go @@ -3,6 +3,7 @@ package storage import ( "bytes" "context" + "strings" "sync" "testing" ) @@ -87,3 +88,20 @@ func TestService_GetMissingObjectReturnsClearError(t *testing.T) { t.Fatal("get eines fehlenden objekts haette einen fehler liefern muessen") } } + +// FDN-17-Fund: abgeleitete Objekte duerfen nicht unter dem Revisionsobjekt liegen (lokaler Treiber: Datei vs. Verzeichnis). +func TestDerivativeKey_CoexistsWithRevisionObjectOnLocalDriver(t *testing.T) { + ctx := context.Background() + svc := NewService(NewLocalDriver(t.TempDir(), []byte("secret"), "https://files.example.test"), &fakeUsageReporter{}, "acme") + original := ObjectKey("doc-1", "rev-1") + thumb := DerivativeKey("doc-1", "rev-1", "thumbnail.png") + if strings.HasPrefix(thumb, original+"/") { + t.Fatalf("derivat %q liegt unter dem revisionsobjekt %q", thumb, original) + } + if _, err := svc.Put(ctx, original, strings.NewReader("original"), 8, "application/octet-stream"); err != nil { + t.Fatalf("original: %v", err) + } + if _, err := svc.Put(ctx, thumb, strings.NewReader("thumb"), 5, "application/octet-stream"); err != nil { + t.Fatalf("derivat neben dem original ablegen: %v", err) + } +} diff --git a/dms/internal/thumbnail/generate.go b/dms/internal/thumbnail/generate.go index 31e33d68..b1441e7b 100644 --- a/dms/internal/thumbnail/generate.go +++ b/dms/internal/thumbnail/generate.go @@ -120,7 +120,7 @@ func (g *Generator) process(ctx context.Context, payload jobPayload) error { return fmt.Errorf("thumbnail: chiffretext lesen: %w", err) } - thumbKey := storage.ObjectKey(payload.DocumentID, payload.RevisionID) + "/thumbnail.png" + thumbKey := storage.DerivativeKey(payload.DocumentID, payload.RevisionID, "thumbnail.png") if _, err := g.Storage.Put(ctx, thumbKey, bytes.NewReader(ciphertext), int64(len(ciphertext)), "application/octet-stream"); err != nil { return fmt.Errorf("thumbnail: im storage ablegen: %w", err) } diff --git a/dms/internal/workerruntime/workerruntime_test.go b/dms/internal/workerruntime/workerruntime_test.go index c29ffe1d..1eeca41c 100644 --- a/dms/internal/workerruntime/workerruntime_test.go +++ b/dms/internal/workerruntime/workerruntime_test.go @@ -129,8 +129,8 @@ func TestEndToEnd_AllJobTypesReachTheirFinalState(t *testing.T) { return err == nil && ex.Status == export.StatusReady }) - if n := count(t, e, `SELECT count(*) FROM processing_jobs WHERE status <> 'completed'`); n != 0 { - t.Errorf("%d jobs nicht im endzustand completed", n) + if n := count(t, e, `SELECT count(*) FROM processing_jobs WHERE status <> 'succeeded'`); n != 0 { + t.Errorf("%d jobs nicht im endzustand succeeded", n) } } @@ -199,7 +199,7 @@ func TestMultipleWorkersProcessEachJobExactlyOnce(t *testing.T) { defer func() { cancel(); <-done }() waitFor(t, e, "alle jobs abgeschlossen", 3*time.Minute, func() bool { - return count(t, e, `SELECT count(*) FROM processing_jobs WHERE status <> 'completed'`) == 0 && count(t, e, `SELECT count(*) FROM processing_jobs`) >= docs*2 + return count(t, e, `SELECT count(*) FROM processing_jobs WHERE status <> 'succeeded'`) == 0 && count(t, e, `SELECT count(*) FROM processing_jobs`) >= docs*2 }) if n := count(t, e, `SELECT count(*) FROM revision_thumbnails WHERE status = 'ready'`); n != docs { t.Errorf("%d von %d vorschauen fertig", n, docs)