package main import ( "context" "errors" "fmt" "net/url" "os" "os/signal" "path/filepath" "syscall" "time" "github.com/grmrgecko/repo-sync/mirror" ) // SyncArgs holds the flags and positional arguments shared by every // repository type. type SyncArgs struct { Trim int `help:"Number of leading URL path components to drop when building the destination path." default:"0"` Flat bool `help:"Place repositories directly in the destination directory without copying the URL path."` Discover bool `help:"Treat each URL as a directory index and crawl it for repositories."` Depth int `help:"Maximum directory depth crawled in discovery mode." default:"5"` Workers int `help:"Number of concurrent download workers." default:"4"` Verify bool `help:"Re-verify checksums of files that already exist locally."` Prune bool `help:"Delete local files that are no longer part of the repository."` PruneGrace time.Duration `help:"Keep files that left the repository for this long before pruning them (e.g. 72h)."` DryRun bool `help:"Report what would be downloaded or pruned without changing the repository."` Args []string `arg:"" name:"url" help:"Repository or mirrorlist URLs followed by the destination directory."` } // options validates the shared arguments and builds the mirror options. func (s *SyncArgs) options(typ mirror.RepoType) (*mirror.Options, error) { if len(s.Args) < 2 { return nil, errors.New("expected at least one repository URL and a destination directory") } urls := s.Args[:len(s.Args)-1] for _, raw := range urls { u, err := url.Parse(raw) if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { return nil, fmt.Errorf("invalid repository URL %q", raw) } } dest, err := filepath.Abs(s.Args[len(s.Args)-1]) if err != nil { return nil, err } if s.Trim < 0 { return nil, errors.New("trim must not be negative") } if s.Depth < 0 { return nil, errors.New("depth must not be negative") } if s.PruneGrace < 0 { return nil, errors.New("prune grace must not be negative") } return &mirror.Options{ Type: typ, URLs: urls, Destination: dest, Trim: s.Trim, Flat: s.Flat, Discover: s.Discover, DiscoverDepth: s.Depth, Workers: s.Workers, Verify: s.Verify, Prune: s.Prune, PruneGrace: s.PruneGrace, DryRun: s.DryRun, }, nil } // run executes a synchronization with signal-aware cancellation. func (s *SyncArgs) run(opts *mirror.Options) error { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() return mirror.Sync(ctx, opts) } // RPMCmd synchronizes RPM repositories. type RPMCmd struct { SyncArgs `embed:""` } // Run performs the RPM synchronization. func (c *RPMCmd) Run() error { opts, err := c.options(mirror.RepoRPM) if err != nil { return err } return c.run(opts) } // ArchCmd synchronizes Arch Linux repositories. type ArchCmd struct { SyncArgs `embed:""` } // Run performs the Arch Linux synchronization. func (c *ArchCmd) Run() error { opts, err := c.options(mirror.RepoArch) if err != nil { return err } return c.run(opts) } // ApkCmd synchronizes Alpine Linux repositories. type ApkCmd struct { SyncArgs `embed:""` } // Run performs the Alpine Linux synchronization. func (c *ApkCmd) Run() error { opts, err := c.options(mirror.RepoApk) if err != nil { return err } return c.run(opts) } // DebCmd synchronizes DEB repositories. type DebCmd struct { Component []string `help:"Limit synchronization to these components."` Arch []string `help:"Limit synchronization to these architectures; include \"source\" to keep source indexes."` SyncArgs `embed:""` } // Run performs the DEB synchronization. func (c *DebCmd) Run() error { opts, err := c.options(mirror.RepoDeb) if err != nil { return err } opts.Components = c.Component opts.Architectures = c.Arch return c.run(opts) }