feat(office): Dokumente am Telefon als PDF ansehen statt bearbeiten

Am Finger öffnet ein Office-Dokument jetzt das gewandelte PDF im eigenen
Betrachter; "Edit" daneben führt trotzdem in den Editor. Mit Maus ändert
sich nichts.

Der Web-Editor ist eine vollständige Textverarbeitung – zwischen seinen
Leisten bleibt auf 400 Pixeln vom Dokument ein Rechteck. Collabora und
OnlyOffice verhalten sich gleich; deshalb kein besserer Bedienmodus,
sondern ein anderes Werkzeug. Gewandelt wird von dem Server, der auch
bearbeitet: Sonst sähe man unterwegs ein LibreOffice-Layout eines
Dokuments, für das man OnlyOffice gewählt hat.

Je Dokument gibt es GENAU EIN PDF, und das ist keine Frage des
Aufräumens, sondern des Schlüssels: previews/<id>.pdf trägt die Fassung
nicht im Namen, eine neue Fassung überschreibt. files.preview_version
sagt, ob das Abgelegte noch passt.

Sicherheit: Die Adresse, die OnlyOffice zurückgibt, wird von uns
abgerufen – der Wähler des Transports lässt nur OFFICE_ALLOWED_HOSTS zu,
zusätzlich eine ausdrückliche Prüfung mit Test. JWT-Signatur (HS256) für
die eigene API des Dokumentenservers. Fremde Kennung 404, ohne Anmeldung
401. Das Ergebnis muss mit %PDF- beginnen, sonst landete eine
Fehlerseite im Zwischenspeicher.

Gemessen gegen einen echten OnlyOffice-Server im Docker-Netz:
  erster Abruf 1,05 s -> zweiter 0,009 s
  erneut hochgeladen -> Fassung 2, preview_version 2, weiterhin 1 Datei
  GC: herrenloses PDF von vorgestern weg, echtes bleibt
  endgültig gelöscht -> PDF weg; Papierkorb -> PDF bleibt
  ohne Anmeldung 401, fremde Kennung 404, Nicht-Office 415

Dazu im Admin-Bereich neu: Platzbedarf der abgeleiteten Daten
(Vorschaubilder, PDFs, Profilbilder). Sie zählen gegen kein Kontingent –
ein Ordner, der wächst, ohne dass ihn jemand sieht, ist eine stille
Überraschung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-31 23:30:49 +02:00
1 parent 84ae570305
commit 9c5cda75d7
22 files changed
+959 -12

No files matched your search

+57 -1
View File
@@ -72,7 +72,7 @@ func (c *Collector) Run(ctx context.Context) (Result, error) {
// "tmp/" bleibt außen vor: Das gehört dem Upload-Pfad und wird dort
// aufgeräumt; ein GC, der dort mitmischt, zerrisse laufende Uploads.
for _, prefix := range []string{"users", "thumbs", "branding", "avatars"} {
for _, prefix := range []string{"users", "thumbs", "branding", "avatars", "previews"} {
err := c.storage.Walk(ctx, prefix, func(key string, info storage.FileInfo) error {
res.Scanned++
if referenced[key] {
@@ -153,6 +153,27 @@ func (c *Collector) referencedKeys(ctx context.Context) (map[string]bool, error)
return nil, fmt.Errorf("iterate file keys: %w", err)
}
// Die als PDF angesehenen Office-Dokumente. Ohne diesen Griff hielte der
// GC jedes Vorschau-PDF für herrenlos und löschte es nach einem Tag – der
// Fehler, der uns beim Logo und beim Favicon schon passiert ist.
prevRows, err := c.db.Query(ctx,
`SELECT id::text FROM files WHERE preview_version > 0`)
if err != nil {
return nil, fmt.Errorf("query preview keys: %w", err)
}
for prevRows.Next() {
var id string
if err := prevRows.Scan(&id); err != nil {
prevRows.Close()
return nil, fmt.Errorf("scan preview key: %w", err)
}
keys["previews/"+id+".pdf"] = true
}
prevRows.Close()
if err := prevRows.Err(); err != nil {
return nil, fmt.Errorf("iterate preview keys: %w", err)
}
// Logo und App-Symbol haben keine Zeile in files, sondern stehen in den
// Einstellungen. Ohne diese Abfrage hielte der GC sie für verwaist, sobald
// "branding" mit durchsucht wird – und löschte sie beim nächsten Lauf.
@@ -263,3 +284,38 @@ func (c *Collector) Start(ctx context.Context, onDone func(Result)) {
}
}()
}
// DerivedUsage: Wie viel Platz die **abgeleiteten** Daten belegen.
//
// Vorschaubilder, Profilbilder und die als PDF gewandelten Office-Dokumente
// zählen nicht gegen das Kontingent eines Kontos – niemand hat sie
// hochgeladen, und sie entstehen jederzeit neu. Platte kosten sie trotzdem,
// und in der Anzeige „X von Y GB belegt" tauchen sie nicht auf.
//
// Genau deshalb steht die Zahl hier: Ein Ordner, der wächst, ohne dass ihn
// irgendwo jemand sieht, ist eine stille Überraschung. Sichtbar ist sie eine
// Größe, über die man entscheiden kann.
type DerivedUsage struct {
Prefix string `json:"prefix"`
Files int `json:"files"`
Bytes int64 `json:"bytes"`
}
// Derived zählt die abgeleiteten Ordner durch. Reines Lesen – dieser Aufruf
// löscht nichts.
func (c *Collector) Derived(ctx context.Context) ([]DerivedUsage, error) {
out := make([]DerivedUsage, 0, 3)
for _, prefix := range []string{"thumbs", "previews", "avatars"} {
u := DerivedUsage{Prefix: prefix}
err := c.storage.Walk(ctx, prefix, func(_ string, info storage.FileInfo) error {
u.Files++
u.Bytes += info.Size
return nil
})
if err != nil {
return nil, fmt.Errorf("walk %s: %w", prefix, err)
}
out = append(out, u)
}
return out, nil
}