first with full handle over rpm
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
parent
7948368573
commit
274ea454dd
50 changed files with 4309 additions and 0 deletions
83
internal/api/clone_handler.go
Normal file
83
internal/api/clone_handler.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/syonad/clonepack/internal/core"
|
||||
"github.com/syonad/clonepack/internal/store"
|
||||
)
|
||||
|
||||
type CloneHandler struct {
|
||||
svc *core.CloneService
|
||||
}
|
||||
|
||||
func NewCloneHandler(svc *core.CloneService) *CloneHandler {
|
||||
return &CloneHandler{svc: svc}
|
||||
}
|
||||
|
||||
func (h *CloneHandler) StartClone(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
|
||||
}
|
||||
|
||||
jobID, err := h.svc.StartClone(r.Context(), repoID)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
Error(w, http.StatusNotFound, "repo not found")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, core.ErrCloneAlreadyRunning) {
|
||||
Error(w, http.StatusConflict, err.Error())
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusAccepted, StartCloneResponse{JobID: jobID, Status: "running"})
|
||||
}
|
||||
|
||||
func (h *CloneHandler) GetStatus(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
|
||||
}
|
||||
|
||||
job, err := h.svc.GetLatestCloneJob(r.Context(), repoID)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
Error(w, http.StatusNotFound, "no clone job found for this repo")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, cloneJobToResponse(job))
|
||||
}
|
||||
|
||||
func cloneJobToResponse(j *store.CloneJob) CloneStatusResponse {
|
||||
resp := CloneStatusResponse{
|
||||
JobID: j.ID,
|
||||
RepoID: j.RepoID,
|
||||
Status: string(j.Status),
|
||||
Error: j.Error,
|
||||
CreatedAt: j.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
if j.StartedAt != nil {
|
||||
s := j.StartedAt.Format(time.RFC3339)
|
||||
resp.StartedAt = &s
|
||||
}
|
||||
if j.FinishedAt != nil {
|
||||
f := j.FinishedAt.Format(time.RFC3339)
|
||||
resp.FinishedAt = &f
|
||||
}
|
||||
return resp
|
||||
}
|
||||
43
internal/api/proxy_handler.go
Normal file
43
internal/api/proxy_handler.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/syonad/clonepack/internal/core"
|
||||
"github.com/syonad/clonepack/internal/store"
|
||||
)
|
||||
|
||||
type ProxyHandler struct {
|
||||
repoSvc *core.RepoService
|
||||
dataDir string
|
||||
}
|
||||
|
||||
func NewProxyHandler(repoSvc *core.RepoService, dataDir string) *ProxyHandler {
|
||||
return &ProxyHandler{repoSvc: repoSvc, dataDir: dataDir}
|
||||
}
|
||||
|
||||
func (h *ProxyHandler) ServeFile(w http.ResponseWriter, r *http.Request) {
|
||||
repoID, err := strconv.ParseInt(chi.URLParam(r, "repo_id"), 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid repo_id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := h.repoSvc.Get(r.Context(), repoID)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
localDir := fmt.Sprintf("%s/repos/%d/%s", h.dataDir, repoID, repo.Type)
|
||||
prefix := fmt.Sprintf("/mirror/%d", repoID)
|
||||
http.StripPrefix(prefix, http.FileServer(http.Dir(localDir))).ServeHTTP(w, r)
|
||||
}
|
||||
102
internal/api/repo_handler.go
Normal file
102
internal/api/repo_handler.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/syonad/clonepack/internal/core"
|
||||
"github.com/syonad/clonepack/internal/store"
|
||||
)
|
||||
|
||||
type RepoHandler struct {
|
||||
svc *core.RepoService
|
||||
}
|
||||
|
||||
func NewRepoHandler(svc *core.RepoService) *RepoHandler {
|
||||
return &RepoHandler{svc: svc}
|
||||
}
|
||||
|
||||
func (h *RepoHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
var req CreateRepoRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid JSON")
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := h.svc.Create(r.Context(), core.CreateRepoInput{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
SourceURL: req.SourceURL,
|
||||
SyncMode: req.SyncMode,
|
||||
})
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusCreated, repoToResponse(repo))
|
||||
}
|
||||
|
||||
func (h *RepoHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
repos, err := h.svc.List(r.Context())
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]RepoResponse, len(repos))
|
||||
for i, repo := range repos {
|
||||
items[i] = repoToResponse(&repo)
|
||||
}
|
||||
JSON(w, http.StatusOK, ListReposResponse{Items: items, Total: len(items)})
|
||||
}
|
||||
|
||||
func (h *RepoHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := h.svc.Get(r.Context(), id)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
Error(w, http.StatusNotFound, "repo not found")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, repoToResponse(repo))
|
||||
}
|
||||
|
||||
func (h *RepoHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.Delete(r.Context(), id); errors.Is(err, store.ErrNotFound) {
|
||||
Error(w, http.StatusNotFound, "repo not found")
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func repoToResponse(r *store.Repo) RepoResponse {
|
||||
return RepoResponse{
|
||||
ID: r.ID,
|
||||
Name: r.Name,
|
||||
Type: r.Type,
|
||||
SourceURL: r.SourceURL,
|
||||
Frozen: r.Frozen,
|
||||
SyncMode: r.SyncMode,
|
||||
CreatedAt: r.CreatedAt,
|
||||
}
|
||||
}
|
||||
16
internal/api/respond.go
Normal file
16
internal/api/respond.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func JSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func Error(w http.ResponseWriter, status int, msg string) {
|
||||
JSON(w, status, ErrorResponse{Error: msg})
|
||||
}
|
||||
50
internal/api/router.go
Normal file
50
internal/api/router.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
func NewRouter(repoHandler *RepoHandler, cloneHandler *CloneHandler, syncHandler *SyncHandler, snapshotHandler *SnapshotHandler, proxyHandler *ProxyHandler) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(middleware.RequestID)
|
||||
|
||||
r.Get("/mirror/{repo_id}/*", proxyHandler.ServeFile)
|
||||
|
||||
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
JSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
})
|
||||
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
r.Route("/repos", func(r chi.Router) {
|
||||
r.Post("/", repoHandler.Create)
|
||||
r.Get("/", repoHandler.List)
|
||||
r.Get("/{id}", repoHandler.Get)
|
||||
r.Delete("/{id}", repoHandler.Delete)
|
||||
|
||||
r.Post("/{id}/clone", cloneHandler.StartClone)
|
||||
r.Get("/{id}/clone/status", cloneHandler.GetStatus)
|
||||
|
||||
r.Post("/{id}/sync/trigger", syncHandler.Trigger)
|
||||
r.Get("/{id}/sync/pending", syncHandler.ListPending)
|
||||
r.Post("/{id}/sync/approve", syncHandler.Approve)
|
||||
r.Post("/{id}/sync/reject", syncHandler.Reject)
|
||||
r.Post("/{id}/sync/block", syncHandler.Block)
|
||||
r.Post("/{id}/sync/unblock", syncHandler.Unblock)
|
||||
r.Get("/{id}/sync/blocked", syncHandler.ListBlocked)
|
||||
|
||||
r.Post("/{id}/snapshots", snapshotHandler.Create)
|
||||
r.Get("/{id}/snapshots", snapshotHandler.List)
|
||||
r.Get("/{id}/snapshots/diff", snapshotHandler.Diff)
|
||||
r.Get("/{id}/snapshots/{snap_id}", snapshotHandler.Get)
|
||||
r.Delete("/{id}/snapshots/{snap_id}", snapshotHandler.Delete)
|
||||
r.Post("/{id}/snapshots/{snap_id}/rollback", snapshotHandler.Rollback)
|
||||
})
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
196
internal/api/snapshot_handler.go
Normal file
196
internal/api/snapshot_handler.go
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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 := 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
|
||||
}
|
||||
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 := 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,
|
||||
}
|
||||
}
|
||||
43
internal/api/snapshot_types.go
Normal file
43
internal/api/snapshot_types.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package api
|
||||
|
||||
import "time"
|
||||
|
||||
type SnapshotResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
Label string `json:"label"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type SnapshotDetailResponse struct {
|
||||
SnapshotResponse
|
||||
Packages []SnapshotPackageResponse `json:"packages"`
|
||||
}
|
||||
|
||||
type SnapshotPackageResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Arch string `json:"arch"`
|
||||
Location string `json:"location"`
|
||||
Checksum string `json:"checksum"`
|
||||
ChecksumType string `json:"checksum_type"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type ListSnapshotsResponse struct {
|
||||
Items []SnapshotResponse `json:"items"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type CreateSnapshotRequest struct {
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type SnapshotDiffResponse struct {
|
||||
From SnapshotResponse `json:"from"`
|
||||
To SnapshotResponse `json:"to"`
|
||||
Added []SnapshotPackageResponse `json:"added"`
|
||||
Removed []SnapshotPackageResponse `json:"removed"`
|
||||
Unchanged int `json:"unchanged"`
|
||||
}
|
||||
191
internal/api/sync_handler.go
Normal file
191
internal/api/sync_handler.go
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/syonad/clonepack/internal/core"
|
||||
"github.com/syonad/clonepack/internal/store"
|
||||
)
|
||||
|
||||
type SyncHandler struct {
|
||||
svc *core.SyncService
|
||||
}
|
||||
|
||||
func NewSyncHandler(svc *core.SyncService) *SyncHandler {
|
||||
return &SyncHandler{svc: svc}
|
||||
}
|
||||
|
||||
func (h *SyncHandler) Trigger(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
|
||||
}
|
||||
// Validate repo exists before returning.
|
||||
if err := h.svc.ValidateRepo(r.Context(), repoID); errors.Is(err, store.ErrNotFound) {
|
||||
Error(w, http.StatusNotFound, "repo not found")
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
if err := h.svc.ScanRepo(context.Background(), repoID); err != nil {
|
||||
log.Printf("scan repo %d: %v", repoID, err)
|
||||
}
|
||||
}()
|
||||
JSON(w, http.StatusAccepted, map[string]string{"status": "scan started"})
|
||||
}
|
||||
|
||||
func (h *SyncHandler) ListPending(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
|
||||
}
|
||||
pkgs, err := h.svc.ListPending(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([]PendingPackageResponse, len(pkgs))
|
||||
for i, p := range pkgs {
|
||||
items[i] = pendingToResponse(p)
|
||||
}
|
||||
JSON(w, http.StatusOK, ListPendingResponse{Items: items, Total: len(items)})
|
||||
}
|
||||
|
||||
func (h *SyncHandler) Approve(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
|
||||
}
|
||||
var req SyncSelectionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || len(req.IDs) == 0 {
|
||||
Error(w, http.StatusBadRequest, "body must contain non-empty ids array")
|
||||
return
|
||||
}
|
||||
if err := h.svc.ValidateRepo(r.Context(), repoID); errors.Is(err, store.ErrNotFound) {
|
||||
Error(w, http.StatusNotFound, "repo not found")
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
ids := req.IDs
|
||||
go func() {
|
||||
if err := h.svc.ApprovePending(context.Background(), repoID, ids); err != nil {
|
||||
log.Printf("approve pending for repo %d: %v", repoID, err)
|
||||
}
|
||||
}()
|
||||
JSON(w, http.StatusAccepted, map[string]string{"status": "approval started"})
|
||||
}
|
||||
|
||||
func (h *SyncHandler) Reject(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
|
||||
}
|
||||
var req SyncSelectionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || len(req.IDs) == 0 {
|
||||
Error(w, http.StatusBadRequest, "body must contain non-empty ids array")
|
||||
return
|
||||
}
|
||||
if err := h.svc.RejectPending(r.Context(), repoID, req.IDs); errors.Is(err, store.ErrNotFound) {
|
||||
Error(w, http.StatusNotFound, "repo not found")
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *SyncHandler) Block(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
|
||||
}
|
||||
var req SyncSelectionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || len(req.IDs) == 0 {
|
||||
Error(w, http.StatusBadRequest, "body must contain non-empty ids array")
|
||||
return
|
||||
}
|
||||
if err := h.svc.BlockPackages(r.Context(), repoID, req.IDs); errors.Is(err, store.ErrNotFound) {
|
||||
Error(w, http.StatusNotFound, "repo not found")
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *SyncHandler) Unblock(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
|
||||
}
|
||||
var req SyncSelectionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || len(req.IDs) == 0 {
|
||||
Error(w, http.StatusBadRequest, "body must contain non-empty ids array")
|
||||
return
|
||||
}
|
||||
if err := h.svc.UnblockPackages(r.Context(), repoID, req.IDs); errors.Is(err, store.ErrNotFound) {
|
||||
Error(w, http.StatusNotFound, "repo not found")
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *SyncHandler) ListBlocked(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
|
||||
}
|
||||
pkgs, err := h.svc.ListBlocked(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([]BlockedPackageResponse, len(pkgs))
|
||||
for i, p := range pkgs {
|
||||
items[i] = BlockedPackageResponse{ID: p.ID, RepoID: p.RepoID, Name: p.Name, Location: p.Location, CreatedAt: p.CreatedAt}
|
||||
}
|
||||
JSON(w, http.StatusOK, ListBlockedResponse{Items: items, Total: len(items)})
|
||||
}
|
||||
|
||||
func pendingToResponse(p store.PendingPackage) PendingPackageResponse {
|
||||
return PendingPackageResponse{
|
||||
ID: p.ID,
|
||||
RepoID: p.RepoID,
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
Arch: p.Arch,
|
||||
Location: p.Location,
|
||||
Checksum: p.Checksum,
|
||||
ChecksumType: p.ChecksumType,
|
||||
Size: p.Size,
|
||||
CreatedAt: p.CreatedAt,
|
||||
}
|
||||
}
|
||||
38
internal/api/sync_types.go
Normal file
38
internal/api/sync_types.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package api
|
||||
|
||||
import "time"
|
||||
|
||||
type SyncSelectionRequest struct {
|
||||
IDs []int64 `json:"ids"`
|
||||
}
|
||||
|
||||
type PendingPackageResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Arch string `json:"arch"`
|
||||
Location string `json:"location"`
|
||||
Checksum string `json:"checksum"`
|
||||
ChecksumType string `json:"checksum_type"`
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type ListPendingResponse struct {
|
||||
Items []PendingPackageResponse `json:"items"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type BlockedPackageResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
Name string `json:"name"`
|
||||
Location string `json:"location"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type ListBlockedResponse struct {
|
||||
Items []BlockedPackageResponse `json:"items"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
44
internal/api/types.go
Normal file
44
internal/api/types.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package api
|
||||
|
||||
import "time"
|
||||
|
||||
type CreateRepoRequest struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
SourceURL string `json:"source_url"`
|
||||
SyncMode string `json:"sync_mode,omitempty"`
|
||||
}
|
||||
|
||||
type RepoResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
SourceURL string `json:"source_url"`
|
||||
Frozen bool `json:"frozen"`
|
||||
SyncMode string `json:"sync_mode"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type ListReposResponse struct {
|
||||
Items []RepoResponse `json:"items"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type StartCloneResponse struct {
|
||||
JobID int64 `json:"job_id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type CloneStatusResponse struct {
|
||||
JobID int64 `json:"job_id"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
Status string `json:"status"`
|
||||
StartedAt *string `json:"started_at,omitempty"`
|
||||
FinishedAt *string `json:"finished_at,omitempty"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue