- 3.7: MCP-Werkzeug "related" liefert Nachbarn im Verweisgraphen in beide
Richtungen, bis zur zweiten Reihe (store.RelatedEntries).
- 3.5: Einträge, die seit über einem Jahr unverändert sind, tragen in
recall/get/list/related "stale": true; get und recall geben einen
Hinweis, die Eintragsseite eine Marke. instructions erklären das Feld.
- 3.2: Import von Markdown-Dateien als ZIP unter /p/{projekt}/import
(Rolle admin), mit Vorschau, überspringen/überschreiben, Erkennung von
Unverändertem und Papierkorb. Neue Einträge behalten Autor, Herkunft,
Prüfvermerk und Zeitstempel aus dem Export.
Nebenbei behoben: Das 1-MB-Body-Limit für Formulare lag über dem ganzen
Mux und begrenzte damit auch /mcp auf 1 MB statt MAX_API_BODY_BYTES.
LimitBodies wählt die Grenze jetzt einmal außen je Route.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 (
|
|
// 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
|
|
}
|