Add gallery admin and video media support.
This updates gallery handling to support video playback with generated poster thumbnails, adds authenticated admin upload/delete flows, and improves dev/runtime behavior including reliable thumbnail generation and media-safe response handling. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,8 +4,10 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"technical.kiwi/website/internal/auth"
|
||||
"technical.kiwi/website/internal/contact"
|
||||
"technical.kiwi/website/internal/gallery"
|
||||
"technical.kiwi/website/internal/mail"
|
||||
@@ -22,6 +24,9 @@ type Server struct {
|
||||
HasHero bool
|
||||
Mail *mail.Config
|
||||
ContactEnabled bool
|
||||
Auth auth.Config
|
||||
Sessions *auth.Sessions
|
||||
galleryMu sync.RWMutex
|
||||
}
|
||||
|
||||
// New builds a Server after loading the image gallery.
|
||||
@@ -43,6 +48,7 @@ func New(imagesDir, staticDir string, mailCfg *mail.Config) (*Server, error) {
|
||||
}
|
||||
log.Printf("hero image: %s", hero.RelPath)
|
||||
}
|
||||
authCfg := auth.Load()
|
||||
srv := &Server{
|
||||
ImagesDir: imagesDir,
|
||||
StaticDir: staticDir,
|
||||
@@ -51,20 +57,32 @@ func New(imagesDir, staticDir string, mailCfg *mail.Config) (*Server, error) {
|
||||
HasHero: hasHero,
|
||||
Mail: mailCfg,
|
||||
ContactEnabled: mailCfg != nil,
|
||||
Auth: authCfg,
|
||||
Sessions: auth.NewSessions(),
|
||||
}
|
||||
go func() {
|
||||
if err := gallery.EnsureThumbnails(imagesDir); err != nil {
|
||||
log.Printf("gallery thumbnails: %v", err)
|
||||
return
|
||||
}
|
||||
if authCfg.Enabled {
|
||||
log.Print("admin: enabled at /admin/")
|
||||
} else {
|
||||
log.Print("admin: disabled (set ADMIN_USER and ADMIN_PASSWORD)")
|
||||
}
|
||||
if err := gallery.EnsureThumbnails(imagesDir); err != nil {
|
||||
log.Printf("gallery thumbnails: %v", err)
|
||||
} else {
|
||||
log.Print("gallery thumbnails: ready")
|
||||
}()
|
||||
}
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
func (s *Server) snapshot() ([]gallery.Image, gallery.Image, bool) {
|
||||
s.galleryMu.RLock()
|
||||
defer s.galleryMu.RUnlock()
|
||||
return s.Images, s.Hero, s.HasHero
|
||||
}
|
||||
|
||||
func (s *Server) index(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
templates.Home(s.Images, s.Hero, s.HasHero, s.ContactEnabled).Render(r.Context(), w)
|
||||
images, hero, hasHero := s.snapshot()
|
||||
templates.Home(images, hero, hasHero, s.ContactEnabled).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func (s *Server) contact(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -107,54 +125,31 @@ func (s *Server) contact(w http.ResponseWriter, r *http.Request) {
|
||||
templates.ContactSuccess().Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func (s *Server) galleryPartial(w http.ResponseWriter, r *http.Request) {
|
||||
album := r.URL.Query().Get("album")
|
||||
collection := r.URL.Query().Get("collection")
|
||||
filtered := s.filterImages(album, collection)
|
||||
templates.GalleryGrid(filtered).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func (s *Server) imageModal(w http.ResponseWriter, r *http.Request) {
|
||||
rel := r.PathValue("path")
|
||||
if !gallery.SafeRelPath(rel) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
img, ok := s.findImage(rel)
|
||||
images, _, _ := s.snapshot()
|
||||
img, ok := findImage(images, rel)
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
idx := s.indexOf(rel)
|
||||
idx := indexOf(images, rel)
|
||||
prev, next := "", ""
|
||||
if idx > 0 {
|
||||
prev = s.Images[idx-1].RelPath
|
||||
prev = images[idx-1].RelPath
|
||||
}
|
||||
if idx >= 0 && idx < len(s.Images)-1 {
|
||||
next = s.Images[idx+1].RelPath
|
||||
if idx >= 0 && idx < len(images)-1 {
|
||||
next = images[idx+1].RelPath
|
||||
}
|
||||
templates.ImageModal(img, prev, next).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func (s *Server) filterImages(album, collection string) []gallery.Image {
|
||||
if album == "" && collection == "" {
|
||||
return s.Images
|
||||
}
|
||||
var out []gallery.Image
|
||||
for _, img := range s.Images {
|
||||
if album != "" && img.Album != album {
|
||||
continue
|
||||
}
|
||||
if collection != "" && img.CollectionKey != collection {
|
||||
continue
|
||||
}
|
||||
out = append(out, img)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Server) findImage(rel string) (gallery.Image, bool) {
|
||||
for _, img := range s.Images {
|
||||
func findImage(images []gallery.Image, rel string) (gallery.Image, bool) {
|
||||
for _, img := range images {
|
||||
if img.RelPath == rel {
|
||||
return img, true
|
||||
}
|
||||
@@ -162,8 +157,8 @@ func (s *Server) findImage(rel string) (gallery.Image, bool) {
|
||||
return gallery.Image{}, false
|
||||
}
|
||||
|
||||
func (s *Server) indexOf(rel string) int {
|
||||
for i, img := range s.Images {
|
||||
func indexOf(images []gallery.Image, rel string) int {
|
||||
for i, img := range images {
|
||||
if img.RelPath == rel {
|
||||
return i
|
||||
}
|
||||
@@ -175,9 +170,15 @@ func (s *Server) indexOf(rel string) int {
|
||||
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /{$}", s.index)
|
||||
mux.HandleFunc("POST /contact", s.contact)
|
||||
mux.HandleFunc("GET /gallery", s.galleryPartial)
|
||||
mux.HandleFunc("GET /gallery/{path...}", s.imageModal)
|
||||
staticHandler := middleware.CacheStatic(s.StaticDir, 7*24*time.Hour)
|
||||
mux.Handle("GET /static/", http.StripPrefix("/static/", staticHandler))
|
||||
mux.Handle("GET /images/", http.StripPrefix("/images/", middleware.CacheImages(s.ImagesDir)))
|
||||
|
||||
mux.HandleFunc("GET /admin/login", s.adminLogin)
|
||||
mux.HandleFunc("POST /admin/login", s.adminLoginPost)
|
||||
mux.HandleFunc("POST /admin/logout", s.adminLogout)
|
||||
mux.HandleFunc("GET /admin/", s.requireAdmin(s.adminIndex))
|
||||
mux.HandleFunc("POST /admin/upload", s.requireAdmin(s.adminUpload))
|
||||
mux.HandleFunc("POST /admin/delete", s.requireAdmin(s.adminDelete))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user