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 }