Strip .md suffix from filename when creating files to prevent filename.md.md from being created. This aligns with the behavior in FileHandler which also normalizes filenames from URLs. Fixes issue where files created with .md extension could not be deleted due to filename mismatch.
180 lines
4.9 KiB
Go
180 lines
4.9 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
|
|
}
|
|
|
|
// Normalize filename by stripping .md extension if present
|
|
name := strings.TrimSuffix(req.Name, ".md")
|
|
|
|
if err := h.storage.Save(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: 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})
|
|
}
|