github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/run/download/download.go (about)

     1  package download
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"path/filepath"
     7  
     8  	"github.com/AlecAivazis/survey/v2"
     9  	"github.com/MakeNowJust/heredoc"
    10  	"github.com/ungtb10d/cli/v2/pkg/cmd/run/shared"
    11  	"github.com/ungtb10d/cli/v2/pkg/cmdutil"
    12  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    13  	"github.com/ungtb10d/cli/v2/pkg/prompt"
    14  	"github.com/ungtb10d/cli/v2/pkg/set"
    15  	"github.com/spf13/cobra"
    16  )
    17  
    18  type DownloadOptions struct {
    19  	IO       *iostreams.IOStreams
    20  	Platform platform
    21  	Prompter prompter
    22  
    23  	DoPrompt       bool
    24  	RunID          string
    25  	DestinationDir string
    26  	Names          []string
    27  	FilePatterns   []string
    28  }
    29  
    30  type platform interface {
    31  	List(runID string) ([]shared.Artifact, error)
    32  	Download(url string, dir string) error
    33  }
    34  type prompter interface {
    35  	Prompt(message string, options []string, result interface{}) error
    36  }
    37  
    38  func NewCmdDownload(f *cmdutil.Factory, runF func(*DownloadOptions) error) *cobra.Command {
    39  	opts := &DownloadOptions{
    40  		IO: f.IOStreams,
    41  	}
    42  
    43  	cmd := &cobra.Command{
    44  		Use:   "download [<run-id>]",
    45  		Short: "Download artifacts generated by a workflow run",
    46  		Long: heredoc.Doc(`
    47  			Download artifacts generated by a GitHub Actions workflow run.
    48  
    49  			The contents of each artifact will be extracted under separate directories based on
    50  			the artifact name. If only a single artifact is specified, it will be extracted into
    51  			the current directory.
    52  		`),
    53  		Args: cobra.MaximumNArgs(1),
    54  		Example: heredoc.Doc(`
    55  		  # Download all artifacts generated by a workflow run
    56  		  $ gh run download <run-id>
    57  
    58  		  # Download a specific artifact within a run
    59  		  $ gh run download <run-id> -n <name>
    60  
    61  		  # Download specific artifacts across all runs in a repository
    62  		  $ gh run download -n <name1> -n <name2>
    63  
    64  		  # Select artifacts to download interactively
    65  		  $ gh run download
    66  		`),
    67  		RunE: func(cmd *cobra.Command, args []string) error {
    68  			if len(args) > 0 {
    69  				opts.RunID = args[0]
    70  			} else if len(opts.Names) == 0 &&
    71  				len(opts.FilePatterns) == 0 &&
    72  				opts.IO.CanPrompt() {
    73  				opts.DoPrompt = true
    74  			}
    75  			// support `-R, --repo` override
    76  			baseRepo, err := f.BaseRepo()
    77  			if err != nil {
    78  				return err
    79  			}
    80  			httpClient, err := f.HttpClient()
    81  			if err != nil {
    82  				return err
    83  			}
    84  			opts.Platform = &apiPlatform{
    85  				client: httpClient,
    86  				repo:   baseRepo,
    87  			}
    88  			opts.Prompter = &surveyPrompter{}
    89  
    90  			if runF != nil {
    91  				return runF(opts)
    92  			}
    93  			return runDownload(opts)
    94  		},
    95  	}
    96  
    97  	cmd.Flags().StringVarP(&opts.DestinationDir, "dir", "D", ".", "The directory to download artifacts into")
    98  	cmd.Flags().StringArrayVarP(&opts.Names, "name", "n", nil, "Download artifacts that match any of the given names")
    99  	cmd.Flags().StringArrayVarP(&opts.FilePatterns, "pattern", "p", nil, "Download artifacts that match a glob pattern")
   100  
   101  	return cmd
   102  }
   103  
   104  func runDownload(opts *DownloadOptions) error {
   105  	opts.IO.StartProgressIndicator()
   106  	artifacts, err := opts.Platform.List(opts.RunID)
   107  	opts.IO.StopProgressIndicator()
   108  	if err != nil {
   109  		return fmt.Errorf("error fetching artifacts: %w", err)
   110  	}
   111  
   112  	numValidArtifacts := 0
   113  	for _, a := range artifacts {
   114  		if a.Expired {
   115  			continue
   116  		}
   117  		numValidArtifacts++
   118  	}
   119  	if numValidArtifacts == 0 {
   120  		return errors.New("no valid artifacts found to download")
   121  	}
   122  
   123  	wantPatterns := opts.FilePatterns
   124  	wantNames := opts.Names
   125  	if opts.DoPrompt {
   126  		artifactNames := set.NewStringSet()
   127  		for _, a := range artifacts {
   128  			if !a.Expired {
   129  				artifactNames.Add(a.Name)
   130  			}
   131  		}
   132  		options := artifactNames.ToSlice()
   133  		if len(options) > 10 {
   134  			options = options[:10]
   135  		}
   136  		err := opts.Prompter.Prompt("Select artifacts to download:", options, &wantNames)
   137  		if err != nil {
   138  			return err
   139  		}
   140  		if len(wantNames) == 0 {
   141  			return errors.New("no artifacts selected")
   142  		}
   143  	}
   144  
   145  	opts.IO.StartProgressIndicator()
   146  	defer opts.IO.StopProgressIndicator()
   147  
   148  	// track downloaded artifacts and avoid re-downloading any of the same name
   149  	downloaded := set.NewStringSet()
   150  	for _, a := range artifacts {
   151  		if a.Expired {
   152  			continue
   153  		}
   154  		if downloaded.Contains(a.Name) {
   155  			continue
   156  		}
   157  		if len(wantNames) > 0 || len(wantPatterns) > 0 {
   158  			if !matchAnyName(wantNames, a.Name) && !matchAnyPattern(wantPatterns, a.Name) {
   159  				continue
   160  			}
   161  		}
   162  		destDir := opts.DestinationDir
   163  		if len(wantPatterns) != 0 || len(wantNames) != 1 {
   164  			destDir = filepath.Join(destDir, a.Name)
   165  		}
   166  		err := opts.Platform.Download(a.DownloadURL, destDir)
   167  		if err != nil {
   168  			return fmt.Errorf("error downloading %s: %w", a.Name, err)
   169  		}
   170  		downloaded.Add(a.Name)
   171  	}
   172  
   173  	if downloaded.Len() == 0 {
   174  		return errors.New("no artifact matches any of the names or patterns provided")
   175  	}
   176  
   177  	return nil
   178  }
   179  
   180  func matchAnyName(names []string, name string) bool {
   181  	for _, n := range names {
   182  		if name == n {
   183  			return true
   184  		}
   185  	}
   186  	return false
   187  }
   188  
   189  func matchAnyPattern(patterns []string, name string) bool {
   190  	for _, p := range patterns {
   191  		if isMatch, err := filepath.Match(p, name); err == nil && isMatch {
   192  			return true
   193  		}
   194  	}
   195  	return false
   196  }
   197  
   198  type surveyPrompter struct{}
   199  
   200  func (sp *surveyPrompter) Prompt(message string, options []string, result interface{}) error {
   201  	//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
   202  	return prompt.SurveyAskOne(&survey.MultiSelect{
   203  		Message: message,
   204  		Options: options,
   205  	}, result)
   206  }