first with full handle over rpm

Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
GnomeZworc 2026-04-25 14:49:14 +02:00
commit 274ea454dd
Signed by: nicolas.boufideline
GPG key ID: 4406BBBF8845D632
50 changed files with 4309 additions and 0 deletions

9
cmd/clonepack/main.go Normal file
View file

@ -0,0 +1,9 @@
package main
import "os"
func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

498
cmd/clonepack/repo.go Normal file
View file

@ -0,0 +1,498 @@
package main
import (
"errors"
"fmt"
"os"
"strconv"
"text/tabwriter"
"time"
"github.com/spf13/cobra"
"github.com/syonad/clonepack/client"
)
var repoCloneCmd = &cobra.Command{
Use: "clone <id>",
Short: "Clone an RPM repository",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid id: %s", args[0])
}
c := client.New(apiURL)
resp, err := c.StartClone(cmd.Context(), id)
if err != nil {
return fmt.Errorf("start clone: %w", err)
}
fmt.Printf("Clone job started: job_id=%d\n", resp.JobID)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-cmd.Context().Done():
return cmd.Context().Err()
case <-ticker.C:
status, err := c.GetCloneStatus(cmd.Context(), id)
if err != nil {
return fmt.Errorf("get status: %w", err)
}
fmt.Printf(" status: %s\n", status.Status)
switch status.Status {
case "completed":
fmt.Println("Clone completed successfully.")
return nil
case "failed":
errMsg := "(unknown error)"
if status.Error != nil {
errMsg = *status.Error
}
return fmt.Errorf("clone failed: %s", errMsg)
}
}
}
},
}
var repoCmd = &cobra.Command{
Use: "repo",
Short: "Manage repositories",
}
var (
repoName string
repoType string
repoSourceURL string
)
var repoCreateCmd = &cobra.Command{
Use: "create",
Short: "Create a new repository",
RunE: func(cmd *cobra.Command, args []string) error {
c := client.New(apiURL)
repo, err := c.CreateRepo(cmd.Context(), client.CreateRepoInput{
Name: repoName,
Type: repoType,
SourceURL: repoSourceURL,
SyncMode: repoSyncMode,
})
if err != nil {
return err
}
printRepos([]client.Repo{*repo})
return nil
},
}
var repoListCmd = &cobra.Command{
Use: "list",
Short: "List all repositories",
RunE: func(cmd *cobra.Command, args []string) error {
c := client.New(apiURL)
repos, err := c.ListRepos(cmd.Context())
if err != nil {
return err
}
if len(repos) == 0 {
fmt.Println("No repositories found.")
return nil
}
printRepos(repos)
return nil
},
}
var repoGetCmd = &cobra.Command{
Use: "get <id>",
Short: "Get a repository by ID",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid id: %s", args[0])
}
c := client.New(apiURL)
repo, err := c.GetRepo(cmd.Context(), id)
if errors.Is(err, client.ErrNotFound) {
return fmt.Errorf("repo %d not found", id)
}
if err != nil {
return err
}
printRepos([]client.Repo{*repo})
return nil
},
}
var repoDeleteCmd = &cobra.Command{
Use: "delete <id>",
Short: "Delete a repository by ID",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid id: %s", args[0])
}
c := client.New(apiURL)
if err := c.DeleteRepo(cmd.Context(), id); errors.Is(err, client.ErrNotFound) {
return fmt.Errorf("repo %d not found", id)
} else if err != nil {
return err
}
fmt.Printf("Deleted repo %d\n", id)
return nil
},
}
func printRepos(repos []client.Repo) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tNAME\tTYPE\tSOURCE URL\tFROZEN\tSYNC MODE\tCREATED AT")
for _, r := range repos {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%v\t%s\t%s\n",
r.ID, r.Name, r.Type, r.SourceURL, r.Frozen, r.SyncMode,
r.CreatedAt.Format(time.RFC3339),
)
}
w.Flush()
}
var repoSyncCmd = &cobra.Command{
Use: "sync <id>",
Short: "Trigger a sync scan for a repository",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, _ := strconv.ParseInt(args[0], 10, 64)
c := client.New(apiURL)
if err := c.TriggerSync(cmd.Context(), id); err != nil {
return err
}
fmt.Println("Scan started. Use 'sync-list' to check pending packages.")
return nil
},
}
var repoSyncListCmd = &cobra.Command{
Use: "sync-list <id>",
Short: "List packages pending approval for a repository",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, _ := strconv.ParseInt(args[0], 10, 64)
c := client.New(apiURL)
pkgs, err := c.ListPending(cmd.Context(), id)
if err != nil {
return err
}
if len(pkgs) == 0 {
fmt.Println("No packages pending approval.")
return nil
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tNAME\tVERSION\tARCH\tSIZE")
for _, p := range pkgs {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%d\n", p.ID, p.Name, p.Version, p.Arch, p.Size)
}
w.Flush()
return nil
},
}
var syncApproveAll bool
var repoSyncApproveCmd = &cobra.Command{
Use: "sync-approve <repo_id> [pkg_id...]",
Short: "Approve pending packages for download",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
repoID, _ := strconv.ParseInt(args[0], 10, 64)
c := client.New(apiURL)
var ids []int64
if syncApproveAll {
pkgs, err := c.ListPending(cmd.Context(), repoID)
if err != nil {
return fmt.Errorf("list pending: %w", err)
}
if len(pkgs) == 0 {
fmt.Println("No packages pending approval.")
return nil
}
for _, p := range pkgs {
ids = append(ids, p.ID)
}
} else {
if len(args) < 2 {
return fmt.Errorf("provide package IDs or use --all")
}
for _, a := range args[1:] {
id, err := strconv.ParseInt(a, 10, 64)
if err != nil {
return fmt.Errorf("invalid pkg id: %s", a)
}
ids = append(ids, id)
}
}
if err := c.ApprovePending(cmd.Context(), repoID, ids); err != nil {
return err
}
fmt.Printf("Approval started for %d package(s). Downloads running in background.\n", len(ids))
return nil
},
}
var repoSyncRejectCmd = &cobra.Command{
Use: "sync-reject <repo_id> <pkg_id>...",
Short: "Reject pending packages",
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
repoID, _ := strconv.ParseInt(args[0], 10, 64)
var ids []int64
for _, a := range args[1:] {
id, _ := strconv.ParseInt(a, 10, 64)
ids = append(ids, id)
}
c := client.New(apiURL)
if err := c.RejectPending(cmd.Context(), repoID, ids); err != nil {
return err
}
fmt.Printf("Rejected %d package(s)\n", len(ids))
return nil
},
}
var repoSyncBlockCmd = &cobra.Command{
Use: "sync-block <repo_id> <pkg_id>...",
Short: "Bloquer définitivement des paquets (par ID pending)",
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
repoID, _ := strconv.ParseInt(args[0], 10, 64)
var ids []int64
for _, a := range args[1:] {
id, err := strconv.ParseInt(a, 10, 64)
if err != nil {
return fmt.Errorf("invalid id: %s", a)
}
ids = append(ids, id)
}
c := client.New(apiURL)
if err := c.BlockPackages(cmd.Context(), repoID, ids); err != nil {
return err
}
fmt.Printf("%d paquet(s) bloqué(s) définitivement.\n", len(ids))
return nil
},
}
var repoSyncUnblockCmd = &cobra.Command{
Use: "sync-unblock <repo_id> <blocked_id>...",
Short: "Débloquer des paquets (par ID blocked)",
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
repoID, _ := strconv.ParseInt(args[0], 10, 64)
var ids []int64
for _, a := range args[1:] {
id, _ := strconv.ParseInt(a, 10, 64)
ids = append(ids, id)
}
c := client.New(apiURL)
if err := c.UnblockPackages(cmd.Context(), repoID, ids); err != nil {
return err
}
fmt.Printf("%d paquet(s) débloqué(s).\n", len(ids))
return nil
},
}
var repoSyncBlockListCmd = &cobra.Command{
Use: "sync-blocklist <repo_id>",
Short: "Lister les paquets bloqués définitivement",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
repoID, _ := strconv.ParseInt(args[0], 10, 64)
c := client.New(apiURL)
pkgs, err := c.ListBlocked(cmd.Context(), repoID)
if err != nil {
return err
}
if len(pkgs) == 0 {
fmt.Println("Aucun paquet bloqué.")
return nil
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tNOM\tLOCATION\tBLOQUÉ LE")
for _, p := range pkgs {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", p.ID, p.Name, p.Location, p.CreatedAt.Format(time.RFC3339))
}
w.Flush()
return nil
},
}
var repoSnapshotCmd = &cobra.Command{
Use: "snapshot",
Short: "Manage repository snapshots",
}
var repoSnapshotCreateCmd = &cobra.Command{
Use: "create <repo_id>",
Short: "Create a snapshot of a repository",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
repoID, _ := strconv.ParseInt(args[0], 10, 64)
c := client.New(apiURL)
snap, err := c.CreateSnapshot(cmd.Context(), repoID, snapshotLabel)
if err != nil {
return err
}
printSnapshots([]client.Snapshot{*snap})
return nil
},
}
var repoSnapshotListCmd = &cobra.Command{
Use: "list <repo_id>",
Short: "List snapshots of a repository",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
repoID, _ := strconv.ParseInt(args[0], 10, 64)
c := client.New(apiURL)
snaps, err := c.ListSnapshots(cmd.Context(), repoID)
if err != nil {
return err
}
if len(snaps) == 0 {
fmt.Println("No snapshots found.")
return nil
}
printSnapshots(snaps)
return nil
},
}
var repoSnapshotShowCmd = &cobra.Command{
Use: "show <repo_id> <snap_id>",
Short: "Show snapshot details",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
repoID, _ := strconv.ParseInt(args[0], 10, 64)
snapID, _ := strconv.ParseInt(args[1], 10, 64)
c := client.New(apiURL)
detail, err := c.GetSnapshot(cmd.Context(), repoID, snapID)
if err != nil {
return err
}
printSnapshots([]client.Snapshot{detail.Snapshot})
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "NAME\tVERSION\tARCH\tSIZE")
for _, p := range detail.Packages {
fmt.Fprintf(w, "%s\t%s\t%s\t%d\n", p.Name, p.Version, p.Arch, p.Size)
}
w.Flush()
return nil
},
}
var repoSnapshotDeleteCmd = &cobra.Command{
Use: "delete <repo_id> <snap_id>",
Short: "Delete a snapshot",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
repoID, _ := strconv.ParseInt(args[0], 10, 64)
snapID, _ := strconv.ParseInt(args[1], 10, 64)
c := client.New(apiURL)
if err := c.DeleteSnapshot(cmd.Context(), repoID, snapID); err != nil {
return err
}
fmt.Printf("Deleted snapshot %d\n", snapID)
return nil
},
}
var repoSnapshotDiffCmd = &cobra.Command{
Use: "diff <repo_id> <from_snap_id> <to_snap_id>",
Short: "Diff two snapshots",
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
repoID, _ := strconv.ParseInt(args[0], 10, 64)
fromID, _ := strconv.ParseInt(args[1], 10, 64)
toID, _ := strconv.ParseInt(args[2], 10, 64)
c := client.New(apiURL)
diff, err := c.DiffSnapshots(cmd.Context(), repoID, fromID, toID)
if err != nil {
return err
}
fmt.Printf("From: [%d] %s\nTo: [%d] %s\nUnchanged: %d\n",
diff.From.ID, diff.From.Label, diff.To.ID, diff.To.Label, diff.Unchanged)
if len(diff.Added) > 0 {
fmt.Printf("\nAdded (%d):\n", len(diff.Added))
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, " NAME\tVERSION\tARCH")
for _, p := range diff.Added {
fmt.Fprintf(w, " %s\t%s\t%s\n", p.Name, p.Version, p.Arch)
}
w.Flush()
}
if len(diff.Removed) > 0 {
fmt.Printf("\nRemoved (%d):\n", len(diff.Removed))
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, " NAME\tVERSION\tARCH")
for _, p := range diff.Removed {
fmt.Fprintf(w, " %s\t%s\t%s\n", p.Name, p.Version, p.Arch)
}
w.Flush()
}
return nil
},
}
var repoSnapshotRollbackCmd = &cobra.Command{
Use: "rollback <repo_id> <snap_id>",
Short: "Rollback repository to a snapshot",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
repoID, _ := strconv.ParseInt(args[0], 10, 64)
snapID, _ := strconv.ParseInt(args[1], 10, 64)
c := client.New(apiURL)
if err := c.RollbackSnapshot(cmd.Context(), repoID, snapID); err != nil {
return err
}
fmt.Println("Rollback completed.")
return nil
},
}
func printSnapshots(snaps []client.Snapshot) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tREPO ID\tLABEL\tCREATED AT")
for _, s := range snaps {
fmt.Fprintf(w, "%d\t%d\t%s\t%s\n", s.ID, s.RepoID, s.Label, s.CreatedAt.Format(time.RFC3339))
}
w.Flush()
}
var snapshotLabel string
var repoSyncMode string
func init() {
repoCreateCmd.Flags().StringVar(&repoName, "name", "", "Repository name (required)")
repoCreateCmd.Flags().StringVar(&repoType, "type", "", "Type: apt|rpm|docker|binary (required)")
repoCreateCmd.Flags().StringVar(&repoSourceURL, "source-url", "", "Upstream source URL (required)")
repoCreateCmd.Flags().StringVar(&repoSyncMode, "sync-mode", "auto", "Sync mode: auto|manual")
repoCreateCmd.MarkFlagRequired("name")
repoCreateCmd.MarkFlagRequired("type")
repoCreateCmd.MarkFlagRequired("source-url")
repoSyncApproveCmd.Flags().BoolVar(&syncApproveAll, "all", false, "Approuver tous les paquets en attente")
repoSnapshotCreateCmd.Flags().StringVar(&snapshotLabel, "label", "", "Snapshot label")
repoSnapshotCmd.AddCommand(repoSnapshotCreateCmd, repoSnapshotListCmd, repoSnapshotShowCmd,
repoSnapshotDeleteCmd, repoSnapshotDiffCmd, repoSnapshotRollbackCmd)
repoCmd.AddCommand(repoCreateCmd, repoListCmd, repoGetCmd, repoDeleteCmd, repoCloneCmd,
repoSyncCmd, repoSyncListCmd, repoSyncApproveCmd, repoSyncRejectCmd,
repoSyncBlockCmd, repoSyncUnblockCmd, repoSyncBlockListCmd,
repoSnapshotCmd)
}

