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>
This commit is contained in:
commit
31a027c93a
225 files changed
+38042
No files matched your search
@@ -0,0 +1,93 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user