package storage import ( "context" "io" "os" "time" ) // TempFiler kann Uploads unbekannter Größe auf der Platte zwischenlagern, // statt sie im Arbeitsspeicher zu halten. Bewusst eine eigene, optionale // Schnittstelle: *os.File gehört nicht in den allgemeinen Storage-Vertrag, und // eine Implementierung ohne lokales Dateisystem kann sie schlicht weglassen. type TempFiler interface { CreateTemp(pattern string) (*os.File, error) } // LocalPather gibt den echten Pfad eines Blobs heraus – nur lesend gedacht. // // Gebraucht von der Texterkennung: ocrmypdf und tesseract sind externe // Programme und wollen einen Pfad, keinen Datenstrom. Ohne diese Schnittstelle // müsste jede Datei erst in eine vollständige Kopie geschrieben werden, bevor // sie erkannt werden kann – bei einem 300-MB-Scan also 300 MB zusätzlich auf // einer Platte, die ohnehin knapp sein kann. // // Wie TempFiler bewusst optional: Eine Ablage ohne lokales Dateisystem (S3) // lässt sie weg, und der Aufrufer fällt dann auf die Kopie zurück. type LocalPather interface { LocalPath(key string) (string, error) } // FileInfo holds metadata about a stored blob. type FileInfo struct { Size int64 Checksum string // SHA-256 hex ModTime time.Time } // Storage is the blob storage abstraction. // All implementations must guarantee atomic writes and never corrupt existing data. type Storage interface { // Put streams r to the given key atomically (temp → fsync → rename). // Returns the SHA-256 checksum and byte count written. Put(ctx context.Context, key string, r io.Reader) (checksum string, size int64, err error) // Get returns a reader for the blob at key. Caller must close it. Get(ctx context.Context, key string) (io.ReadCloser, error) // Delete removes a blob. In Phase 1 this is a hard delete used only by // cleanup/GC paths; user-facing deletes are soft-deletes in the DB. Delete(ctx context.Context, key string) error // Stat returns metadata for a blob. Stat(ctx context.Context, key string) (FileInfo, error) // Move atomically moves a blob from oldKey to newKey within the same storage root. Move(ctx context.Context, oldKey, newKey string) error // Walk ruft fn für jeden Blob unterhalb von prefix auf. Existiert der // Präfix nicht, ist das kein Fehler – nur nichts zu tun. Gedacht für die // Bereinigung verwaister Blobs, die den Bestand auf der Platte gegen die // Datenbank abgleichen muss. Walk(ctx context.Context, prefix string, fn func(key string, info FileInfo) error) error // DeletePrefix entfernt alles unterhalb eines Präfixes, etwa den kompletten // Bereich eines Nutzers ("users/alice/"). Gedacht für das endgültige Löschen // eines Kontos: danach existiert keine Datenbankzeile mehr, die noch auf die // einzelnen Blobs zeigen könnte. DeletePrefix(ctx context.Context, prefix string) error }