storage: DerivativeKey neben statt unter dem Revisionsobjekt (Vorschau und PDF-Derivat scheiterten mit lokalem Treiber)
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s

Mit dem lokalen Treiber ist der Revisionsschluessel eine Datei; der Schluessel
<revision>/thumbnail.png liess sich nie anlegen. Gefunden beim Ende-zu-Ende-Test
der Worker-Verdrahtung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
patrickandClaude Sonnet 5 committed 2026-09-20 00:10:15 +02:00
1 parent 9ea791817b
commit ccacbf12c2
5 files changed
+31 -5

No files matched your search

+1 -1
View File
@@ -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)
}
+8
View File
@@ -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
}
+18
View File
@@ -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)
}
}
+1 -1
View File
@@ -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)
}
@@ -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)