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: // // / // _index.md // .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 // /… // /… 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() }