caching-proxy/main.go
2023-07-19 11:44:34 -07:00

155 lines
3.4 KiB
Go

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
}