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 } } }