munin/internal/export/zip.go

101 lines
2.9 KiB
Go

package export
import (
"archive/zip"
"context"
"fmt"
"io"
"time"
"munin/internal/store"
)
// Datei ist eine Datei des Exports, noch nicht geschrieben.
type Datei struct {
// Pfad ist relativ zur Wurzel des Exports und nutzt immer "/", auch unter
// Windows — so verlangt es das ZIP-Format.
Pfad string
Inhalt string
// Geaendert ist der Zeitpunkt, den die Datei im ZIP trägt. Bei Einträgen
// ihre letzte Änderung, damit ein entpacktes Archiv in jedem Dateimanager
// die richtige Reihenfolge zeigt. Leer bedeutet: Zeitpunkt des Downloads.
Geaendert time.Time
}
// ProjektDateien stellt den Export eines einzelnen Projekts zusammen:
//
// <projekt>/
// _index.md
// <eintrag>.md
//
// Das ist genau das, was der Import eines Projekts erwartet (Unterordner
// erlaubt, Dateien mit "_" am Anfang werden übergangen).
func ProjektDateien(ctx context.Context, s *store.Store, project store.Project) ([]Datei, error) {
return projektDateien(ctx, s, project)
}
// WorkspaceDateien stellt den Export aller Projekte eines Workspace zusammen,
// im selben Aufbau wie ein Workspace-Ordner unter EXPORT_DIR:
//
// _workspace.md
// <projekt-a>/…
// <projekt-b>/…
func WorkspaceDateien(ctx context.Context, s *store.Store, ws store.Workspace) ([]Datei, error) {
dateien := []Datei{{Pfad: workspaceDateiname, Inhalt: rendereWorkspace(ws)}}
projekte, err := s.ListProjects(ctx, ws.ID)
if err != nil {
return nil, err
}
for _, p := range projekte {
projekt, err := projektDateien(ctx, s, p)
if err != nil {
return nil, fmt.Errorf("Projekt %q: %w", p.Slug, err)
}
dateien = append(dateien, projekt...)
}
return dateien, nil
}
// SchreibeZIP packt die Dateien als ZIP-Archiv nach w.
//
// # Warum erst sammeln, dann packen?
//
// Beim Download schreibt w direkt in die Antwort an den Browser. Ist der
// erste Teil des Archivs einmal unterwegs, lässt sich ein Fehler nicht mehr
// als Fehlerseite melden — der Browser bekäme nur ein kaputtes ZIP. Deshalb
// liest der Aufrufer zuerst alles aus der Datenbank (ProjektDateien,
// WorkspaceDateien). Was danach noch schiefgehen kann, ist nur noch die
// Verbindung selbst.
func SchreibeZIP(w io.Writer, dateien []Datei) error {
jetzt := time.Now()
archiv := zip.NewWriter(w)
for _, d := range dateien {
geaendert := d.Geaendert
if geaendert.IsZero() {
geaendert = jetzt
}
// CreateHeader statt Create, um Komprimierung und Zeitstempel
// selbst festzulegen. Deflate ist das Verfahren, das jedes
// Entpackprogramm kennt.
f, err := archiv.CreateHeader(&zip.FileHeader{
Name: d.Pfad,
Method: zip.Deflate,
Modified: geaendert,
})
if err != nil {
return err
}
if _, err := io.WriteString(f, d.Inhalt); err != nil {
return err
}
}
// Close schreibt das Inhaltsverzeichnis ans Ende des Archivs. Ohne diesen
// Schritt ist das ZIP unbrauchbar, auch wenn alle Dateien darin stehen.
return archiv.Close()
}