- Fix API to return JSON response for file content instead of plain text - Fix file display showing [object Object] by properly extracting content field - Fix infinite save loop by tracking last saved content - Remove auto-save that was causing cursor jumping on every keystroke - Add manual save button with disabled state when content unchanged - Add validation in FileList to prevent undefined filenames - Improve error handling for file operations
223 lines
5.4 KiB
Go
223 lines
5.4 KiB
Go
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"`
|
|
}
|
|
|
|
// CORS middleware for allowing cross-origin requests
|
|
func corsMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Allow requests from any origin during development
|
|
// In production, you would specify allowed origins
|
|
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, Authorization")
|
|
|
|
// Handle preflight requests
|
|
if r.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
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)
|
|
|
|
// Wrap with CORS middleware
|
|
handler := corsMiddleware(mux)
|
|
|
|
addr := fmt.Sprintf("%s:%d", s.host, s.port)
|
|
logger.Infof("Starting server on %s", addr)
|
|
|
|
return http.ListenAndServe(addr, handler)
|
|
}
|
|
|
|
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
|
|
}
|
|
// Return file content as JSON
|
|
response := map[string]string{
|
|
"name": filename,
|
|
"content": content,
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(response)
|
|
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)
|
|
}
|