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