package server import ( "encoding/json" "fmt" "net" "net/http" "github.com/sirupsen/logrus" "markdown-editor-backend/handlers" "markdown-editor-backend/storage" ) // Server represents the HTTP server type Server struct { dataDir string host string port int log *logrus.Logger storage *storage.Storage handler *handlers.Handler staticPath string listener net.Listener } // New creates a new Server instance func New(dataDir, host string, port int, log *logrus.Logger) *Server { store := storage.New(dataDir, log) handler := handlers.New(store, log) return &Server{ dataDir: dataDir, host: host, port: port, log: log, storage: store, handler: handler, staticPath: "./static", } } // SetStaticPath sets the path for static files (used for embedding frontend) func (s *Server) SetStaticPath(path string) { s.staticPath = path } // Start initializes storage and starts the HTTP server func (s *Server) Start() error { if err := s.storage.Init(); err != nil { return err } mux := http.NewServeMux() // API routes mux.HandleFunc("/api/files", s.handler.FilesHandler) mux.HandleFunc("/api/files/", s.handler.FileHandler) // Health check mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) }) // Static file serving - serve frontend build fs := http.FileServer(http.Dir(s.staticPath)) mux.Handle("/", fs) // CORS middleware handler := s.corsMiddleware(mux) addr := fmt.Sprintf("%s:%d", s.host, s.port) s.log.WithField("address", addr).Info("Server starting") listener, err := net.Listen("tcp", addr) if err != nil { return err } s.listener = listener return http.Serve(listener, handler) } // StartAsync starts the server asynchronously and returns the actual address func (s *Server) StartAsync() (string, error) { if err := s.storage.Init(); err != nil { return "", err } mux := http.NewServeMux() // API routes mux.HandleFunc("/api/files", s.handler.FilesHandler) mux.HandleFunc("/api/files/", s.handler.FileHandler) // Health check mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) }) // Static file serving - serve frontend build fs := http.FileServer(http.Dir(s.staticPath)) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if len(r.URL.Path) >= 4 && r.URL.Path[:4] == "/api" { return } fs.ServeHTTP(w, r) }) // CORS middleware handler := s.corsMiddleware(mux) addr := fmt.Sprintf("%s:%d", s.host, s.port) if s.port == 0 { addr = fmt.Sprintf("%s:0", s.host) } listener, err := net.Listen("tcp", addr) if err != nil { return "", err } s.listener = listener actualAddr := listener.Addr().String() s.log.WithField("address", actualAddr).Info("Server starting") go http.Serve(listener, handler) return actualAddr, nil } // Stop stops the server func (s *Server) Stop() error { if s.listener != nil { return s.listener.Close() } return nil } // corsMiddleware adds CORS headers for frontend development func (s *Server) corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) } // GetStorage returns the storage instance (for testing) func (s *Server) GetStorage() *storage.Storage { return s.storage } // GetStaticPath returns the static files path (for testing) func (s *Server) GetStaticPath() string { return s.staticPath } // GetAddr returns the server address (for testing) func (s *Server) GetAddr() string { if s.listener != nil { return s.listener.Addr().String() } return fmt.Sprintf("%s:%d", s.host, s.port) }