package api import ( "encoding/json" "errors" "net/http" "strconv" "time" "github.com/go-chi/chi/v5" "github.com/syonad/clonepack/internal/core" "github.com/syonad/clonepack/internal/store" ) type SnapshotHandler struct { svc *core.SnapshotService } func NewSnapshotHandler(svc *core.SnapshotService) *SnapshotHandler { return &SnapshotHandler{svc: svc} } func (h *SnapshotHandler) Create(w http.ResponseWriter, r *http.Request) { repoID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { Error(w, http.StatusBadRequest, "invalid id") return } if err := Authorize(r.Context(), ActionSnapshotCreate, repoID); err != nil { Error(w, http.StatusForbidden, "forbidden") return } var req CreateSnapshotRequest json.NewDecoder(r.Body).Decode(&req) if req.Label == "" { req.Label = "manual-" + time.Now().UTC().Format(time.RFC3339) } id, err := h.svc.TakeSnapshot(r.Context(), repoID, req.Label) if errors.Is(err, store.ErrNotFound) { Error(w, http.StatusNotFound, "repo not found") return } else if err != nil { Error(w, http.StatusInternalServerError, err.Error()) return } snap, _, _ := h.svc.Get(r.Context(), repoID, id) JSON(w, http.StatusCreated, snapshotToResponse(*snap)) } func (h *SnapshotHandler) List(w http.ResponseWriter, r *http.Request) { repoID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { Error(w, http.StatusBadRequest, "invalid id") return } if err := Authorize(r.Context(), ActionSnapshotRead, repoID); err != nil { Error(w, http.StatusForbidden, "forbidden") return } snaps, err := h.svc.List(r.Context(), repoID) if errors.Is(err, store.ErrNotFound) { Error(w, http.StatusNotFound, "repo not found") return } else if err != nil { Error(w, http.StatusInternalServerError, err.Error()) return } items := make([]SnapshotResponse, len(snaps)) for i, s := range snaps { items[i] = snapshotToResponse(s) } JSON(w, http.StatusOK, ListSnapshotsResponse{Items: items, Total: len(items)}) } func (h *SnapshotHandler) Get(w http.ResponseWriter, r *http.Request) { repoID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { Error(w, http.StatusBadRequest, "invalid id") return } snapID, err := strconv.ParseInt(chi.URLParam(r, "snap_id"), 10, 64) if err != nil { Error(w, http.StatusBadRequest, "invalid snap_id") return } if err := Authorize(r.Context(), ActionSnapshotRead, repoID); err != nil { Error(w, http.StatusForbidden, "forbidden") return } snap, pkgs, err := h.svc.Get(r.Context(), repoID, snapID) if errors.Is(err, store.ErrNotFound) { Error(w, http.StatusNotFound, "snapshot not found") return } else if err != nil { Error(w, http.StatusInternalServerError, err.Error()) return } resp := SnapshotDetailResponse{ SnapshotResponse: snapshotToResponse(*snap), Packages: make([]SnapshotPackageResponse, len(pkgs)), } for i, p := range pkgs { resp.Packages[i] = snapshotPkgToResponse(p) } JSON(w, http.StatusOK, resp) } func (h *SnapshotHandler) Delete(w http.ResponseWriter, r *http.Request) { repoID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { Error(w, http.StatusBadRequest, "invalid id") return } snapID, err := strconv.ParseInt(chi.URLParam(r, "snap_id"), 10, 64) if err != nil { Error(w, http.StatusBadRequest, "invalid snap_id") return } if err := Authorize(r.Context(), ActionSnapshotDelete, repoID); err != nil { Error(w, http.StatusForbidden, "forbidden") return } if err := h.svc.Delete(r.Context(), repoID, snapID); errors.Is(err, store.ErrNotFound) { Error(w, http.StatusNotFound, "snapshot not found") return } else if err != nil { Error(w, http.StatusInternalServerError, err.Error()) return } w.WriteHeader(http.StatusNoContent) } func (h *SnapshotHandler) Diff(w http.ResponseWriter, r *http.Request) { repoID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { Error(w, http.StatusBadRequest, "invalid id") return } if err := Authorize(r.Context(), ActionSnapshotRead, repoID); err != nil { Error(w, http.StatusForbidden, "forbidden") return } fromID, err := strconv.ParseInt(r.URL.Query().Get("from"), 10, 64) if err != nil { Error(w, http.StatusBadRequest, "invalid from param") return } toID, err := strconv.ParseInt(r.URL.Query().Get("to"), 10, 64) if err != nil { Error(w, http.StatusBadRequest, "invalid to param") return } diff, err := h.svc.Diff(r.Context(), repoID, fromID, toID) if errors.Is(err, store.ErrNotFound) { Error(w, http.StatusNotFound, "snapshot not found") return } else if err != nil { Error(w, http.StatusBadRequest, err.Error()) return } resp := SnapshotDiffResponse{ From: snapshotToResponse(*diff.From), To: snapshotToResponse(*diff.To), Unchanged: diff.Unchanged, } for _, p := range diff.Added { resp.Added = append(resp.Added, snapshotPkgToResponse(p)) } for _, p := range diff.Removed { resp.Removed = append(resp.Removed, snapshotPkgToResponse(p)) } JSON(w, http.StatusOK, resp) } func (h *SnapshotHandler) Rollback(w http.ResponseWriter, r *http.Request) { repoID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { Error(w, http.StatusBadRequest, "invalid id") return } snapID, err := strconv.ParseInt(chi.URLParam(r, "snap_id"), 10, 64) if err != nil { Error(w, http.StatusBadRequest, "invalid snap_id") return } if err := Authorize(r.Context(), ActionSnapshotRollback, repoID); err != nil { Error(w, http.StatusForbidden, "forbidden") return } if err := h.svc.Rollback(r.Context(), repoID, snapID); errors.Is(err, store.ErrNotFound) { Error(w, http.StatusNotFound, "snapshot not found") return } else if err != nil { Error(w, http.StatusInternalServerError, err.Error()) return } JSON(w, http.StatusOK, map[string]string{"status": "rollback completed"}) } func snapshotToResponse(s store.Snapshot) SnapshotResponse { return SnapshotResponse{ ID: s.ID, RepoID: s.RepoID, Label: s.Label, CreatedAt: s.CreatedAt, } } func snapshotPkgToResponse(p store.SnapshotPackage) SnapshotPackageResponse { return SnapshotPackageResponse{ ID: p.ID, Name: p.Name, Version: p.Version, Arch: p.Arch, Location: p.Location, Checksum: p.Checksum, ChecksumType: p.ChecksumType, Size: p.Size, } }