// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 package admin import ( json "encoding/json/v2" "log/slog" "net/http" "strconv" "sourcedock.dev/petrbalvin/volumen/internal/i18n" "sourcedock.dev/petrbalvin/volumen/internal/imagefile" "sourcedock.dev/petrbalvin/volumen/internal/web" ) // writeAdminJSON writes one JSON object response; encoding/json escapes // what a browser's JSON.parse requires, which a %q verb does not. func writeAdminJSON(w http.ResponseWriter, status int, value map[string]any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.MarshalWrite(w, value, json.Deterministic(true)); err != nil { slog.Warn("admin: cannot encode JSON response", "error", err) } } func (a *Admin) handleUpload(w http.ResponseWriter, r *http.Request) { if !a.requireCSRF(w, r) { return } file, header, err := r.FormFile("file") if err != nil { writeAdminJSONError(w, http.StatusBadRequest, "no_file", i18n.Admin.T(a.langFor(r), "No file was uploaded.")) return } defer file.Close() limit := int64(a.deps.Config.Admin.MaxUploadBytes) raw, err := readLimited(file, limit) if err != nil { writeAdminJSONError(w, http.StatusRequestEntityTooLarge, "too_large", i18n.Admin.Tf(a.langFor(r), "The file could not be read (limit %s bytes).", strconv.FormatInt(limit, 10))) return } if errMsg := validateImageData(raw); errMsg != "" { writeAdminJSONError(w, http.StatusUnsupportedMediaType, errMsg, i18n.Admin.T(a.langFor(r), "Only WebP, AVIF and SVG images are supported.")) return } url, err := a.deps.Store.StoreUpload(header.Filename, raw) if err != nil { writeAdminJSONError(w, http.StatusInternalServerError, "upload_failed", i18n.Admin.T(a.langFor(r), "The upload could not be stored.")) return } writeAdminJSON(w, http.StatusOK, map[string]any{"url": url}) } func writeAdminJSONError(w http.ResponseWriter, status int, code, message string) { writeAdminJSON(w, status, map[string]any{"error": code, "message": message}) } // validateImageData reports why data is not an acceptable upload. The // stored extension is taken from the byte signature, so the declared // filename's type is irrelevant: what matters is that the bytes are one // of the accepted image formats. func validateImageData(data []byte) string { if imagefile.Detect(data) == "" { return "invalid_signature" } return "" } func (a *Admin) handleIcon(w http.ResponseWriter, _ *http.Request) { a.serveStaticSVG(w, "volumen-icon.svg") } func (a *Admin) serveStaticSVG(w http.ResponseWriter, name string) { data, err := web.StaticFile(name) if err != nil { w.WriteHeader(http.StatusNotFound) return } w.Header().Set("Content-Type", "image/svg+xml") w.WriteHeader(http.StatusOK) _, _ = w.Write(data) }