package main import ( "context" "encoding/json" "fmt" "io" "log" "net/http" "os" "strings" minio "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) type Config struct { S3 S3Config `json:"s3"` Bind string `json:"bind"` } type S3Config struct { Endpoint string `json:"endpoint"` AccessKeyID string `json:"access_key_id"` SecretAccessKey string `json:"secret_access_key"` Bucket string `json:"bucket"` } var ( config = Config{ Bind: ":8080", } minioClient *minio.Client ) func loadconfig(filename string) { f, err := os.Open(filename) if err != nil { log.Fatal("error reading", filename, ":", err) } defer f.Close() if err := json.NewDecoder(f).Decode(&config); err != nil { log.Fatal("error parsing", filename, ":", err) } } func main() { loadconfig("caching-proxy.json") initializeMinioClient() log.Println("starting listener on", config.Bind) if err := http.ListenAndServe(config.Bind, proxyhandler{}); err != nil { log.Fatal("error starting http listener:", err) } } func initializeMinioClient() { var err error minioClient, err = minio.New(config.S3.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(config.S3.AccessKeyID, config.S3.SecretAccessKey, ""), Secure: true, }) if err != nil { log.Fatalln("error creating minio client:", err) } } type proxyhandler struct{} func (proxyhandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { requestPath := strings.SplitN(r.URL.Path, "/", 3) if len(requestPath) != 3 || requestPath[2] == "" { w.WriteHeader(http.StatusNotFound) w.Write([]byte("not found")) log.Println("invalid request, 404", r.URL.Path) return } get(requestPath[1]+"/"+requestPath[2], w) } func get(path string, w http.ResponseWriter) { source, err := minioClient.GetObject(context.Background(), config.S3.Bucket, path, minio.GetObjectOptions{}) if err != nil { log.Println("error getting cached object for", path, ":", err) w.WriteHeader(500) return } stat, err := source.Stat() if err != nil { resp := minio.ToErrorResponse(err) if resp.StatusCode == http.StatusNotFound { download(path, w) return } log.Println("error getting cached file", path, ":", resp.Code) w.WriteHeader(500) return } w.Header().Add("Content-Length", fmt.Sprint(stat.Size)) w.Header().Add("Content-Type", stat.ContentType) log.Println("cache hit: ", path) if _, err := io.Copy(w, source); err != nil { log.Println("error serving cached file:", err) return } } func download(path string, w http.ResponseWriter) { resp, err := http.Get("https://" + path) if err != nil { log.Println("error getting", path, ":", err) return } defer resp.Body.Close() w.Header().Add("Content-Length", fmt.Sprint(resp.ContentLength)) w.WriteHeader(resp.StatusCode) if resp.StatusCode > 299 { log.Println("upstream non-ok:", resp.Status, ":", path) io.Copy(w, resp.Body) return } reader := io.TeeReader(resp.Body, infallableWriter{Next: w}) _, err = minioClient.PutObject(resp.Request.Context(), config.S3.Bucket, path, reader, resp.ContentLength, minio.PutObjectOptions{}) if err != nil { log.Println("error PUTting", path, ":", err) } log.Println("cache miss:", path) } type infallableWriter struct { Next io.Writer } func (i infallableWriter) Write(data []byte) (int, error) { n, err := i.Next.Write(data) if err != nil { return len(data), nil } return n, nil }