package httpserver import ( "encoding/json" "net/http" "os" "github.com/go-chi/chi/v5" "github.com/tim/cairnquire/apps/server/internal/sync" ) func (s *Server) handleSyncInit(w http.ResponseWriter, r *http.Request) { var req sync.InitRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) return } if req.DeviceID == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "deviceId is required"}) return } principal, ok := requirePrincipal(r) if !ok { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"}) return } snapshot, err := s.syncService.InitSync(r.Context(), req.DeviceID, principal.UserID) if err != nil { s.logger.Error("sync init failed", "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to initialize sync"}) return } writeJSON(w, http.StatusOK, sync.InitResponse{ SnapshotID: snapshot.ID, ServerSnapshot: snapshot.Files, }) } func (s *Server) handleSyncDelta(w http.ResponseWriter, r *http.Request) { var req sync.DeltaRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) return } if req.SnapshotID == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "snapshotId is required"}) return } result, err := s.syncService.ApplyDelta(r.Context(), req.SnapshotID, sync.Delta{Changes: req.ClientDelta}) if err != nil { s.logger.Error("sync delta failed", "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to apply delta"}) return } writeJSON(w, http.StatusOK, sync.DeltaResponse{ ServerDelta: result.ServerDelta, Conflicts: result.Conflicts, }) } func (s *Server) handleSyncResolve(w http.ResponseWriter, r *http.Request) { var req sync.ResolveRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) return } if req.SnapshotID == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "snapshotId is required"}) return } snapshot, err := s.syncService.ResolveConflicts(r.Context(), req.SnapshotID, req.Resolutions) if err != nil { s.logger.Error("sync resolve failed", "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to resolve conflicts"}) return } writeJSON(w, http.StatusOK, sync.ResolveResponse{ NewSnapshotID: snapshot.ID, }) } func (s *Server) handleContentFetch(w http.ResponseWriter, r *http.Request) { hash := chi.URLParam(r, "hash") if hash == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "hash is required"}) return } content, err := s.syncService.GetContent(hash) if err != nil { if os.IsNotExist(err) { writeJSON(w, http.StatusNotFound, map[string]string{"error": "content not found"}) return } s.logger.Error("content fetch failed", "error", err, "hash", hash) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to fetch content"}) return } w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") w.Header().Set("ETag", "\""+hash+"\"") w.WriteHeader(http.StatusOK) w.Write(content) }