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

Route %s not found.

` ) // Server hosts the Luxtools HTTP handlers. type Server struct { mux *http.ServeMux indexPage []byte staticRoot string assetsRoot string counterMu sync.Mutex clickCount int } // New constructs a configured Server ready to serve requests. func New() (*Server, error) { projectRoot, err := goProjectRoot() if err != nil { return nil, err } indexPath := filepath.Join(projectRoot, "web", "templates", "index.html") staticDir := filepath.Join(projectRoot, "web", "static") assetsDir := filepath.Join(projectRoot, "assets") indexPage, err := os.ReadFile(indexPath) if err != nil { return nil, fmt.Errorf("failed to read index template: %w", err) } if _, err := os.Stat(staticDir); err != nil { return nil, fmt.Errorf("static directory check failed: %w", err) } if _, err := os.Stat(assetsDir); err != nil { return nil, fmt.Errorf("assets directory check failed: %w", err) } s := &Server{ mux: http.NewServeMux(), indexPage: indexPage, staticRoot: staticDir, assetsRoot: assetsDir, } 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() { s.mux.Handle("/static/", http.StripPrefix("/static/", allowGetHead(http.FileServer(http.Dir(s.staticRoot))))) s.mux.Handle("/assets/", http.StripPrefix("/assets/", allowGetHead(http.FileServer(http.Dir(s.assetsRoot))))) 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) } }) } func goProjectRoot() (string, error) { _, currentFile, _, ok := runtime.Caller(0) if !ok { return "", errors.New("unable to determine caller information") } dir := filepath.Dir(currentFile) projectRoot := filepath.Clean(filepath.Join(dir, "..", "..")) return projectRoot, nil }