Files
Aether/backend/internal/files/replace.go
T
HoneyandClaude Opus 5 31a027c93a feat(aether): initial commit – self-hosted Cloud, Phasen 1–7
Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA.
Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb,
Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming,
Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office
(Collabora/OnlyOffice).

Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort)
sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 09:25:36 +02:00

94 lines
2.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}