198 lines
4.8 KiB
Go
198 lines
4.8 KiB
Go
package download
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/v2/pkg/cmd/run/shared"
|
|
"github.com/cli/cli/v2/pkg/cmdutil"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/cli/cli/v2/pkg/set"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type DownloadOptions struct {
|
|
IO *iostreams.IOStreams
|
|
Platform platform
|
|
Prompter iprompter
|
|
|
|
DoPrompt bool
|
|
RunID string
|
|
DestinationDir string
|
|
Names []string
|
|
FilePatterns []string
|
|
}
|
|
|
|
type platform interface {
|
|
List(runID string) ([]shared.Artifact, error)
|
|
Download(url string, dir string) error
|
|
}
|
|
type iprompter interface {
|
|
MultiSelect(string, []string, []string) ([]int, error)
|
|
}
|
|
|
|
func NewCmdDownload(f *cmdutil.Factory, runF func(*DownloadOptions) error) *cobra.Command {
|
|
opts := &DownloadOptions{
|
|
IO: f.IOStreams,
|
|
Prompter: f.Prompter,
|
|
}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "download [<run-id>]",
|
|
Short: "Download artifacts generated by a workflow run",
|
|
Long: heredoc.Doc(`
|
|
Download artifacts generated by a GitHub Actions workflow run.
|
|
|
|
The contents of each artifact will be extracted under separate directories based on
|
|
the artifact name. If only a single artifact is specified, it will be extracted into
|
|
the current directory.
|
|
`),
|
|
Args: cobra.MaximumNArgs(1),
|
|
Example: heredoc.Doc(`
|
|
# Download all artifacts generated by a workflow run
|
|
$ gh run download <run-id>
|
|
|
|
# Download a specific artifact within a run
|
|
$ gh run download <run-id> -n <name>
|
|
|
|
# Download specific artifacts across all runs in a repository
|
|
$ gh run download -n <name1> -n <name2>
|
|
|
|
# Select artifacts to download interactively
|
|
$ gh run download
|
|
`),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if len(args) > 0 {
|
|
opts.RunID = args[0]
|
|
} else if len(opts.Names) == 0 &&
|
|
len(opts.FilePatterns) == 0 &&
|
|
opts.IO.CanPrompt() {
|
|
opts.DoPrompt = true
|
|
}
|
|
// support `-R, --repo` override
|
|
baseRepo, err := f.BaseRepo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
httpClient, err := f.HttpClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Platform = &apiPlatform{
|
|
client: httpClient,
|
|
repo: baseRepo,
|
|
}
|
|
|
|
if runF != nil {
|
|
return runF(opts)
|
|
}
|
|
return runDownload(opts)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVarP(&opts.DestinationDir, "dir", "D", ".", "The directory to download artifacts into")
|
|
cmd.Flags().StringArrayVarP(&opts.Names, "name", "n", nil, "Download artifacts that match any of the given names")
|
|
cmd.Flags().StringArrayVarP(&opts.FilePatterns, "pattern", "p", nil, "Download artifacts that match a glob pattern")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func runDownload(opts *DownloadOptions) error {
|
|
opts.IO.StartProgressIndicator()
|
|
artifacts, err := opts.Platform.List(opts.RunID)
|
|
opts.IO.StopProgressIndicator()
|
|
if err != nil {
|
|
return fmt.Errorf("error fetching artifacts: %w", err)
|
|
}
|
|
|
|
numValidArtifacts := 0
|
|
for _, a := range artifacts {
|
|
if a.Expired {
|
|
continue
|
|
}
|
|
numValidArtifacts++
|
|
}
|
|
if numValidArtifacts == 0 {
|
|
return errors.New("no valid artifacts found to download")
|
|
}
|
|
|
|
wantPatterns := opts.FilePatterns
|
|
wantNames := opts.Names
|
|
if opts.DoPrompt {
|
|
artifactNames := set.NewStringSet()
|
|
for _, a := range artifacts {
|
|
if !a.Expired {
|
|
artifactNames.Add(a.Name)
|
|
}
|
|
}
|
|
options := artifactNames.ToSlice()
|
|
if len(options) > 10 {
|
|
options = options[:10]
|
|
}
|
|
var selected []int
|
|
if selected, err = opts.Prompter.MultiSelect("Select artifacts to download:", nil, options); err != nil {
|
|
return err
|
|
}
|
|
wantNames = []string{}
|
|
for _, x := range selected {
|
|
wantNames = append(wantNames, options[x])
|
|
}
|
|
if len(wantNames) == 0 {
|
|
return errors.New("no artifacts selected")
|
|
}
|
|
}
|
|
|
|
opts.IO.StartProgressIndicator()
|
|
defer opts.IO.StopProgressIndicator()
|
|
|
|
// track downloaded artifacts and avoid re-downloading any of the same name
|
|
downloaded := set.NewStringSet()
|
|
for _, a := range artifacts {
|
|
if a.Expired {
|
|
continue
|
|
}
|
|
if downloaded.Contains(a.Name) {
|
|
continue
|
|
}
|
|
if len(wantNames) > 0 || len(wantPatterns) > 0 {
|
|
if !matchAnyName(wantNames, a.Name) && !matchAnyPattern(wantPatterns, a.Name) {
|
|
continue
|
|
}
|
|
}
|
|
destDir := opts.DestinationDir
|
|
if len(wantPatterns) != 0 || len(wantNames) != 1 {
|
|
destDir = filepath.Join(destDir, a.Name)
|
|
}
|
|
err := opts.Platform.Download(a.DownloadURL, destDir)
|
|
if err != nil {
|
|
return fmt.Errorf("error downloading %s: %w", a.Name, err)
|
|
}
|
|
downloaded.Add(a.Name)
|
|
}
|
|
|
|
if downloaded.Len() == 0 {
|
|
return errors.New("no artifact matches any of the names or patterns provided")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func matchAnyName(names []string, name string) bool {
|
|
for _, n := range names {
|
|
if name == n {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func matchAnyPattern(patterns []string, name string) bool {
|
|
for _, p := range patterns {
|
|
if isMatch, err := filepath.Match(p, name); err == nil && isMatch {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|