- 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>
142 lines
4.8 KiB
Go
142 lines
4.8 KiB
Go
package web
|
||
|
||
// Import von Markdown-Dateien in ein Projekt (AUSBAUSTUFEN.md 3.2).
|
||
//
|
||
// # Warum über die Weboberfläche und nicht als Kommandozeilenbefehl?
|
||
//
|
||
// Der Suchindex liegt im Arbeitsspeicher des laufenden Servers (siehe
|
||
// internal/search). Ein zweiter Prozess, der direkt in die Datenbank
|
||
// schreibt, könnte ihn nicht nachziehen, und die importierten Einträge blieben
|
||
// bis zum nächsten Neustart für recall unsichtbar. Hier läuft der Import im
|
||
// Server selbst und aktualisiert den Index im selben Zug.
|
||
//
|
||
// Dazu kommt: So können Projekt-Admins ihre vorhandene Dokumentation selbst
|
||
// einspielen, ohne auf den Plattform-Admin zu warten.
|
||
//
|
||
// # Rechte
|
||
//
|
||
// Nur mit der Rolle admin, wie die Zugänge. Ein Import schreibt womöglich
|
||
// tausende Einträge auf einmal, und wer ihn im Modus „Überschreiben“ startet,
|
||
// ändert Einträge, die andere geschrieben haben.
|
||
|
||
import (
|
||
"errors"
|
||
"io"
|
||
"net/http"
|
||
|
||
"munin/internal/auth"
|
||
"munin/internal/importer"
|
||
"munin/internal/store"
|
||
)
|
||
|
||
// importData sind die Daten der Import-Seite.
|
||
type importData struct {
|
||
// Bericht ist nach einem Lauf gesetzt, sonst nil.
|
||
Bericht *importer.Bericht
|
||
|
||
// Vorschau: Der Bericht zeigt nur, was passieren würde.
|
||
Vorschau bool
|
||
|
||
// Modus ist die zuletzt gewählte Einstellung, damit das Formular sie nach
|
||
// einer Vorschau beibehält.
|
||
Modus importer.Modus
|
||
|
||
MaxDateien int
|
||
}
|
||
|
||
// handleImportForm zeigt das Formular zum Hochladen.
|
||
func (s *Server) handleImportForm(w http.ResponseWriter, r *http.Request) {
|
||
s.renderImport(w, r, http.StatusOK, importData{Modus: importer.ModusUeberspringen}, "", "")
|
||
}
|
||
|
||
// handleImport nimmt ein ZIP-Archiv entgegen und importiert es, oder zeigt
|
||
// bei gesetzter Vorschau nur, was passieren würde.
|
||
func (s *Server) handleImport(w http.ResponseWriter, r *http.Request) {
|
||
ws, sess := s.workspaceAndSession(r)
|
||
project, _ := ProjectFrom(r)
|
||
|
||
daten := importData{
|
||
Modus: importer.Modus(r.FormValue("modus")),
|
||
Vorschau: r.FormValue("vorschau") == "1",
|
||
}
|
||
if daten.Modus != importer.ModusUeberschreiben {
|
||
daten.Modus = importer.ModusUeberspringen
|
||
}
|
||
|
||
// r.FormFile liest das hochgeladene Feld "archiv". Die Größe ist schon
|
||
// begrenzt (LimitBodies in middleware.go), ReadAll kann also nicht mehr
|
||
// als das Import-Limit in den Speicher holen.
|
||
datei, _, err := r.FormFile("archiv")
|
||
if err != nil {
|
||
var zuGross *http.MaxBytesError
|
||
if errors.As(err, &zuGross) {
|
||
s.renderImport(w, r, http.StatusRequestEntityTooLarge, daten, "",
|
||
"Das Archiv ist zu groß. Bitte in mehrere kleinere aufteilen.")
|
||
return
|
||
}
|
||
s.renderImport(w, r, http.StatusBadRequest, daten, "", "Bitte ein ZIP-Archiv auswählen.")
|
||
return
|
||
}
|
||
defer datei.Close()
|
||
|
||
inhalt, err := io.ReadAll(datei)
|
||
if err != nil {
|
||
s.renderImport(w, r, http.StatusBadRequest, daten, "", "Das Archiv konnte nicht gelesen werden.")
|
||
return
|
||
}
|
||
|
||
dateien, err := importer.LeseZip(inhalt)
|
||
if err != nil {
|
||
s.renderImport(w, r, http.StatusBadRequest, daten, "", "Import nicht möglich: "+err.Error()+".")
|
||
return
|
||
}
|
||
|
||
bericht, err := importer.Importiere(r.Context(), ws.Store, project.ID, dateien, importer.Optionen{
|
||
Modus: daten.Modus,
|
||
Vorschau: daten.Vorschau,
|
||
Autor: sess.ActorName(),
|
||
})
|
||
// Auch nach einem Abbruch sind die bis dahin geschriebenen Einträge da.
|
||
// Deshalb Index und Protokoll in jedem Fall nachziehen, erst danach den
|
||
// Fehler melden.
|
||
for _, e := range bericht.Geschrieben {
|
||
ws.Search.Update(project.ID, e)
|
||
}
|
||
if !daten.Vorschau && len(bericht.Geschrieben) > 0 {
|
||
// Ein Protokolleintrag für den ganzen Import statt einer je Datei.
|
||
// Sonst verschwände alles andere im Protokoll unter tausend Zeilen.
|
||
// Und die Nutzungsübersicht (4.6) soll zählen, was die Teams im
|
||
// Alltag schreiben, nicht einen einmaligen Import.
|
||
ws.Store.LogAudit(r.Context(), store.AuditEvent{
|
||
ProjectID: project.ID, APIKeyID: sess.Key.ID, Actor: sess.ActorName(),
|
||
Action: store.ActionEntryImport, Source: store.SourceWeb, IP: auth.ClientIP(r),
|
||
Detail: bericht.Zusammenfassung(),
|
||
})
|
||
}
|
||
if err != nil {
|
||
s.serverError(w, "Import abgebrochen nach "+bericht.Zusammenfassung(), err)
|
||
return
|
||
}
|
||
|
||
daten.Bericht = &bericht
|
||
hinweis := "Import abgeschlossen: " + bericht.Zusammenfassung() + "."
|
||
if daten.Vorschau {
|
||
hinweis = "Vorschau, noch nichts geschrieben: " + bericht.Zusammenfassung() +
|
||
". Zum Übernehmen dasselbe Archiv ohne Häkchen bei „Nur Vorschau“ erneut hochladen."
|
||
}
|
||
s.renderImport(w, r, http.StatusOK, daten, hinweis, "")
|
||
}
|
||
|
||
// renderImport zeigt die Import-Seite mit optionaler Meldung.
|
||
func (s *Server) renderImport(w http.ResponseWriter, r *http.Request, status int, daten importData, hinweis, fehler string) {
|
||
project, _ := ProjectFrom(r)
|
||
daten.MaxDateien = importer.MaxDateien
|
||
|
||
s.render(w, r, "import.html", status, pageData{
|
||
Title: "Import – " + project.Name,
|
||
Bereich: "import",
|
||
Flash: hinweis,
|
||
Error: fehler,
|
||
Data: daten,
|
||
})
|
||
}
|