package importer import ( "archive/zip" "bytes" "errors" "fmt" "io" "path" "strings" ) // Grenzen für ein hochgeladenes Archiv. // // Ein ZIP-Archiv kann ausgepackt ein Vielfaches seiner Größe haben. Ein // „Zip-Bomb“ macht aus wenigen Kilobyte Gigabytes. Deshalb zählt hier, was // tatsächlich ausgepackt wird, nicht was das Archiv über sich behauptet: Die // Größenangaben im Inneren eines ZIP-Archivs kann jeder frei hineinschreiben. const ( // MaxDateien ist die Höchstzahl an Markdown-Dateien je Import. MaxDateien = 5000 // MaxDateiBytes entspricht der Grenze für ein Formular der Weboberfläche. // Ein Eintrag, der sich nicht im Browser anlegen ließe, soll auch über // den Import nicht hineinkommen. MaxDateiBytes = 1 << 20 // 1 MB // MaxGesamtBytes begrenzt den ausgepackten Inhalt insgesamt. MaxGesamtBytes = 100 << 20 // 100 MB ) // Datei ist eine Markdown-Datei aus dem Archiv, noch nicht ausgewertet. type Datei struct { Pfad string Inhalt []byte } // LeseZip liest alle Markdown-Dateien aus einem ZIP-Archiv. // // Übergangen werden: // - alles, was nicht auf .md oder .markdown endet (Bilder, PDFs, …) // - Dateien, deren Name mit "_" beginnt: _index.md und _workspace.md sind // Übersichten aus dem Export, keine Einträge // - versteckte Dateien und Ordner (".git", ".obsidian") und der Ordner // "__MACOSX", den der Finder beim Packen dazulegt // // Unterordner sind erlaubt. So lässt sich ein ganzer Doku-Ordner auf einmal // hochladen. Die Einträge landen trotzdem alle nebeneinander im Projekt, // denn Munin kennt keine Ordner. func LeseZip(daten []byte) ([]Datei, error) { archiv, err := zip.NewReader(bytes.NewReader(daten), int64(len(daten))) if err != nil { return nil, errors.New("die Datei ist kein lesbares ZIP-Archiv") } var dateien []Datei var gesamt int64 for _, f := range archiv.File { if f.FileInfo().IsDir() || !istEintragsdatei(f.Name) { continue } if len(dateien) >= MaxDateien { return nil, fmt.Errorf("das Archiv enthält mehr als %d Markdown-Dateien — bitte aufteilen", MaxDateien) } inhalt, err := leseBegrenzt(f, MaxDateiBytes) if err != nil { return nil, fmt.Errorf("%s: %w", f.Name, err) } gesamt += int64(len(inhalt)) if gesamt > MaxGesamtBytes { return nil, fmt.Errorf("das Archiv ist ausgepackt größer als %d MB — bitte aufteilen", MaxGesamtBytes>>20) } dateien = append(dateien, Datei{Pfad: f.Name, Inhalt: inhalt}) } if len(dateien) == 0 { return nil, errors.New("im Archiv liegt keine Markdown-Datei (.md)") } return dateien, nil } // istEintragsdatei entscheidet anhand des Pfads, ob eine Datei ein Eintrag // werden soll (siehe LeseZip). func istEintragsdatei(pfad string) bool { for _, teil := range strings.Split(pfad, "/") { if strings.HasPrefix(teil, ".") || teil == "__MACOSX" { return false } } name := path.Base(pfad) if strings.HasPrefix(name, "_") { return false } endung := strings.ToLower(path.Ext(name)) return endung == ".md" || endung == ".markdown" } // leseBegrenzt packt eine Datei aus und bricht ab, sobald sie größer als // grenze wird. Gelesen wird ein Byte mehr als erlaubt: Kommt es an, ist die // Datei zu groß. func leseBegrenzt(f *zip.File, grenze int64) ([]byte, error) { r, err := f.Open() if err != nil { return nil, fmt.Errorf("konnte nicht ausgepackt werden: %w", err) } defer r.Close() inhalt, err := io.ReadAll(io.LimitReader(r, grenze+1)) if err != nil { return nil, fmt.Errorf("konnte nicht ausgepackt werden: %w", err) } if int64(len(inhalt)) > grenze { return nil, fmt.Errorf("größer als %d KB", grenze>>10) } return inhalt, nil }