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, } }