github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/release/shared/upload.go (about)

     1  package shared
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"io"
     7  	"io/ioutil"
     8  	"mime"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"path"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/cli/cli/api"
    17  )
    18  
    19  type AssetForUpload struct {
    20  	Name  string
    21  	Label string
    22  
    23  	Size     int64
    24  	MIMEType string
    25  	Open     func() (io.ReadCloser, error)
    26  
    27  	ExistingURL string
    28  }
    29  
    30  func AssetsFromArgs(args []string) (assets []*AssetForUpload, err error) {
    31  	for _, arg := range args {
    32  		var label string
    33  		fn := arg
    34  		if idx := strings.IndexRune(arg, '#'); idx > 0 {
    35  			fn = arg[0:idx]
    36  			label = arg[idx+1:]
    37  		}
    38  
    39  		var fi os.FileInfo
    40  		fi, err = os.Stat(fn)
    41  		if err != nil {
    42  			return
    43  		}
    44  
    45  		assets = append(assets, &AssetForUpload{
    46  			Open: func() (io.ReadCloser, error) {
    47  				return os.Open(fn)
    48  			},
    49  			Size:     fi.Size(),
    50  			Name:     fi.Name(),
    51  			Label:    label,
    52  			MIMEType: typeForFilename(fi.Name()),
    53  		})
    54  	}
    55  	return
    56  }
    57  
    58  func typeForFilename(fn string) string {
    59  	ext := fileExt(fn)
    60  	switch ext {
    61  	case ".zip":
    62  		return "application/zip"
    63  	case ".js":
    64  		return "application/javascript"
    65  	case ".tar":
    66  		return "application/x-tar"
    67  	case ".tgz", ".tar.gz":
    68  		return "application/x-gtar"
    69  	case ".bz2":
    70  		return "application/x-bzip2"
    71  	case ".dmg":
    72  		return "application/x-apple-diskimage"
    73  	case ".rpm":
    74  		return "application/x-rpm"
    75  	case ".deb":
    76  		return "application/x-debian-package"
    77  	}
    78  
    79  	t := mime.TypeByExtension(ext)
    80  	if t == "" {
    81  		return "application/octet-stream"
    82  	}
    83  	return t
    84  }
    85  
    86  func fileExt(fn string) string {
    87  	fn = strings.ToLower(fn)
    88  	if strings.HasSuffix(fn, ".tar.gz") {
    89  		return ".tar.gz"
    90  	}
    91  	return path.Ext(fn)
    92  }
    93  
    94  func ConcurrentUpload(httpClient *http.Client, uploadURL string, numWorkers int, assets []*AssetForUpload) error {
    95  	if numWorkers == 0 {
    96  		return errors.New("the number of concurrent workers needs to be greater than 0")
    97  	}
    98  
    99  	jobs := make(chan AssetForUpload, len(assets))
   100  	results := make(chan error, len(assets))
   101  
   102  	if len(assets) < numWorkers {
   103  		numWorkers = len(assets)
   104  	}
   105  
   106  	for w := 1; w <= numWorkers; w++ {
   107  		go func() {
   108  			for a := range jobs {
   109  				results <- uploadWithDelete(httpClient, uploadURL, a)
   110  			}
   111  		}()
   112  	}
   113  
   114  	for _, a := range assets {
   115  		jobs <- *a
   116  	}
   117  	close(jobs)
   118  
   119  	var uploadError error
   120  	for i := 0; i < len(assets); i++ {
   121  		if err := <-results; err != nil {
   122  			uploadError = err
   123  		}
   124  	}
   125  	return uploadError
   126  }
   127  
   128  const maxRetries = 3
   129  
   130  func uploadWithDelete(httpClient *http.Client, uploadURL string, a AssetForUpload) error {
   131  	if a.ExistingURL != "" {
   132  		err := deleteAsset(httpClient, a.ExistingURL)
   133  		if err != nil {
   134  			return err
   135  		}
   136  	}
   137  
   138  	retries := 0
   139  	for {
   140  		var httpError api.HTTPError
   141  		_, err := uploadAsset(httpClient, uploadURL, a)
   142  		// retry upload several times upon receiving HTTP 5xx
   143  		if err == nil || !errors.As(err, &httpError) || httpError.StatusCode < 500 || retries < maxRetries {
   144  			return err
   145  		}
   146  		retries++
   147  		time.Sleep(time.Duration(retries) * time.Second)
   148  	}
   149  }
   150  
   151  func uploadAsset(httpClient *http.Client, uploadURL string, asset AssetForUpload) (*ReleaseAsset, error) {
   152  	u, err := url.Parse(uploadURL)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  	params := u.Query()
   157  	params.Set("name", asset.Name)
   158  	params.Set("label", asset.Label)
   159  	u.RawQuery = params.Encode()
   160  
   161  	f, err := asset.Open()
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  	defer f.Close()
   166  
   167  	req, err := http.NewRequest("POST", u.String(), f)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  	req.ContentLength = asset.Size
   172  	req.Header.Set("Content-Type", asset.MIMEType)
   173  	req.GetBody = asset.Open
   174  
   175  	resp, err := httpClient.Do(req)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  	defer resp.Body.Close()
   180  
   181  	success := resp.StatusCode >= 200 && resp.StatusCode < 300
   182  	if !success {
   183  		return nil, api.HandleHTTPError(resp)
   184  	}
   185  
   186  	b, err := ioutil.ReadAll(resp.Body)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  
   191  	var newAsset ReleaseAsset
   192  	err = json.Unmarshal(b, &newAsset)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	return &newAsset, nil
   198  }
   199  
   200  func deleteAsset(httpClient *http.Client, assetURL string) error {
   201  	req, err := http.NewRequest("DELETE", assetURL, nil)
   202  	if err != nil {
   203  		return err
   204  	}
   205  
   206  	resp, err := httpClient.Do(req)
   207  	if err != nil {
   208  		return err
   209  	}
   210  	defer resp.Body.Close()
   211  
   212  	success := resp.StatusCode >= 200 && resp.StatusCode < 300
   213  	if !success {
   214  		return api.HandleHTTPError(resp)
   215  	}
   216  
   217  	return nil
   218  }