Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Assisted-by: GLM 5.3
89 lines
2.8 KiB
Go
89 lines
2.8 KiB
Go
// 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)
|
|
}
|