blob: 7ae2eaa6014bda56b74788553c6994931d5d5c60 [file] [log] [blame]
package main
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/dimchansky/utfbom"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
)
var token = os.Getenv("ACTIONS_RUNTIME_TOKEN")
var httpClient = &http.Client{}
type GetCacheResponse struct {
ArchiveLocation string `json:"archiveLocation"`
}
type ReserveCacheResponse struct {
CacheId int `json:"cacheId"`
}
func main() {
http.HandleFunc("/", handler)
log.Println("Starting http cache server on port 12321")
err := http.ListenAndServe(":12321", nil)
if err != nil {
log.Printf("Failed to start server: %v\n", err)
}
}
func handler(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
key := r.URL.Path
if key[0] == '/' {
key = key[1:]
}
if key == "" {
_, _ = w.Write([]byte("HTTP Cache is running!"))
w.WriteHeader(200)
} else if r.Method == "GET" {
downloadCache(w, r, key)
} else if r.Method == "HEAD" {
checkCacheExists(w, key)
} else if r.Method == "POST" {
uploadCache(w, r, key)
} else if r.Method == "PUT" {
uploadCache(w, r, key)
}
duration := time.Since(startTime)
log.Printf("Served %s request for %s key in %dms\n", r.Method, key, duration.Milliseconds())
}
func downloadCache(w http.ResponseWriter, r *http.Request, key string) {
location, err := findCacheLocation(key)
if err != nil {
log.Printf("Failed to download key %s: %v\n", key, err)
w.Write([]byte(err.Error()))
w.WriteHeader(500)
return
}
if location == "" {
log.Printf("Cache %s not found\n", key)
w.WriteHeader(404)
return
}
proxyDownloadFromURL(w, location)
}
func proxyDownloadFromURL(w http.ResponseWriter, url string) {
resp, err := http.Get(url)
if err != nil {
log.Printf("Proxying cache %s failed: %v\n", url, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
successfulStatus := 100 <= resp.StatusCode && resp.StatusCode < 300
if !successfulStatus {
log.Printf("Proxying cache %s failed with %d status\n", url, resp.StatusCode)
w.WriteHeader(resp.StatusCode)
return
}
_, err = io.Copy(w, resp.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
}
func checkCacheExists(w http.ResponseWriter, key string) {
location, err := findCacheLocation(key)
if location == "" || err != nil {
log.Printf("Cache %s not found\n", key)
w.WriteHeader(404)
return
}
w.WriteHeader(200)
}
func findCacheLocation(key string) (string, error) {
resource := fmt.Sprintf("cache?keys=%s&version=%s", key, calculateSHA256(key))
requestUrl := getCacheApiUrl(resource)
request, _ := http.NewRequest("GET", requestUrl, nil)
request.Header.Set("Authorization", "Bearer "+token)
request.Header.Set("User-Agent", "actions/cache")
request.Header.Set("Accept", "application/json;api-version=6.0-preview.1")
request.Header.Set("Accept-Charset", "utf-8")
response, err := httpClient.Do(request)
if err != nil {
return "", err
}
if response.StatusCode == 404 {
return "", nil
}
if response.StatusCode == 204 {
// no content
return "", nil
}
defer response.Body.Close()
bodyBytes, err := ioutil.ReadAll(utfbom.SkipOnly(response.Body))
if response.StatusCode >= 400 {
log.Printf("Failed to download key %s: %d %s\n", key, response.StatusCode, string(bodyBytes))
return "", fmt.Errorf("failed to get location: %d", response.StatusCode)
}
cacheResponse := GetCacheResponse{}
err = json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(&cacheResponse)
if err != nil {
log.Println(string(bodyBytes))
return "", err
}
if cacheResponse.ArchiveLocation == "" {
log.Println(string(bodyBytes))
}
return cacheResponse.ArchiveLocation, nil
}
func uploadCache(w http.ResponseWriter, r *http.Request, key string) {
cacheId, err := reserveCache(key)
if err != nil {
log.Printf("Failed to reserve upload for cache key %s: %v\n", key, err)
w.Write([]byte(err.Error()))
w.WriteHeader(500)
return
}
err = uploadCacheFromReader(cacheId, r.Body)
if err != nil {
log.Printf("Failed to upload cache %s: %v\n", key, err)
w.Write([]byte(err.Error()))
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusCreated)
}
func uploadCacheFromReader(cacheId int, body io.Reader) error {
resourceUrl := getCacheApiUrl(fmt.Sprintf("caches/%d", cacheId))
readBufferSize := int(1024 * 1024)
readBuffer := make([]byte, readBufferSize)
bufferedBodyReader := bufio.NewReaderSize(body, readBufferSize)
bytesUploaded := 0
for {
n, err := bufferedBodyReader.Read(readBuffer)
if n > 0 {
uploadCacheChunk(resourceUrl, readBuffer[:n], bytesUploaded)
bytesUploaded += n
}
if err == io.EOF || n == 0 {
break
}
if err != nil {
return err
}
}
return commitCache(cacheId, bytesUploaded)
}
func uploadCacheChunk(url string, data []byte, position int) error {
request, _ := http.NewRequest("PATCH", url, bytes.NewBuffer(data))
request.Header.Set("Authorization", "Bearer "+token)
request.Header.Set("User-Agent", "actions/cache")
request.Header.Set("Content-Type", "application/octet-stream")
request.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/*", position, position+len(data)-1))
request.Header.Set("Accept", "application/json;api-version=6.0-preview.1")
request.Header.Set("Accept-Charset", "utf-8")
response, _ := httpClient.Do(request)
if response.StatusCode != 204 {
defer response.Body.Close()
bodyBytes, _ := ioutil.ReadAll(response.Body)
log.Printf("Failed to upload cache chunk: %s\n", string(bodyBytes))
log.Println(string(bodyBytes))
return fmt.Errorf("failed to upload chunk with status %d: %s", response.StatusCode, string(bodyBytes))
}
return nil
}
func commitCache(cacheId int, size int) error {
url := getCacheApiUrl(fmt.Sprintf("caches/%d", cacheId))
requestBody := fmt.Sprintf("{ \"size\": \"%d\" }", size)
request, _ := http.NewRequest("POST", url, bytes.NewBufferString(requestBody))
request.Header.Set("Authorization", "Bearer "+token)
request.Header.Set("User-Agent", "actions/cache")
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Accept", "application/json;api-version=6.0-preview.1")
request.Header.Set("Accept-Charset", "utf-8")
response, _ := httpClient.Do(request)
if response.StatusCode != 204 {
defer response.Body.Close()
bodyBytes, _ := ioutil.ReadAll(response.Body)
log.Printf("Failed to commit cache %d: %s\n", cacheId, string(bodyBytes))
return fmt.Errorf("failed to commit cache %d with status %d: %s", cacheId, response.StatusCode, string(bodyBytes))
}
return nil
}
func reserveCache(key string) (int, error) {
requestUrl := getCacheApiUrl("caches")
requestBody := fmt.Sprintf("{ \"key\": \"%s\", \"version\": \"%s\" }", key, calculateSHA256(key))
request, _ := http.NewRequest("POST", requestUrl, bytes.NewBufferString(requestBody))
request.Header.Set("Authorization", "Bearer "+token)
request.Header.Set("User-Agent", "actions/cache")
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Accept", "application/json;api-version=6.0-preview.1")
request.Header.Set("Accept-Charset", "utf-8")
response, err := httpClient.Do(request)
if err != nil {
return -1, err
}
defer response.Body.Close()
bodyBytes, err := ioutil.ReadAll(utfbom.SkipOnly(response.Body))
if response.StatusCode >= 400 {
return -1, fmt.Errorf("failed to reserve cache: %d", response.StatusCode)
}
var cacheResponse ReserveCacheResponse
err = json.Unmarshal(bodyBytes, &cacheResponse)
if err != nil {
return -1, err
}
return cacheResponse.CacheId, nil
}
func calculateSHA256(s string) string {
h := sha256.New()
h.Write([]byte(s))
return hex.EncodeToString(h.Sum(nil))
}
func getCacheApiUrl(resource string) string {
baseUrl := strings.ReplaceAll(os.Getenv("ACTIONS_CACHE_URL"), "pipelines", "artifactcache")
return baseUrl + "_apis/artifactcache/" + resource
}