2023-09-21 05:54:08 +02:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"embed"
|
2023-09-24 20:21:44 +02:00
|
|
|
"fmt"
|
2023-09-21 05:54:08 +02:00
|
|
|
"html/template"
|
|
|
|
"io"
|
|
|
|
"io/fs"
|
2023-09-24 20:21:44 +02:00
|
|
|
"log/slog"
|
2023-09-21 05:54:08 +02:00
|
|
|
"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
|
|
|
|
}
|
|
|
|
|
2023-09-24 20:21:44 +02:00
|
|
|
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])
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2023-09-21 05:54:08 +02:00
|
|
|
func (s *Server) serveHomepage(w http.ResponseWriter, r *http.Request) {
|
2023-09-24 20:21:44 +02:00
|
|
|
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)
|
2023-09-21 05:54:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2023-09-24 20:21:44 +02:00
|
|
|
templates, err := template.New("").Funcs(templateFuncs).ParseFS(fs, "templates/*.html")
|
2023-09-21 05:54:08 +02:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
}
|