package api import ( "encoding/json" "fmt" "net/http" "path/filepath" "strings" "github.com/markdown-editor/internal/storage" "github.com/markdown-editor/pkg/logger" ) type ErrorResponse struct { Error string `json:"error"` } type Server struct { storage *storage.Storage host string port int } func NewServer(dataDir, host string, port int) (*Server, error) { return &Server{ storage: storage.NewStorage(dataDir), host: host, port: port, }, nil } func (s *Server) Start() error { mux := http.NewServeMux() // API routes mux.HandleFunc("/api/files", s.handleFiles) mux.HandleFunc("/api/files/", s.handleFiles) // Static files mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) // Frontend SPA fallback mux.HandleFunc("/", s.handleFrontend) addr := fmt.Sprintf("%s:%d", s.host, s.port) logger.Infof("Starting server on %s", addr) return http.ListenAndServe(addr, mux) } func (s *Server) handleFiles(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: s.handleGetFiles(w, r) case http.MethodPost: s.handleCreateFile(w, r) case http.MethodPut: s.handleUpdateFile(w, r) case http.MethodDelete: s.handleDeleteFile(w, r) default: s.sendError(w, http.StatusMethodNotAllowed, "method not allowed") } } func (s *Server) handleGetFiles(w http.ResponseWriter, r *http.Request) { // Check if URL includes a filename (e.g., /api/files/test.md) filename := filepath.Base(r.URL.Path) if filename != "" && r.URL.Path != "/api/files" { // Get specific file content, err := s.storage.GetFile(filename) if err != nil { s.sendError(w, http.StatusNotFound, err.Error()) return } w.Header().Set("Content-Type", "text/markdown") w.Write([]byte(content)) return } // List all files files, err := s.storage.ListFiles() if err != nil { s.sendError(w, http.StatusInternalServerError, err.Error()) return } w.Header().Set("Content-Type", "application/json") w.Write([]byte{'['}) for i, file := range files { if i > 0 { w.Write([]byte{','}) } w.Write([]byte{'"'}) w.Write([]byte(file)) w.Write([]byte{'"'}) } w.Write([]byte{']'}) } func (s *Server) handleCreateFile(w http.ResponseWriter, r *http.Request) { var req struct { Content string `json:"content"` Name string `json:"name"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.sendError(w, http.StatusBadRequest, "invalid request body") return } if req.Name == "" { s.sendError(w, http.StatusBadRequest, "filename is required") return } if !strings.HasSuffix(req.Name, ".md") { s.sendError(w, http.StatusBadRequest, "filename must end with .md") return } if err := s.storage.SaveFile(req.Name, req.Content); err != nil { s.sendError(w, http.StatusInternalServerError, err.Error()) return } logger.Infof("Created file: %s", req.Name) w.WriteHeader(http.StatusCreated) } func (s *Server) handleUpdateFile(w http.ResponseWriter, r *http.Request) { filename := filepath.Base(r.URL.Path) if filename == "" { s.sendError(w, http.StatusBadRequest, "filename is required") return } if !strings.HasSuffix(filename, ".md") { s.sendError(w, http.StatusBadRequest, "filename must end with .md") return } var req struct { Content string `json:"content"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.sendError(w, http.StatusBadRequest, "invalid request body") return } if err := s.storage.SaveFile(filename, req.Content); err != nil { s.sendError(w, http.StatusInternalServerError, err.Error()) return } logger.Infof("Updated file: %s", filename) } func (s *Server) handleDeleteFile(w http.ResponseWriter, r *http.Request) { filename := filepath.Base(r.URL.Path) if filename == "" { s.sendError(w, http.StatusBadRequest, "filename is required") return } if !strings.HasSuffix(filename, ".md") { s.sendError(w, http.StatusBadRequest, "filename must end with .md") return } if err := s.storage.DeleteFile(filename); err != nil { s.sendError(w, http.StatusInternalServerError, err.Error()) return } logger.Infof("Deleted file: %s", filename) } func (s *Server) handleFrontend(w http.ResponseWriter, r *http.Request) { // Serve the index.html for SPA http.ServeFile(w, r, "./static/index.html") } func (s *Server) sendError(w http.ResponseWriter, statusCode int, message string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) json.NewEncoder(w).Encode(ErrorResponse{Error: message}) } func (s *Server) ServeStaticFiles(w http.ResponseWriter, r *http.Request) { http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))).ServeHTTP(w, r) }