Files

89 lines
2.8 KiB
Go
Raw Permalink Normal View History

2026-09-18 12:03:35 +02:00
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
}