// Package web serves gallery pages and images. package web import ( "bytes" "embed" "fmt" "html/template" "io/fs" "log" "net/http" "os" "strconv" "strings" "gitbay.org/cmc/gallery/internal/library" "gitbay.org/cmc/gallery/internal/render" ) //go:embed templates/*.html var embeddedTemplates embed.FS //go:embed assets/theme.css var embeddedTheme []byte //go:embed assets/gallery.js var embeddedScript []byte type Options struct { Title string ThemePath string // replaces the embedded theme.css Templates fs.FS // replaces the embedded templates; same file names } var pageNames = []string{"home", "album", "photo", "info", "timeline", "error"} type server struct { store *library.Store rend *render.Renderer title string theme []byte pages map[string]*template.Template } type page struct { Title string Lib *library.Library Current string Album *library.Album Item *library.Item Index int Prev *library.Item Next *library.Item Code int Message string } func New(store *library.Store, rend *render.Renderer, opt Options) (http.Handler, error) { s := &server{store: store, rend: rend, title: opt.Title, theme: embeddedTheme, pages: map[string]*template.Template{}} if opt.ThemePath != "" { b, err := os.ReadFile(opt.ThemePath) if err != nil { return nil, err } s.theme = b } tfs := opt.Templates if tfs == nil { sub, err := fs.Sub(embeddedTemplates, "templates") if err != nil { return nil, err } tfs = sub } for _, name := range pageNames { t, err := template.New("base.html").Funcs(funcs).ParseFS(tfs, "base.html", name+".html") if err != nil { return nil, fmt.Errorf("template %s: %w", name, err) } s.pages[name] = t } mux := http.NewServeMux() mux.HandleFunc("GET /{$}", s.home) mux.HandleFunc("GET /info/{$}", s.info) mux.HandleFunc("GET /timeline/{$}", s.timeline) mux.HandleFunc("GET /_/theme.css", s.themeCSS) mux.HandleFunc("GET /_/gallery.js", s.script) mux.HandleFunc("GET /img/{album}/{file}/{width}", s.image) mux.HandleFunc("GET /{album}/{$}", s.album) mux.HandleFunc("GET /{album}/{file}/{$}", s.photo) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { s.error(w, http.StatusNotFound) }) return mux, nil } func (s *server) render(w http.ResponseWriter, name string, status int, p page) { p.Title = s.title if p.Lib == nil { p.Lib = s.store.Load() } var buf bytes.Buffer if err := s.pages[name].Execute(&buf, p); err != nil { log.Printf("render %s: %v", name, err) http.Error(w, "internal error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(status) buf.WriteTo(w) } func (s *server) error(w http.ResponseWriter, code int) { s.render(w, "error", code, page{Code: code, Message: http.StatusText(code)}) } func (s *server) home(w http.ResponseWriter, r *http.Request) { s.render(w, "home", http.StatusOK, page{}) } func (s *server) info(w http.ResponseWriter, r *http.Request) { lib := s.store.Load() if lib.About == "" { s.error(w, http.StatusNotFound) return } s.render(w, "info", http.StatusOK, page{Lib: lib, Current: "info"}) } func (s *server) timeline(w http.ResponseWriter, r *http.Request) { s.render(w, "timeline", http.StatusOK, page{Current: "timeline"}) } func (s *server) album(w http.ResponseWriter, r *http.Request) { lib := s.store.Load() a := lib.Album(r.PathValue("album")) if a == nil { s.error(w, http.StatusNotFound) return } s.render(w, "album", http.StatusOK, page{Lib: lib, Current: a.Slug, Album: a}) } func (s *server) photo(w http.ResponseWriter, r *http.Request) { lib := s.store.Load() a := lib.Album(r.PathValue("album")) if a == nil { s.error(w, http.StatusNotFound) return } i, it := a.Item(r.PathValue("file")) if it == nil { s.error(w, http.StatusNotFound) return } p := page{Lib: lib, Current: a.Slug, Album: a, Item: it, Index: i} if i > 0 { p.Prev = a.Items[i-1] } if i < len(a.Items)-1 { p.Next = a.Items[i+1] } s.render(w, "photo", http.StatusOK, p) } func (s *server) image(w http.ResponseWriter, r *http.Request) { lib := s.store.Load() a := lib.Album(r.PathValue("album")) if a == nil { http.NotFound(w, r) return } _, it := a.Item(r.PathValue("file")) if it == nil { http.NotFound(w, r) return } ws, ok := strings.CutSuffix(r.PathValue("width"), ".jpg") n, err := strconv.Atoi(ws) if !ok || err != nil || !render.Allowed(n) { http.Error(w, "bad width", http.StatusBadRequest) return } p, err := s.rend.Get(it, n) if err != nil { log.Printf("render %s/%s@%d: %v", it.Album, it.Name, n, err) http.Error(w, "render failed", http.StatusInternalServerError) return } w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") w.Header().Set("Content-Type", "image/jpeg") http.ServeFile(w, r, p) } func (s *server) themeCSS(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/css; charset=utf-8") w.Header().Set("Cache-Control", "no-cache") w.Write(s.theme) } func (s *server) script(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/javascript; charset=utf-8") w.Header().Set("Cache-Control", "no-cache") w.Write(embeddedScript) }