20
cmd/clonepack/root.go Normal file
View file

@ -0,0 +1,20 @@
package main
import "github.com/spf13/cobra"
var (
cfgFile string
apiURL string
)
var rootCmd = &cobra.Command{
Use: "clonepack",
Short: "ClonePack artifact mirror manager",
}
func init() {
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file path")
rootCmd.PersistentFlags().StringVar(&apiURL, "api-url", "http://localhost:8080", "ClonePack server URL")
rootCmd.AddCommand(serveCmd)
rootCmd.AddCommand(repoCmd)
}

60
cmd/clonepack/serve.go Normal file
View file

@ -0,0 +1,60 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/spf13/cobra"
"github.com/syonad/clonepack/config"
"github.com/syonad/clonepack/internal/api"
"github.com/syonad/clonepack/internal/core"
"github.com/syonad/clonepack/internal/store"
)
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the ClonePack HTTP server",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.Load(cfgFile)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
db, err := store.Open(cfg.DB)
if err != nil {
return fmt.Errorf("open store: %w", err)
}
repoStore := store.NewRepoStore(db)
jobStore := store.NewCloneJobStore(db)
pendingStore := store.NewPendingPackageStore(db)
blockedStore := store.NewBlockedPackageStore(db)
snapshotStore := store.NewSnapshotStore(db)
repoSvc := core.NewRepoService(repoStore, cfg.DataDir)
cloneSvc := core.NewCloneService(repoStore, jobStore, cfg.DataDir)
snapshotSvc := core.NewSnapshotService(snapshotStore, repoStore, cfg.DataDir)
syncSvc := core.NewSyncService(repoStore, pendingStore, blockedStore, cloneSvc, snapshotSvc, cfg.DataDir)
repoH := api.NewRepoHandler(repoSvc)
cloneH := api.NewCloneHandler(cloneSvc)
syncH := api.NewSyncHandler(syncSvc)
snapshotH := api.NewSnapshotHandler(snapshotSvc)
proxyH := api.NewProxyHandler(repoSvc, cfg.DataDir)
router := api.NewRouter(repoH, cloneH, syncH, snapshotH, proxyH)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
syncSvc.StartScheduler(ctx, cfg.Sync.IntervalDuration())
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
log.Printf("ClonePack listening on %s", addr)
return http.ListenAndServe(addr, router)
},
}