github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/release/download/download.go (about) 1 package download 2 3 import ( 4 "errors" 5 "io" 6 "net/http" 7 "os" 8 "path/filepath" 9 10 "github.com/MakeNowJust/heredoc" 11 "github.com/cli/cli/api" 12 "github.com/cli/cli/internal/ghrepo" 13 "github.com/cli/cli/pkg/cmd/release/shared" 14 "github.com/cli/cli/pkg/cmdutil" 15 "github.com/cli/cli/pkg/iostreams" 16 "github.com/spf13/cobra" 17 ) 18 19 type DownloadOptions struct { 20 HttpClient func() (*http.Client, error) 21 IO *iostreams.IOStreams 22 BaseRepo func() (ghrepo.Interface, error) 23 24 TagName string 25 FilePatterns []string 26 Destination string 27 28 // maximum number of simultaneous downloads 29 Concurrency int 30 } 31 32 func NewCmdDownload(f *cmdutil.Factory, runF func(*DownloadOptions) error) *cobra.Command { 33 opts := &DownloadOptions{ 34 IO: f.IOStreams, 35 HttpClient: f.HttpClient, 36 } 37 38 cmd := &cobra.Command{ 39 Use: "download [<tag>]", 40 Short: "Download release assets", 41 Long: heredoc.Doc(` 42 Download assets from a GitHub release. 43 44 Without an explicit tag name argument, assets are downloaded from the 45 latest release in the project. In this case, '--pattern' is required. 46 `), 47 Example: heredoc.Doc(` 48 # download all assets from a specific release 49 $ gh release download v1.2.3 50 51 # download only Debian packages for the latest release 52 $ gh release download --pattern '*.deb' 53 54 # specify multiple file patterns 55 $ gh release download -p '*.deb' -p '*.rpm' 56 `), 57 Args: cobra.MaximumNArgs(1), 58 RunE: func(cmd *cobra.Command, args []string) error { 59 // support `-R, --repo` override 60 opts.BaseRepo = f.BaseRepo 61 62 if len(args) == 0 { 63 if len(opts.FilePatterns) == 0 { 64 return &cmdutil.FlagError{Err: errors.New("the '--pattern' flag is required when downloading the latest release")} 65 } 66 } else { 67 opts.TagName = args[0] 68 } 69 70 opts.Concurrency = 5 71 72 if runF != nil { 73 return runF(opts) 74 } 75 return downloadRun(opts) 76 }, 77 } 78 79 cmd.Flags().StringVarP(&opts.Destination, "dir", "D", ".", "The directory to download files into") 80 cmd.Flags().StringArrayVarP(&opts.FilePatterns, "pattern", "p", nil, "Download only assets that match a glob pattern") 81 82 return cmd 83 } 84 85 func downloadRun(opts *DownloadOptions) error { 86 httpClient, err := opts.HttpClient() 87 if err != nil { 88 return err 89 } 90 91 baseRepo, err := opts.BaseRepo() 92 if err != nil { 93 return err 94 } 95 96 var release *shared.Release 97 98 if opts.TagName == "" { 99 release, err = shared.FetchLatestRelease(httpClient, baseRepo) 100 if err != nil { 101 return err 102 } 103 } else { 104 release, err = shared.FetchRelease(httpClient, baseRepo, opts.TagName) 105 if err != nil { 106 return err 107 } 108 } 109 110 var toDownload []shared.ReleaseAsset 111 for _, a := range release.Assets { 112 if len(opts.FilePatterns) > 0 && !matchAny(opts.FilePatterns, a.Name) { 113 continue 114 } 115 toDownload = append(toDownload, a) 116 } 117 118 if len(toDownload) == 0 { 119 if len(release.Assets) > 0 { 120 return errors.New("no assets match the file pattern") 121 } 122 return errors.New("no assets to download") 123 } 124 125 if opts.Destination != "." { 126 err := os.MkdirAll(opts.Destination, 0755) 127 if err != nil { 128 return err 129 } 130 } 131 132 opts.IO.StartProgressIndicator() 133 err = downloadAssets(httpClient, toDownload, opts.Destination, opts.Concurrency) 134 opts.IO.StopProgressIndicator() 135 return err 136 } 137 138 func matchAny(patterns []string, name string) bool { 139 for _, p := range patterns { 140 if isMatch, err := filepath.Match(p, name); err == nil && isMatch { 141 return true 142 } 143 } 144 return false 145 } 146 147 func downloadAssets(httpClient *http.Client, toDownload []shared.ReleaseAsset, destDir string, numWorkers int) error { 148 if numWorkers == 0 { 149 return errors.New("the number of concurrent workers needs to be greater than 0") 150 } 151 152 jobs := make(chan shared.ReleaseAsset, len(toDownload)) 153 results := make(chan error, len(toDownload)) 154 155 if len(toDownload) < numWorkers { 156 numWorkers = len(toDownload) 157 } 158 159 for w := 1; w <= numWorkers; w++ { 160 go func() { 161 for a := range jobs { 162 results <- downloadAsset(httpClient, a.APIURL, filepath.Join(destDir, a.Name)) 163 } 164 }() 165 } 166 167 for _, a := range toDownload { 168 jobs <- a 169 } 170 close(jobs) 171 172 var downloadError error 173 for i := 0; i < len(toDownload); i++ { 174 if err := <-results; err != nil { 175 downloadError = err 176 } 177 } 178 179 return downloadError 180 } 181 182 func downloadAsset(httpClient *http.Client, assetURL, destinationPath string) error { 183 req, err := http.NewRequest("GET", assetURL, nil) 184 if err != nil { 185 return err 186 } 187 188 req.Header.Set("Accept", "application/octet-stream") 189 190 resp, err := httpClient.Do(req) 191 if err != nil { 192 return err 193 } 194 defer resp.Body.Close() 195 196 if resp.StatusCode > 299 { 197 return api.HandleHTTPError(resp) 198 } 199 200 f, err := os.OpenFile(destinationPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) 201 if err != nil { 202 return err 203 } 204 defer f.Close() 205 206 _, err = io.Copy(f, resp.Body) 207 return err 208 }