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 (
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"sourcedock.dev/petrbalvin/volumen/internal/store"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func (a *Admin) registerMediaRoutes(mux *http.ServeMux) {
|
|
|
|
|
mux.HandleFunc("GET /admin/media", a.requireLogin(a.handleMediaLibrary))
|
|
|
|
|
mux.HandleFunc("POST /admin/media/{name}/delete", a.requireLogin(a.handleMediaDelete))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *Admin) handleMediaLibrary(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
data := a.pageData(r)
|
|
|
|
|
items := a.deps.Store.ListMedia()
|
2026-09-29 23:47:27 +02:00
|
|
|
// A profile photo of any account is not library content. Avatars
|
|
|
|
|
// stored since the avatar directory existed never reach this list
|
|
|
|
|
// at all; the filter keeps the flat photos of installations that
|
|
|
|
|
// predate it out of the tiles the same way.
|
|
|
|
|
photos := make(map[string]bool)
|
|
|
|
|
for _, user := range a.deps.Users.All() {
|
|
|
|
|
if user.Photo != "" {
|
|
|
|
|
photos[user.Photo] = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(photos) > 0 {
|
|
|
|
|
filtered := items[:0]
|
|
|
|
|
for _, item := range items {
|
|
|
|
|
if !photos[item.URL] {
|
|
|
|
|
filtered = append(filtered, item)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
items = filtered
|
|
|
|
|
}
|
2026-09-18 12:03:35 +02:00
|
|
|
data.MediaItems = mediaRows(items)
|
|
|
|
|
data.MediaTotal = humanSize(totalSize(items))
|
|
|
|
|
data.Crumbs = []Crumb{{Label: "Media", IsLast: true, UI: true}}
|
|
|
|
|
a.renderPage(w, r, "media.html", data, http.StatusOK)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// totalSize sums the byte sizes of the media library.
|
|
|
|
|
func totalSize(items []store.Media) int64 {
|
|
|
|
|
var total int64
|
|
|
|
|
for _, item := range items {
|
|
|
|
|
total += item.Size
|
|
|
|
|
}
|
|
|
|
|
return total
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// humanSize formats a byte count for the library summary: kilobytes
|
|
|
|
|
// below a megabyte (rounded up, so nothing reads as zero), megabytes
|
|
|
|
|
// above it, empty for an empty library.
|
|
|
|
|
func humanSize(total int64) string {
|
|
|
|
|
switch {
|
|
|
|
|
case total <= 0:
|
|
|
|
|
return ""
|
|
|
|
|
case total < 1024*1024:
|
|
|
|
|
kb := (total + 1023) / 1024
|
|
|
|
|
return fmt.Sprintf("%d kB", kb)
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Sprintf("%.1f MB", float64(total)/(1024*1024))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *Admin) handleMediaDelete(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !a.requireCSRF(w, r) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
name := r.PathValue("name")
|
|
|
|
|
if !a.deps.Store.DeleteMedia("/media/" + strings.TrimPrefix(name, "/")) {
|
|
|
|
|
http.Error(w, "File not found", http.StatusNotFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
a.record(r, "media.deleted", fmt.Sprintf("/media/%s", name), nil)
|
|
|
|
|
http.Redirect(w, r, "/admin/media", http.StatusSeeOther)
|
|
|
|
|
}
|