rebound/http/server.go

164 lines
3.2 KiB
Go

package http
import (
"bytes"
"embed"
"fmt"
"html/template"
"io"
"io/fs"
"log/slog"
"net"
"net/http"
"os"
"path/filepath"
_ "embed"
"github.com/dschmidt/go-layerfs"
"github.com/pkg/errors"
)
//go:embed templates
var embeddedTemplates embed.FS
//go:embed assets
var embeddedAssets embed.FS
type Server struct {
http *http.Server
opts *Options
templates template.Template
}
var templateFuncs = template.FuncMap{
"humanSize": func(b float64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%d B", int64(b))
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB",
float64(b)/float64(div), "kMGTPE"[exp])
},
}
func (s *Server) serveHomepage(w http.ResponseWriter, r *http.Request) {
stats, err := s.opts.Stats.Snapshot()
if err != nil {
slog.Error("could not make stats snapshot", slog.Any("error", errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
data := struct {
TemplateData
Stats map[string]float64
}{
TemplateData: *s.opts.TemplateData,
Stats: stats,
}
s.opts.Stats.Add(StatTotalPageView, 1, 0)
s.renderTemplate(w, "index", data)
}
func (s *Server) Serve(l net.Listener) error {
templatesFilesystem, err := s.getCustomizedFilesystem(embeddedTemplates)
if err != nil {
return errors.WithStack(err)
}
if err := s.parseTemplates(templatesFilesystem); err != nil {
return errors.WithStack(err)
}
assetsFilesystem, err := s.getCustomizedFilesystem(embeddedAssets)
if err != nil {
return errors.WithStack(err)
}
mux := http.NewServeMux()
mux.Handle("/assets/", http.FileServer(http.FS(assetsFilesystem)))
mux.HandleFunc("/", s.serveHomepage)
httpServer := &http.Server{
Handler: mux,
}
s.http = httpServer
if err := s.http.Serve(l); err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *Server) parseTemplates(fs fs.FS) error {
templates, err := template.New("").Funcs(templateFuncs).ParseFS(fs, "templates/*.html")
if err != nil {
return errors.WithStack(err)
}
s.templates = *templates
return nil
}
func (s *Server) renderTemplate(w http.ResponseWriter, name string, data any) {
var buff bytes.Buffer
if err := s.templates.ExecuteTemplate(&buff, name, data); err != nil {
s.log("[ERROR] %+s", errors.WithStack(err))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if _, err := io.Copy(w, &buff); err != nil {
s.log("[ERROR] %+s", errors.WithStack(err))
}
}
func (s *Server) log(message string, args ...any) {
s.opts.Logger(message, args...)
}
func (s *Server) getCustomizedFilesystem(base fs.FS) (fs.FS, error) {
filesystems := []fs.FS{}
if s.opts.CustomDir != "" {
absPath, err := filepath.Abs(s.opts.CustomDir)
if err != nil {
return nil, errors.WithStack(err)
}
filesystems = append(filesystems, os.DirFS(absPath))
}
filesystems = append(filesystems, base)
return layerfs.New(filesystems...), nil
}
func NewServer(funcs ...OptionFunc) *Server {
opts := DefaultOptions()
for _, fn := range funcs {
fn(opts)
}
return &Server{
opts: opts,
}
}