package files import ( "context" "errors" "fmt" "io" "github.com/google/uuid" "github.com/rs/zerolog/log" "aether/internal/acl" "aether/internal/storage" ) // ReplaceContent ersetzt den Inhalt einer bestehenden Datei. // // Gebraucht von der Office-Anbindung: Der Office-Server schickt beim Speichern // den neuen Inhalt, aber keinen Ordner, keinen Namen und keine Sitzung. Der // Weg über den Upload-Handler geht deshalb nicht – der liest ein Formular und // schreibt selbst die HTTP-Antwort. // // **Ein Office-Server ist kein Sonderweg an der Datenintegrität vorbei.** Es // gelten dieselben vier Regeln wie bei jedem Upload: atomar schreiben, Prüfsumme // neu berechnen, das Kontingent beachten, und bei eingeschalteter Versionierung // die alte Fassung aufheben. func ReplaceContent( ctx context.Context, store *Store, blobs storage.Storage, f *File, owner acl.Access, content io.Reader, ) (*File, error) { // Die alte Fassung zählt nach dem Überschreiben nicht mehr zum Verbrauch – // deshalb ihre Größe als Freibetrag. remaining, unlimited, err := store.RemainingQuota(ctx, owner.OwnerID, owner.OwnerQuota, f.Size) if err != nil { return nil, fmt.Errorf("quota: %w", err) } // Nur bei eingeschalteter Versionierung wird die alte Fassung beiseite // geschoben. Sonst ersetzt Put sie atomar und der Platz bleibt frei. var versionKey string if f.VersioningEnabled { versionKey = fmt.Sprintf("%s/.versions/%s", owner.StorageRoot(), uuid.New().String()) if err := blobs.Move(ctx, f.StorageKey, versionKey); err != nil { return nil, fmt.Errorf("keep previous version: %w", err) } } if !unlimited { content = newQuotaReader(content, remaining) } checksum, size, err := blobs.Put(ctx, f.StorageKey, content) if err != nil { // Die alte Fassung ist schon verschoben – zurückrollen, sonst // verliert man die bestehende Datei an ein Speichern, das nie ankam. if versionKey != "" { if mvErr := blobs.Move(ctx, versionKey, f.StorageKey); mvErr != nil { log.Ctx(ctx).Error().Err(mvErr).Str("file_id", f.ID.String()). Msg("rollback of version move failed – blob orphaned") } } if errors.Is(err, ErrQuotaExceeded) { return nil, ErrQuotaExceeded } return nil, fmt.Errorf("write: %w", err) } if versionKey != "" { if err := store.CreateVersion(ctx, f.ID, f.Version, f.Size, f.Checksum, versionKey); err != nil { return nil, fmt.Errorf("record version: %w", err) } } updated, err := store.UpdateFileContent(ctx, f.ID, size, checksum, f.StorageKey) if err != nil { return nil, fmt.Errorf("update file: %w", err) } if versionKey != "" { // Alte Fassungen über der Grenze wegräumen – sonst wächst der Verlauf // eines oft gespeicherten Dokuments unbegrenzt. Ein Fehler hier ist // kein Grund, das Speichern scheitern zu lassen. if oldKeys, err := store.PruneVersions(ctx, f.ID, maxVersionsKept); err == nil { for _, k := range oldKeys { _ = blobs.Delete(ctx, k) } } } return updated, nil }