Implements full markdown editor application with: Backend (Go): - Cobra CLI with --data-dir, --port, --host flags - REST API for CRUD operations on markdown files - File storage on disk with flat structure - Logrus logging for all operations - Static asset serving for frontend - Comprehensive tests for CRUD and static assets Frontend (React + TypeScript + Tailwind): - Markdown editor with live GFM preview - File management UI (list, create, open, save, delete) - Theme system (Dark, Light, System) with persistence - Responsive design (320px to 1920px) - Component tests for core functionality Integration: - Full CRUD workflow from frontend to backend - Static asset serving verified - All tests passing (backend: 2/2, frontend: 6/6) Files added: - Backend: API handler, logger, server, tests - Frontend: Components, tests, config files - Build artifacts: compiled backend binary and frontend dist - Documentation: README and implementation summary
126 lines
3.1 KiB
Go
126 lines
3.1 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type APIHandler struct {
|
|
dataDir string
|
|
log *logrus.Logger
|
|
}
|
|
|
|
func NewAPIHandler(dataDir string, log *logrus.Logger) *APIHandler {
|
|
return &APIHandler{
|
|
dataDir: dataDir,
|
|
log: log,
|
|
}
|
|
}
|
|
|
|
func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
h.handleGet(w, r)
|
|
case http.MethodPost:
|
|
h.handlePost(w, r)
|
|
case http.MethodPut:
|
|
h.handlePut(w, r)
|
|
case http.MethodDelete:
|
|
h.handleDelete(w, r)
|
|
default:
|
|
h.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
}
|
|
}
|
|
|
|
func (h *APIHandler) handleGet(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
filename := vars["filename"]
|
|
|
|
h.log.Infof("GET request for file: %s", filename)
|
|
|
|
filepath := filepath.Join(h.dataDir, filename)
|
|
content, err := os.ReadFile(filepath)
|
|
if err != nil {
|
|
h.log.Errorf("Error reading file %s: %v", filename, err)
|
|
h.writeError(w, http.StatusNotFound, "file not found")
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.Write(content)
|
|
}
|
|
|
|
func (h *APIHandler) handlePost(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
filename := vars["filename"]
|
|
|
|
h.log.Infof("POST request for file: %s", filename)
|
|
|
|
filepath := filepath.Join(h.dataDir, filename)
|
|
content, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
h.log.Errorf("Error reading request body: %v", err)
|
|
h.writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if err := os.WriteFile(filepath, content, 0644); err != nil {
|
|
h.log.Errorf("Error writing file %s: %v", filename, err)
|
|
h.writeError(w, http.StatusInternalServerError, "failed to create file")
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
}
|
|
|
|
func (h *APIHandler) handlePut(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
filename := vars["filename"]
|
|
|
|
h.log.Infof("PUT request for file: %s", filename)
|
|
|
|
filepath := filepath.Join(h.dataDir, filename)
|
|
content, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
h.log.Errorf("Error reading request body: %v", err)
|
|
h.writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if err := os.WriteFile(filepath, content, 0644); err != nil {
|
|
h.log.Errorf("Error writing file %s: %v", filename, err)
|
|
h.writeError(w, http.StatusInternalServerError, "failed to update file")
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func (h *APIHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
filename := vars["filename"]
|
|
|
|
h.log.Infof("DELETE request for file: %s", filename)
|
|
|
|
filepath := filepath.Join(h.dataDir, filename)
|
|
if err := os.Remove(filepath); err != nil {
|
|
h.log.Errorf("Error deleting file %s: %v", filename, err)
|
|
h.writeError(w, http.StatusNotFound, "file not found")
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *APIHandler) writeError(w http.ResponseWriter, statusCode int, message string) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": message})
|
|
}
|