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