package server import ( "fmt" "html" "io/fs" "log" "net/http" "os" "path/filepath" "strings" "sync" "time" webbundle "luxtools/web" ) const ( address = ":5000" htmlContentType = "text/html; charset=utf-8" notFoundTemplate = `
Error

Route %s not found.

` ) // Config contains the runtime options for the HTTP server. type Config struct { MediaDir string } // Server hosts the Luxtools HTTP handlers. type Server struct { mux *http.ServeMux indexPage []byte staticFS fs.FS mediaFS http.FileSystem counterMu sync.Mutex clickCount int } // New constructs a configured Server ready to serve requests. func New(cfg Config) (*Server, error) { if strings.TrimSpace(cfg.MediaDir) == "" { return nil, fmt.Errorf("media directory is required") } mediaDir := filepath.Clean(cfg.MediaDir) info, err := os.Stat(mediaDir) if err != nil { return nil, fmt.Errorf("media directory check failed: %w", err) } if !info.IsDir() { return nil, fmt.Errorf("media directory must be a directory: %s", mediaDir) } indexPage, err := webbundle.Content.ReadFile("templates/index.html") if err != nil { return nil, fmt.Errorf("failed to read index template: %w", err) } staticFS, err := fs.Sub(webbundle.Content, "static") if err != nil { return nil, fmt.Errorf("failed to prepare static assets: %w", err) } s := &Server{ mux: http.NewServeMux(), indexPage: indexPage, staticFS: staticFS, mediaFS: http.Dir(mediaDir), } s.registerRoutes() return s, nil } // Router exposes the configured http.Handler for the server. func (s *Server) Router() http.Handler { return s.mux } // Address returns the default listen address for the server. func (s *Server) Address() string { return address } func (s *Server) registerRoutes() { staticHandler := http.FileServer(http.FS(s.staticFS)) mediaHandler := http.FileServer(s.mediaFS) s.mux.Handle("/static/", http.StripPrefix("/static/", allowGetHead(staticHandler))) s.mux.Handle("/media/", http.StripPrefix("/media/", allowGetHead(mediaHandler))) s.mux.HandleFunc("/", s.handleRequest) } func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/": s.sendHTML(w, http.StatusOK, s.indexPage) case "/counter": s.handleCounter(w, r) case "/time": s.sendHTMLString(w, http.StatusOK, renderTime(time.Now())) default: s.sendHTMLString(w, http.StatusNotFound, fmt.Sprintf(notFoundTemplate, html.EscapeString(r.URL.Path))) } } func (s *Server) handleCounter(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost || r.Method == http.MethodPut { s.counterMu.Lock() s.clickCount++ s.counterMu.Unlock() } s.counterMu.Lock() count := s.clickCount s.counterMu.Unlock() s.sendHTMLString(w, http.StatusOK, renderCounter(count)) } func (s *Server) sendHTML(w http.ResponseWriter, status int, body []byte) { w.Header().Set("Content-Type", htmlContentType) w.WriteHeader(status) if len(body) > 0 { if _, err := w.Write(body); err != nil { log.Printf("write response failed: %v", err) } } } func (s *Server) sendHTMLString(w http.ResponseWriter, status int, body string) { s.sendHTML(w, status, []byte(body)) } func renderCounter(count int) string { return fmt.Sprintf(`

Clicks: %d

`, count) } func renderTime(now time.Time) string { return fmt.Sprintf(`

Server time: %s

`, now.Format("2006-01-02 15:04:05")) } func allowGetHead(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet, http.MethodHead: next.ServeHTTP(w, r) default: http.NotFound(w, r) } }) }