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  }