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  }