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 }