d7e6d64967
Option A (validation): - handlers_syncpairs.go: reject create/update when both SourceMachineID and DestMachineID are set; clear error message explains the constraint - engine.go: detect 'both remote' rsync error at runtime and surface it as error_code=remote_to_remote_unsupported with a human-readable message Option B (remote-to-remote support): - rsync_runner.go: add RunRemote() method that SSHs to the source machine and runs rsync locally there (src=local path, dst=user@host:/path), streaming output back through the onLine callback - engine.go: when both srcMachine and dstMachine are non-nil, use RunRemote() instead of Run(), SSHing to srcMachine and running rsync from there. Also wake dstMachine via WoL when both sides are remote.
199 lines
5.7 KiB
Go
199 lines
5.7 KiB
Go
package api
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"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 {
|
|
slog.Error("failed to fetch sync pairs", "error", err)
|
|
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 {
|
|
slog.Error("failed to fetch sync pair", "id", id, "error", err)
|
|
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.SourceMachineID != nil && req.DestMachineID != nil {
|
|
writeError(w, http.StatusBadRequest, "both source and destination cannot be remote machines; one side must be the server (set one MachineID to null)")
|
|
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 {
|
|
slog.Error("failed to create sync pair", "name", req.Name, "error", err)
|
|
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
|
|
}
|
|
if req.SourceMachineID != nil && req.DestMachineID != nil {
|
|
writeError(w, http.StatusBadRequest, "both source and destination cannot be remote machines; one side must be the server (set one MachineID to null)")
|
|
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 {
|
|
slog.Error("failed to fetch sync pair", "id", id, "error", err)
|
|
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 {
|
|
slog.Error("failed to update sync pair", "id", id, "error", err)
|
|
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 {
|
|
slog.Error("failed to delete sync pair", "id", id, "error", err)
|
|
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,
|
|
}
|
|
}
|