package api import ( "database/sql" "encoding/json" "net/http" "regexp" "strconv" "github.com/go-chi/chi/v5" "github.com/syncserver/internal/models" ) type SyncPairHandler struct { db *sql.DB } func NewSyncPairHandler(db *sql.DB) *SyncPairHandler { return &SyncPairHandler{db: db} } var directionRegex = regexp.MustCompile(`^(push|pull|mirror)$`) func (h *SyncPairHandler) List(w http.ResponseWriter, r *http.Request) { repo := models.NewSyncPairRepository(h.db) pairs, err := repo.GetAll() if err != nil { writeError(w, http.StatusInternalServerError, "failed to fetch sync pairs") return } out := make([]SyncPairResponse, len(pairs)) for i, p := range pairs { out[i] = syncPairToResp(p) } writeJSON(w, out) } func (h *SyncPairHandler) Get(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } repo := models.NewSyncPairRepository(h.db) p, err := repo.GetByID(id) if err == sql.ErrNoRows { writeError(w, http.StatusNotFound, "sync pair not found") return } if err != nil { writeError(w, http.StatusInternalServerError, "failed to fetch sync pair") return } writeJSON(w, syncPairToResp(*p)) } func (h *SyncPairHandler) Create(w http.ResponseWriter, r *http.Request) { var req SyncPairRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.Name == "" || req.SourcePath == "" || req.DestPath == "" { writeError(w, http.StatusBadRequest, "name, source_path and dest_path are required") return } if req.Direction == "" { req.Direction = "push" } if !directionRegex.MatchString(req.Direction) { writeError(w, http.StatusBadRequest, "direction must be push, pull, or mirror") return } if req.RsyncFlags == "" { req.RsyncFlags = "-aP" } if !req.Enabled { req.Enabled = true } sp := &models.SyncPair{ Name: req.Name, SourceMachineID: req.SourceMachineID, SourcePath: req.SourcePath, DestMachineID: req.DestMachineID, DestPath: req.DestPath, Direction: req.Direction, RsyncFlags: req.RsyncFlags, ExcludePatterns: req.ExcludePatterns, Enabled: req.Enabled, } repo := models.NewSyncPairRepository(h.db) id, err := repo.Create(sp) if err != nil { writeError(w, http.StatusInternalServerError, "failed to create sync pair") return } sp.ID = id w.Header().Set("Location", "/api/sync-pairs/"+strconv.FormatInt(id, 10)) writeJSON(w, syncPairToResp(*sp), http.StatusCreated) } func (h *SyncPairHandler) Update(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } var req SyncPairRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.Name == "" || req.SourcePath == "" || req.DestPath == "" { writeError(w, http.StatusBadRequest, "name, source_path and dest_path are required") return } if !directionRegex.MatchString(req.Direction) { writeError(w, http.StatusBadRequest, "direction must be push, pull, or mirror") return } repo := models.NewSyncPairRepository(h.db) existing, err := repo.GetByID(id) if err == sql.ErrNoRows { writeError(w, http.StatusNotFound, "sync pair not found") return } if err != nil { writeError(w, http.StatusInternalServerError, "failed to fetch sync pair") return } existing.Name = req.Name existing.SourceMachineID = req.SourceMachineID existing.SourcePath = req.SourcePath existing.DestMachineID = req.DestMachineID existing.DestPath = req.DestPath existing.Direction = req.Direction existing.RsyncFlags = req.RsyncFlags existing.ExcludePatterns = req.ExcludePatterns existing.Enabled = req.Enabled if err := repo.Update(existing); err != nil { writeError(w, http.StatusInternalServerError, "failed to update sync pair") return } writeJSON(w, syncPairToResp(*existing)) } func (h *SyncPairHandler) Delete(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } repo := models.NewSyncPairRepository(h.db) if err := repo.Delete(id); err != nil { writeError(w, http.StatusInternalServerError, "failed to delete sync pair") return } w.WriteHeader(http.StatusNoContent) } func syncPairToResp(p models.SyncPair) SyncPairResponse { return SyncPairResponse{ ID: p.ID, Name: p.Name, SourceMachineID: p.SourceMachineID, SourcePath: p.SourcePath, DestMachineID: p.DestMachineID, DestPath: p.DestPath, Direction: p.Direction, RsyncFlags: p.RsyncFlags, ExcludePatterns: p.ExcludePatterns, Enabled: p.Enabled, } }