munin/internal/web/handlers_import.go

146 lines
5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.
//
// Der Workspace-Admin darf zwar Zugänge verwalten, aber keine Einträge
// schreiben — deshalb prüft die Route Session.CanImport und nicht allein die
// Key-Verwaltung.
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.Refresh(r.Context(), project.ID, e.Slug)
}
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,
})
}