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 ( // MaxFiles ist die Höchstzahl an Markdown-Dateien je Import. MaxFiles = 5000 // MaxFileBytes 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. MaxFileBytes = 1 << 20 // 1 MB // MaxTotalBytes begrenzt den ausgepackten Inhalt insgesamt. MaxTotalBytes = 100 << 20 // 100 MB ) // File ist eine Markdown-Datei aus dem Archiv, noch nicht ausgewertet. type File struct { Path string Content []byte } // ReadZip 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 ReadZip(data []byte) ([]File, error) { archive, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { return nil, errors.New("die Datei ist kein lesbares ZIP-Archiv") } var files []File var total int64 for _, f := range archive.File { if f.FileInfo().IsDir() || !isEntryFile(f.Name) { continue } if len(files) >= MaxFiles { return nil, fmt.Errorf("das Archiv enthält mehr als %d Markdown-Dateien — bitte aufteilen", MaxFiles) } content, err := readLimited(f, MaxFileBytes) if err != nil { return nil, fmt.Errorf("%s: %w", f.Name, err) } total += int64(len(content)) if total > MaxTotalBytes { return nil, fmt.Errorf("das Archiv ist ausgepackt größer als %d MB — bitte aufteilen", MaxTotalBytes>>20) } files = append(files, File{Path: f.Name, Content: content}) } if len(files) == 0 { return nil, errors.New("im Archiv liegt keine Markdown-Datei (.md)") } return files, nil } // isEntryFile entscheidet anhand des Pfads, ob eine Datei ein Eintrag // werden soll (siehe ReadZip). func isEntryFile(filePath string) bool { for _, part := range strings.Split(filePath, "/") { if strings.HasPrefix(part, ".") || part == "__MACOSX" { return false } } name := path.Base(filePath) if strings.HasPrefix(name, "_") { return false } suffix := strings.ToLower(path.Ext(name)) return suffix == ".md" || suffix == ".markdown" } // readLimited packt eine Datei aus und bricht ab, sobald sie größer als // limit wird. Gelesen wird ein Byte mehr als erlaubt: Kommt es an, ist die // Datei zu groß. func readLimited(f *zip.File, limit int64) ([]byte, error) { r, err := f.Open() if err != nil { return nil, fmt.Errorf("konnte nicht ausgepackt werden: %w", err) } defer r.Close() content, err := io.ReadAll(io.LimitReader(r, limit+1)) if err != nil { return nil, fmt.Errorf("konnte nicht ausgepackt werden: %w", err) } if int64(len(content)) > limit { return nil, fmt.Errorf("größer als %d KB", limit>>10) } return content, nil }