package api import ( "encoding/json" "fmt" "net/http" "os" "path/filepath" "strings" "eval/internal/config" "eval/internal/storage" "github.com/sirupsen/logrus" ) // Handlers holds the API handlers type Handlers struct { store *storage.Storage config *config.Config logger *logrus.Logger } // NewHandlers creates a new Handlers instance func NewHandlers(store *storage.Storage, config *config.Config, logger *logrus.Logger) *Handlers { return &Handlers{ store: store, config: config, logger: logger, } } // ErrorResponse represents an error response type ErrorResponse struct { Error string `json:"error"` } // writeJSON writes a JSON response func (h *Handlers) writeJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(data); err != nil { h.logger.Errorf("failed to encode response: %v", err) http.Error(w, `{"error": "internal server error"}`, http.StatusInternalServerError) } } // writeError writes an error response func (h *Handlers) writeError(w http.ResponseWriter, status int, message string) { h.writeJSON(w, status, ErrorResponse{Error: message}) } // listFilesHandler handles GET /api/files - list all markdown files func (h *Handlers) listFilesHandler(w http.ResponseWriter, r *http.Request) { h.logger.Info("listing files") files, err := h.store.ListFiles() if err != nil { h.logger.Errorf("failed to list files: %v", err) h.writeError(w, http.StatusInternalServerError, "failed to list files") return } h.writeJSON(w, http.StatusOK, files) } // getFileHandler handles GET /api/files/{filename} - get a specific file func (h *Handlers) getFileHandler(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/files/") filename := path if filename == "" { h.writeError(w, http.StatusBadRequest, "filename is required") return } h.logger.WithField("filename", filename).Info("getting file") file, err := h.store.GetFile(filename) if err != nil { if err.Error() == "file not found" { h.writeError(w, http.StatusNotFound, "file not found") return } h.logger.Errorf("failed to get file: %v", err) h.writeError(w, http.StatusInternalServerError, "failed to get file") return } h.writeJSON(w, http.StatusOK, file) } // createFileHandler handles POST /api/files - create a new file func (h *Handlers) createFileHandler(w http.ResponseWriter, r *http.Request) { var file storage.FileContent if err := json.NewDecoder(r.Body).Decode(&file); err != nil { h.writeError(w, http.StatusBadRequest, "invalid JSON") return } if file.Filename == "" { h.writeError(w, http.StatusBadRequest, "filename is required") return } h.logger.WithField("filename", file.Filename).Info("creating file") result, err := h.store.CreateFile(file.Filename, file.Content) if err != nil { if err.Error() == "file already exists" { h.writeError(w, http.StatusConflict, "file already exists") return } h.logger.Errorf("failed to create file: %v", err) h.writeError(w, http.StatusInternalServerError, "failed to create file") return } h.writeJSON(w, http.StatusCreated, result) } // updateFileHandler handles PUT /api/files/{filename} - update a file func (h *Handlers) updateFileHandler(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/files/") filename := path if filename == "" { h.writeError(w, http.StatusBadRequest, "filename is required") return } var file storage.FileContent if err := json.NewDecoder(r.Body).Decode(&file); err != nil { h.writeError(w, http.StatusBadRequest, "invalid JSON") return } h.logger.WithField("filename", filename).Info("updating file") result, err := h.store.UpdateFile(filename, file.Content) if err != nil { if err.Error() == "file not found" { h.writeError(w, http.StatusNotFound, "file not found") return } h.logger.Errorf("failed to update file: %v", err) h.writeError(w, http.StatusInternalServerError, "failed to update file") return } h.writeJSON(w, http.StatusOK, result) } // deleteFileHandler handles DELETE /api/files/{filename} - delete a file func (h *Handlers) deleteFileHandler(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/files/") filename := path if filename == "" { h.writeError(w, http.StatusBadRequest, "filename is required") return } h.logger.WithField("filename", filename).Info("deleting file") if err := h.store.DeleteFile(filename); err != nil { if err.Error() == "file not found" { h.writeError(w, http.StatusNotFound, "file not found") return } h.logger.Errorf("failed to delete file: %v", err) h.writeError(w, http.StatusInternalServerError, "failed to delete file") return } h.writeJSON(w, http.StatusOK, map[string]string{"message": "file deleted"}) } // registerAPIRoutes registers all API routes func (h *Handlers) registerAPIRoutes(router *http.ServeMux) { router.HandleFunc("GET /api/files", h.listFilesHandler) router.HandleFunc("GET /api/files/", h.getFileHandler) router.HandleFunc("POST /api/files", h.createFileHandler) router.HandleFunc("PUT /api/files/", h.updateFileHandler) router.HandleFunc("DELETE /api/files/", h.deleteFileHandler) } // ServeStaticHandler serves static files type ServeStaticHandler struct { fs http.FileSystem } // NewServeStaticHandler creates a new static file handler func NewServeStaticHandler(dir string) (*ServeStaticHandler, error) { fs := http.Dir(dir) _, err := fs.Open(".") if err != nil { return nil, fmt.Errorf("static directory not found: %w", err) } return &ServeStaticHandler{fs: fs}, nil } // ServeHTTP serves files from the static directory func (h *ServeStaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { file, err := h.fs.Open(r.URL.Path) if err != nil { // If file not found, serve index.html for SPA routing file, err = h.fs.Open("/index.html") if err != nil { http.NotFound(w, r) return } } defer file.Close() // Get file info to determine content type info, err := file.Stat() if err != nil { http.NotFound(w, r) return } if info.IsDir() { http.NotFound(w, r) return } http.ServeContent(w, r, info.Name(), info.ModTime(), file) } // SetupStaticHandler sets up the static file handler func (h *Handlers) SetupStaticHandler(buildDir string) (http.HandlerFunc, error) { handler, err := NewServeStaticHandler(buildDir) if err != nil { return nil, err } return handler.ServeHTTP, nil } // SetupRoutes sets up all routes and returns the router func (h *Handlers) SetupRoutes(buildDir string) (*http.ServeMux, error) { router := http.NewServeMux() h.registerAPIRoutes(router) // Setup static handler if _, err := os.Stat(buildDir); err == nil { router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" || r.URL.Path == "/index.html" { http.ServeFile(w, r, filepath.Join(buildDir, "index.html")) return } // Try to serve the file, if not found, serve index.html file, err := http.Dir(buildDir).Open(r.URL.Path) if err != nil { http.ServeFile(w, r, filepath.Join(buildDir, "index.html")) return } file.Close() http.ServeFile(w, r, filepath.Join(buildDir, r.URL.Path)) }) } else { h.logger.Warnf("build directory not found: %s, static file serving disabled", buildDir) } return router, nil } // StartServer starts the HTTP server func (h *Handlers) StartServer(router *http.ServeMux) error { addr := fmt.Sprintf("%s:%d", h.config.Host, h.config.Port) h.logger.Infof("starting server on %s", addr) return http.ListenAndServe(addr, router) }