package export import ( "archive/zip" "context" "fmt" "io" "time" "munin/internal/store" ) // File ist eine Datei des Exports, noch nicht geschrieben. type File struct { // Path ist relativ zur Wurzel des Exports und nutzt immer "/", auch unter // Windows — so verlangt es das ZIP-Format. Path string Content string // Modified 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. Modified time.Time } // ProjectFiles 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 ProjectFiles(ctx context.Context, s *store.Store, project store.Project) ([]File, error) { return projectFiles(ctx, s, project) } // WorkspaceFiles stellt den Export aller Projekte eines Workspace zusammen, // im selben Aufbau wie ein Workspace-Ordner unter EXPORT_DIR: // // _workspace.md // /… // /… func WorkspaceFiles(ctx context.Context, s *store.Store, ws store.Workspace) ([]File, error) { files := []File{{Path: workspaceFilename, Content: renderWorkspace(ws)}} projects, err := s.ListProjects(ctx, ws.ID) if err != nil { return nil, err } for _, p := range projects { project, err := projectFiles(ctx, s, p) if err != nil { return nil, fmt.Errorf("Projekt %q: %w", p.Slug, err) } files = append(files, project...) } return files, nil } // WriteZIP 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 (ProjectFiles, // WorkspaceFiles). Was danach noch schiefgehen kann, ist nur noch die // Verbindung selbst. func WriteZIP(w io.Writer, files []File) error { now := time.Now() archive := zip.NewWriter(w) for _, d := range files { changed := d.Modified if changed.IsZero() { changed = now } // CreateHeader statt Create, um Komprimierung und Zeitstempel // selbst festzulegen. Deflate ist das Verfahren, das jedes // Entpackprogramm kennt. f, err := archive.CreateHeader(&zip.FileHeader{ Name: d.Path, Method: zip.Deflate, Modified: changed, }) if err != nil { return err } if _, err := io.WriteString(f, d.Content); 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 archive.Close() }