Add full-stack markdown editor with Go backend and React frontend. Backend: - Cobra CLI with --data-dir, --port, --host flags - REST API for markdown file CRUD operations - File storage with flat directory structure - logrus logging for all operations - Static file serving for frontend - Comprehensive tests for CRUD and static assets Frontend: - React + TypeScript + Vite + Tailwind CSS - Live markdown preview with marked (GFM) - File management: list, create, open, save, delete - Theme system: Dark/Light/System with persistence - Responsive design (320px to 1920px) - Component tests for Editor, Preview, Sidebar Build: - Makefile for build, test, and run automation - Single command testing (make test) Closes SPEC.md requirements
177 lines
4.8 KiB
Go
177 lines
4.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"markdown-editor-backend/storage"
|
|
)
|
|
|
|
// Handler contains HTTP handlers for the API
|
|
type Handler struct {
|
|
storage *storage.Storage
|
|
log *logrus.Logger
|
|
}
|
|
|
|
// New creates a new Handler instance
|
|
func New(storage *storage.Storage, log *logrus.Logger) *Handler {
|
|
return &Handler{
|
|
storage: storage,
|
|
log: log,
|
|
}
|
|
}
|
|
|
|
// FileRequest represents a request body for file operations
|
|
type FileRequest struct {
|
|
Name string `json:"name"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
// FileResponse represents a file response
|
|
type FileResponse struct {
|
|
Name string `json:"name"`
|
|
Content string `json:"content,omitempty"`
|
|
}
|
|
|
|
// ErrorResponse represents an error response
|
|
type ErrorResponse struct {
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
// FilesHandler handles list and create operations
|
|
func (h *Handler) FilesHandler(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
h.listFiles(w, r)
|
|
case http.MethodPost:
|
|
h.createFile(w, r)
|
|
default:
|
|
h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
}
|
|
}
|
|
|
|
// FileHandler handles get, update, and delete operations for a specific file
|
|
func (h *Handler) FileHandler(w http.ResponseWriter, r *http.Request) {
|
|
// Extract filename from URL path
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/files/")
|
|
if path == "" {
|
|
h.sendError(w, http.StatusBadRequest, "filename required")
|
|
return
|
|
}
|
|
|
|
name := strings.TrimSuffix(path, ".md")
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
h.getFile(w, r, name)
|
|
case http.MethodPut:
|
|
h.updateFile(w, r, name)
|
|
case http.MethodDelete:
|
|
h.deleteFile(w, r, name)
|
|
default:
|
|
h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
}
|
|
}
|
|
|
|
func (h *Handler) listFiles(w http.ResponseWriter, r *http.Request) {
|
|
h.log.Debug("Handling list files request")
|
|
|
|
files, err := h.storage.List()
|
|
if err != nil {
|
|
h.log.WithError(err).Error("Failed to list files")
|
|
h.sendError(w, http.StatusInternalServerError, "failed to list files")
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(files)
|
|
}
|
|
|
|
func (h *Handler) createFile(w http.ResponseWriter, r *http.Request) {
|
|
h.log.Debug("Handling create file request")
|
|
|
|
var req FileRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
h.log.WithError(err).Warn("Invalid request body")
|
|
h.sendError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if req.Name == "" {
|
|
h.sendError(w, http.StatusBadRequest, "name is required")
|
|
return
|
|
}
|
|
|
|
if err := h.storage.Save(req.Name, req.Content); err != nil {
|
|
h.log.WithError(err).Error("Failed to create file")
|
|
h.sendError(w, http.StatusInternalServerError, "failed to create file")
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(FileResponse{Name: req.Name, Content: req.Content})
|
|
}
|
|
|
|
func (h *Handler) getFile(w http.ResponseWriter, r *http.Request, name string) {
|
|
h.log.WithField("name", name).Debug("Handling get file request")
|
|
|
|
content, err := h.storage.Get(name)
|
|
if err != nil {
|
|
if err.Error() == "file not found" {
|
|
h.sendError(w, http.StatusNotFound, "file not found")
|
|
return
|
|
}
|
|
h.log.WithError(err).Error("Failed to get file")
|
|
h.sendError(w, http.StatusInternalServerError, "failed to get file")
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(FileResponse{Name: name, Content: content})
|
|
}
|
|
|
|
func (h *Handler) updateFile(w http.ResponseWriter, r *http.Request, name string) {
|
|
h.log.WithField("name", name).Debug("Handling update file request")
|
|
|
|
var req FileRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
h.log.WithError(err).Warn("Invalid request body")
|
|
h.sendError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if err := h.storage.Save(name, req.Content); err != nil {
|
|
h.log.WithError(err).Error("Failed to update file")
|
|
h.sendError(w, http.StatusInternalServerError, "failed to update file")
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(FileResponse{Name: name, Content: req.Content})
|
|
}
|
|
|
|
func (h *Handler) deleteFile(w http.ResponseWriter, r *http.Request, name string) {
|
|
h.log.WithField("name", name).Debug("Handling delete file request")
|
|
|
|
if err := h.storage.Delete(name); err != nil {
|
|
if err.Error() == "file not found" {
|
|
h.sendError(w, http.StatusNotFound, "file not found")
|
|
return
|
|
}
|
|
h.log.WithError(err).Error("Failed to delete file")
|
|
h.sendError(w, http.StatusInternalServerError, "failed to delete file")
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) sendError(w http.ResponseWriter, status int, message string) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(ErrorResponse{Error: message})
|
|
}
|