forked from hswaw/hscloud
136 lines
3.7 KiB
Go
136 lines
3.7 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"crypto/sha256"
|
||
|
"encoding/hex"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/golang/glog"
|
||
|
"github.com/minio/minio-go/v7"
|
||
|
)
|
||
|
|
||
|
type service struct {
|
||
|
objectClient *minio.Client
|
||
|
objectBucket string
|
||
|
objectPrefix string
|
||
|
publicHandler http.Handler
|
||
|
}
|
||
|
|
||
|
func newService(objectClient *minio.Client, objectBucket, objectPrefix string) *service {
|
||
|
s := &service{
|
||
|
objectClient: objectClient,
|
||
|
objectBucket: objectBucket,
|
||
|
objectPrefix: objectPrefix,
|
||
|
}
|
||
|
mux := http.NewServeMux()
|
||
|
mux.HandleFunc("/", s.handlePublic)
|
||
|
s.publicHandler = mux
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
func (s *service) handlePublic(w http.ResponseWriter, r *http.Request) {
|
||
|
ctx := r.Context()
|
||
|
switch r.Method {
|
||
|
case "GET":
|
||
|
// Always allow GET access to cache.
|
||
|
case "PUT":
|
||
|
// Require authentication for cache writes.
|
||
|
// TODO(q3k): implement
|
||
|
default:
|
||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/"), "/")
|
||
|
if len(parts) != 2 {
|
||
|
http.NotFound(w, r)
|
||
|
return
|
||
|
}
|
||
|
switch parts[0] {
|
||
|
case "ac":
|
||
|
case "cas":
|
||
|
default:
|
||
|
http.NotFound(w, r)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if len(parts[1]) != 64 {
|
||
|
http.NotFound(w, r)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
cacheKey := fmt.Sprintf("%s%s/%s", s.objectPrefix, parts[0], parts[1])
|
||
|
glog.Infof("%s %s %s", r.RemoteAddr, r.Method, cacheKey)
|
||
|
|
||
|
if r.Method == "GET" {
|
||
|
obj, err := s.objectClient.GetObject(ctx, s.objectBucket, cacheKey, minio.GetObjectOptions{})
|
||
|
if err != nil {
|
||
|
glog.Errorf("GetObject(%s, %s): %v", s.objectBucket, cacheKey, err)
|
||
|
http.Error(w, "could not contact object store", http.StatusInternalServerError)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
_, err = obj.Stat()
|
||
|
// Minio-go doesn't seem to let us do this in any nicer way :/
|
||
|
if err != nil && err.Error() == "The specified key does not exist." {
|
||
|
http.NotFound(w, r)
|
||
|
return
|
||
|
} else if err != nil {
|
||
|
glog.Errorf("Stat(%s, %s): %v", s.objectBucket, cacheKey, err)
|
||
|
http.Error(w, "could not contact object store", http.StatusInternalServerError)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Stream object to client.
|
||
|
io.Copy(w, obj)
|
||
|
}
|
||
|
if r.Method == "PUT" {
|
||
|
// Buffer the file, as we need to check its sha256.
|
||
|
// TODO(q3k): check and limit body size.
|
||
|
data, err := ioutil.ReadAll(r.Body)
|
||
|
if err != nil {
|
||
|
glog.Errorf("ReadAll: %v", err)
|
||
|
return
|
||
|
}
|
||
|
hashBytes := sha256.Sum256(data)
|
||
|
hash := hex.EncodeToString(hashBytes[:])
|
||
|
// Bazel cache uploads always seem to use lowercase sha256
|
||
|
// representations.
|
||
|
if parts[0] == "cas" && hash != parts[1] {
|
||
|
glog.Warningf("%s: sent PUT for %s with invalid hash %s", r.RemoteAddr, cacheKey, hash)
|
||
|
// Don't tell the user anything - Bazel won't care, anyway, and us
|
||
|
// logging this is probably good enough for debugging purposes.
|
||
|
return
|
||
|
}
|
||
|
// If the file already exists in the cache, ignore it. S3 doesn't seem
|
||
|
// to give us an upload-if-missing functionality?
|
||
|
_, err = s.objectClient.StatObject(ctx, s.objectBucket, cacheKey, minio.StatObjectOptions{})
|
||
|
if err == nil {
|
||
|
// File already exists, return early.
|
||
|
// This might not fire in case we fail to retrieve the object for
|
||
|
// some reason other than its nonexistence, but an error will be
|
||
|
// served for this at PutObject later on.
|
||
|
return
|
||
|
}
|
||
|
|
||
|
buffer := bytes.NewBuffer(data)
|
||
|
_, err = s.objectClient.PutObject(ctx, s.objectBucket, cacheKey, buffer, int64(len(data)), minio.PutObjectOptions{
|
||
|
UserMetadata: map[string]string{
|
||
|
"remote-cache-origin": r.RemoteAddr,
|
||
|
},
|
||
|
})
|
||
|
if err != nil {
|
||
|
// Swallow the error. Can't do much for the bazel writer, anyway.
|
||
|
// Retrying here isn't easy, as we don't want to become a
|
||
|
// qeueue/buffer unless really needed.
|
||
|
glog.Errorf("%s: PUT %s failed: %v", r.RemoteAddr, cacheKey, err)
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|