github.com/bitrise-io/go-steputils/v2@v2.0.0-alpha.30/cache/network/api.go (about) 1 package network 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "net/http" 9 "net/http/httputil" 10 "net/url" 11 "os" 12 "strings" 13 14 "github.com/bitrise-io/go-utils/v2/log" 15 "github.com/hashicorp/go-retryablehttp" 16 ) 17 18 const maxKeyLength = 512 19 const maxKeyCount = 8 20 21 type prepareUploadRequest struct { 22 CacheKey string `json:"cache_key"` 23 ArchiveFileName string `json:"archive_filename"` 24 ArchiveContentType string `json:"archive_content_type"` 25 ArchiveSizeInBytes int64 `json:"archive_size_in_bytes"` 26 } 27 28 type prepareUploadResponse struct { 29 ID string `json:"id"` 30 UploadMethod string `json:"method"` 31 UploadURL string `json:"url"` 32 UploadHeaders map[string]string `json:"headers"` 33 } 34 35 type acknowledgeResponse struct { 36 Message string `json:"message"` 37 Severity string `json:"severity"` 38 } 39 40 type restoreResponse struct { 41 URL string `json:"url"` 42 MatchedKey string `json:"matched_cache_key"` 43 } 44 45 type apiClient struct { 46 httpClient *retryablehttp.Client 47 baseURL string 48 accessToken string 49 logger log.Logger 50 } 51 52 func newAPIClient(client *retryablehttp.Client, baseURL string, accessToken string, logger log.Logger) apiClient { 53 return apiClient{ 54 httpClient: client, 55 baseURL: baseURL, 56 accessToken: accessToken, 57 logger: logger, 58 } 59 } 60 61 func (c apiClient) prepareUpload(requestBody prepareUploadRequest) (prepareUploadResponse, error) { 62 url := fmt.Sprintf("%s/upload", c.baseURL) 63 64 body, err := json.Marshal(requestBody) 65 if err != nil { 66 return prepareUploadResponse{}, err 67 } 68 69 req, err := retryablehttp.NewRequest(http.MethodPost, url, body) 70 if err != nil { 71 return prepareUploadResponse{}, err 72 } 73 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.accessToken)) 74 req.Header.Set("Content-type", "application/json") 75 76 resp, err := c.httpClient.Do(req) 77 if err != nil { 78 return prepareUploadResponse{}, err 79 } 80 defer func(body io.ReadCloser) { 81 err := body.Close() 82 if err != nil { 83 c.logger.Printf(err.Error()) 84 } 85 }(resp.Body) 86 87 if resp.StatusCode != http.StatusCreated { 88 return prepareUploadResponse{}, unwrapError(resp) 89 } 90 91 var response prepareUploadResponse 92 err = json.NewDecoder(resp.Body).Decode(&response) 93 if err != nil { 94 return prepareUploadResponse{}, err 95 } 96 97 return response, nil 98 } 99 100 func (c apiClient) uploadArchive(archivePath, uploadMethod, uploadURL string, headers map[string]string) error { 101 file, err := os.Open(archivePath) 102 if err != nil { 103 return err 104 } 105 106 req, err := retryablehttp.NewRequest(uploadMethod, uploadURL, file) 107 if err != nil { 108 return err 109 } 110 for k, v := range headers { 111 req.Header.Set(k, v) 112 } 113 114 // Add Content-Length header manually because retryablehttp doesn't do it automatically 115 fileInfo, err := os.Stat(archivePath) 116 if err != nil { 117 return err 118 } 119 req.Header.Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size())) 120 req.ContentLength = fileInfo.Size() 121 122 dump, err := httputil.DumpRequest(req.Request, false) 123 if err != nil { 124 c.logger.Warnf("error while dumping request: %s", err) 125 } 126 c.logger.Debugf("Request dump: %s", string(dump)) 127 128 resp, err := c.httpClient.Do(req) 129 if err != nil { 130 return err 131 } 132 defer func(body io.ReadCloser) { 133 err := body.Close() 134 if err != nil { 135 c.logger.Printf(err.Error()) 136 } 137 }(resp.Body) 138 139 dump, err = httputil.DumpResponse(resp, true) 140 if err != nil { 141 c.logger.Warnf("error while dumping response: %s", err) 142 } 143 c.logger.Debugf("Response dump: %s", string(dump)) 144 145 if resp.StatusCode != http.StatusOK { 146 return unwrapError(resp) 147 } 148 149 return nil 150 } 151 152 func (c apiClient) acknowledgeUpload(uploadID string) (acknowledgeResponse, error) { 153 url := fmt.Sprintf("%s/upload/%s/acknowledge", c.baseURL, uploadID) 154 155 req, err := retryablehttp.NewRequest(http.MethodPatch, url, nil) 156 if err != nil { 157 return acknowledgeResponse{}, err 158 } 159 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.accessToken)) 160 161 resp, err := c.httpClient.Do(req) 162 if err != nil { 163 return acknowledgeResponse{}, err 164 } 165 defer func(body io.ReadCloser) { 166 err := body.Close() 167 if err != nil { 168 c.logger.Printf(err.Error()) 169 } 170 }(resp.Body) 171 172 if resp.StatusCode != http.StatusOK { 173 return acknowledgeResponse{}, unwrapError(resp) 174 } 175 176 var response acknowledgeResponse 177 err = json.NewDecoder(resp.Body).Decode(&response) 178 if err != nil { 179 return acknowledgeResponse{}, err 180 } 181 return response, nil 182 } 183 184 func (c apiClient) restore(cacheKeys []string) (restoreResponse, error) { 185 keysInQuery, err := validateKeys(cacheKeys) 186 if err != nil { 187 return restoreResponse{}, err 188 } 189 apiURL := fmt.Sprintf("%s/restore?cache_keys=%s", c.baseURL, keysInQuery) 190 191 req, err := retryablehttp.NewRequest(http.MethodGet, apiURL, nil) 192 if err != nil { 193 return restoreResponse{}, err 194 } 195 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.accessToken)) 196 197 resp, err := c.httpClient.Do(req) 198 if err != nil { 199 return restoreResponse{}, err 200 } 201 defer func(body io.ReadCloser) { 202 err := body.Close() 203 if err != nil { 204 c.logger.Printf(err.Error()) 205 } 206 }(resp.Body) 207 208 if resp.StatusCode == http.StatusNotFound { 209 return restoreResponse{}, ErrCacheNotFound 210 } 211 if resp.StatusCode != http.StatusOK { 212 return restoreResponse{}, unwrapError(resp) 213 } 214 215 var response restoreResponse 216 err = json.NewDecoder(resp.Body).Decode(&response) 217 if err != nil { 218 return restoreResponse{}, err 219 } 220 221 return response, nil 222 } 223 224 func unwrapError(resp *http.Response) error { 225 errorResp, err := ioutil.ReadAll(resp.Body) 226 if err != nil { 227 return err 228 } 229 return fmt.Errorf("HTTP %d: %s", resp.StatusCode, errorResp) 230 } 231 232 func validateKeys(keys []string) (string, error) { 233 if len(keys) > maxKeyCount { 234 return "", fmt.Errorf("maximum number of keys is %d, %d provided", maxKeyCount, len(keys)) 235 } 236 truncatedKeys := make([]string, 0, len(keys)) 237 for _, key := range keys { 238 if strings.Contains(key, ",") { 239 return "", fmt.Errorf("commas are not allowed in keys (invalid key: %s)", key) 240 } 241 if len(key) > maxKeyLength { 242 truncatedKeys = append(truncatedKeys, key[:maxKeyLength]) 243 } else { 244 truncatedKeys = append(truncatedKeys, key) 245 } 246 } 247 248 return url.QueryEscape(strings.Join(truncatedKeys, ",")), nil 249 }