@@ -2,7 +2,9 @@ package download
22
33import (
44 "errors"
5+ "fmt"
56 "io"
7+ "mime"
68 "net/http"
79 "os"
810 "path/filepath"
@@ -27,6 +29,8 @@ type DownloadOptions struct {
2729
2830 // maximum number of simultaneous downloads
2931 Concurrency int
32+
33+ ArchiveType string
3034}
3135
3236func NewCmdDownload (f * cmdutil.Factory , runF func (* DownloadOptions ) error ) * cobra.Command {
@@ -47,12 +51,15 @@ func NewCmdDownload(f *cmdutil.Factory, runF func(*DownloadOptions) error) *cobr
4751 Example : heredoc .Doc (`
4852 # download all assets from a specific release
4953 $ gh release download v1.2.3
50-
54+
5155 # download only Debian packages for the latest release
5256 $ gh release download --pattern '*.deb'
53-
57+
5458 # specify multiple file patterns
5559 $ gh release download -p '*.deb' -p '*.rpm'
60+
61+ # download the archive of the source code for a release
62+ $ gh release download v1.2.3 --archive=zip
5663 ` ),
5764 Args : cobra .MaximumNArgs (1 ),
5865 RunE : func (cmd * cobra.Command , args []string ) error {
@@ -67,6 +74,11 @@ func NewCmdDownload(f *cmdutil.Factory, runF func(*DownloadOptions) error) *cobr
6774 opts .TagName = args [0 ]
6875 }
6976
77+ // check archive type option validity
78+ if err := checkArchiveTypeOption (opts ); err != nil {
79+ return err
80+ }
81+
7082 opts .Concurrency = 5
7183
7284 if runF != nil {
@@ -78,10 +90,30 @@ func NewCmdDownload(f *cmdutil.Factory, runF func(*DownloadOptions) error) *cobr
7890
7991 cmd .Flags ().StringVarP (& opts .Destination , "dir" , "D" , "." , "The directory to download files into" )
8092 cmd .Flags ().StringArrayVarP (& opts .FilePatterns , "pattern" , "p" , nil , "Download only assets that match a glob pattern" )
93+ cmd .Flags ().StringVarP (& opts .ArchiveType , "archive" , "A" , "" , "Download the source code archive in the specified `format` (zip or tar.gz)" )
8194
8295 return cmd
8396}
8497
98+ func checkArchiveTypeOption (opts * DownloadOptions ) error {
99+ if len (opts .ArchiveType ) == 0 {
100+ return nil
101+ }
102+
103+ if err := cmdutil .MutuallyExclusive (
104+ "specify only one of '--pattern' or '--archive'" ,
105+ true , // ArchiveType len > 0
106+ len (opts .FilePatterns ) > 0 ,
107+ ); err != nil {
108+ return err
109+ }
110+
111+ if opts .ArchiveType != "zip" && opts .ArchiveType != "tar.gz" {
112+ return cmdutil .FlagErrorf ("the value for `--archive` must be one of \" zip\" or \" tar.gz\" " )
113+ }
114+ return nil
115+ }
116+
85117func downloadRun (opts * DownloadOptions ) error {
86118 httpClient , err := opts .HttpClient ()
87119 if err != nil {
@@ -93,8 +125,10 @@ func downloadRun(opts *DownloadOptions) error {
93125 return err
94126 }
95127
96- var release * shared.Release
128+ opts .IO .StartProgressIndicator ()
129+ defer opts .IO .StopProgressIndicator ()
97130
131+ var release * shared.Release
98132 if opts .TagName == "" {
99133 release , err = shared .FetchLatestRelease (httpClient , baseRepo )
100134 if err != nil {
@@ -108,11 +142,22 @@ func downloadRun(opts *DownloadOptions) error {
108142 }
109143
110144 var toDownload []shared.ReleaseAsset
111- for _ , a := range release .Assets {
112- if len (opts .FilePatterns ) > 0 && ! matchAny (opts .FilePatterns , a .Name ) {
113- continue
145+ isArchive := false
146+ if opts .ArchiveType != "" {
147+ var archiveURL = release .ZipballURL
148+ if opts .ArchiveType == "tar.gz" {
149+ archiveURL = release .TarballURL
150+ }
151+ // create pseudo-Asset with no name and pointing to ZipBallURL or TarBallURL
152+ toDownload = append (toDownload , shared.ReleaseAsset {APIURL : archiveURL })
153+ isArchive = true
154+ } else {
155+ for _ , a := range release .Assets {
156+ if len (opts .FilePatterns ) > 0 && ! matchAny (opts .FilePatterns , a .Name ) {
157+ continue
158+ }
159+ toDownload = append (toDownload , a )
114160 }
115- toDownload = append (toDownload , a )
116161 }
117162
118163 if len (toDownload ) == 0 {
@@ -129,10 +174,7 @@ func downloadRun(opts *DownloadOptions) error {
129174 }
130175 }
131176
132- opts .IO .StartProgressIndicator ()
133- err = downloadAssets (httpClient , toDownload , opts .Destination , opts .Concurrency )
134- opts .IO .StopProgressIndicator ()
135- return err
177+ return downloadAssets (httpClient , toDownload , opts .Destination , opts .Concurrency , isArchive )
136178}
137179
138180func matchAny (patterns []string , name string ) bool {
@@ -144,7 +186,7 @@ func matchAny(patterns []string, name string) bool {
144186 return false
145187}
146188
147- func downloadAssets (httpClient * http.Client , toDownload []shared.ReleaseAsset , destDir string , numWorkers int ) error {
189+ func downloadAssets (httpClient * http.Client , toDownload []shared.ReleaseAsset , destDir string , numWorkers int , isArchive bool ) error {
148190 if numWorkers == 0 {
149191 return errors .New ("the number of concurrent workers needs to be greater than 0" )
150192 }
@@ -159,7 +201,7 @@ func downloadAssets(httpClient *http.Client, toDownload []shared.ReleaseAsset, d
159201 for w := 1 ; w <= numWorkers ; w ++ {
160202 go func () {
161203 for a := range jobs {
162- results <- downloadAsset (httpClient , a .APIURL , filepath . Join ( destDir , a .Name ) )
204+ results <- downloadAsset (httpClient , a .APIURL , destDir , a .Name , isArchive )
163205 }
164206 }()
165207 }
@@ -179,13 +221,17 @@ func downloadAssets(httpClient *http.Client, toDownload []shared.ReleaseAsset, d
179221 return downloadError
180222}
181223
182- func downloadAsset (httpClient * http.Client , assetURL , destinationPath string ) error {
224+ func downloadAsset (httpClient * http.Client , assetURL , destinationDir string , fileName string , isArchive bool ) error {
183225 req , err := http .NewRequest ("GET" , assetURL , nil )
184226 if err != nil {
185227 return err
186228 }
187229
188230 req .Header .Set ("Accept" , "application/octet-stream" )
231+ // adding application/json to Accept header due to a bug in the zipball/tarball API endpoint that makes it mandatory
232+ if isArchive {
233+ req .Header .Set ("Accept" , "application/octet-stream, application/json" )
234+ }
189235
190236 resp , err := httpClient .Do (req )
191237 if err != nil {
@@ -197,6 +243,22 @@ func downloadAsset(httpClient *http.Client, assetURL, destinationPath string) er
197243 return api .HandleHTTPError (resp )
198244 }
199245
246+ var destinationPath = filepath .Join (destinationDir , fileName )
247+
248+ if len (fileName ) == 0 {
249+ contentDisposition := resp .Header .Get ("Content-Disposition" )
250+
251+ _ , params , err := mime .ParseMediaType (contentDisposition )
252+ if err != nil {
253+ return fmt .Errorf ("unable to parse file name of archive: %w" , err )
254+ }
255+ if serverFileName , ok := params ["filename" ]; ok {
256+ destinationPath = filepath .Join (destinationDir , serverFileName )
257+ } else {
258+ return errors .New ("unable to determine file name of archive" )
259+ }
260+ }
261+
200262 f , err := os .OpenFile (destinationPath , os .O_WRONLY | os .O_CREATE | os .O_EXCL , 0644 )
201263 if err != nil {
202264 return err
0 commit comments