154 lines
3.4 KiB
Go
154 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
|
|
}
|