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 }