122 lines
3.6 KiB
Go
122 lines
3.6 KiB
Go
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
|
|
}
|