Object Storage Proxy behind nginx


(Levi Durfee) #1

I am working on a private image hosting site. I need it to scale, so I’ve switched to an S3 compatible Object Storage service. The bucket is private. The main web app is built with Laravel. And I would like to proxy the image requests with the below code. This Go app will sit behind nginx.

Would someone mind taking a look at the code below and letting me know if there is anything I can do to increase performance. Or if you have any suggestions, I am open to them. Thanks!

package goos

import (
	"io"
	"net/http"
	"net/url"
	"strconv"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
)

// Logger interface
type Logger interface {
	Print(string)
}

// Goos holds the information to connect to an S3 compatible Object Storage Service
type Goos struct {
	KeyID    string
	Secret   string
	Endpoint string
	Region   string
	Bucket   string
	Logger
}

func (g *Goos) session() *session.Session {
	s3Config := &aws.Config{
		Credentials: credentials.NewStaticCredentials(g.KeyID, g.Secret, ""),
		Endpoint:    aws.String(g.Endpoint),
		Region:      aws.String(g.Region),
	}
	sess := session.New(s3Config)

	return sess
}

// Handler will return the handler we need
func (g *Goos) Handler() http.HandlerFunc {
	svc := s3.New(g.session())
	fn := func(w http.ResponseWriter, r *http.Request) {

		cf := r.Header.Get("X-Real-Ip")
		ip := r.Header.Get("X-Forwarded-For")

		if r.URL.String() == "/" {
			g.logMessage(cf, ip, r.URL.String(), "/")
			notFound(w)
			return
		}

		url, err := url.QueryUnescape(r.URL.String())
		if err != nil {
			g.logMessage(cf, ip, r.URL.String(), "404")
			notFound(w)
			return
		}

		input := &s3.GetObjectInput{
			Bucket: aws.String(g.Bucket),
			Key:    aws.String(url),
		}
		result, err := svc.GetObject(input)
		if err != nil {
			g.logMessage(cf, ip, url, "404")
			notFound(w)
			return
		}
		defer result.Body.Close()

		w.Header().Set("Content-Length", strconv.FormatInt(*result.ContentLength, 10))
		w.Header().Set("Last-Modified", result.LastModified.Format("Mon, 02 Jan 2006 15:04:05 MST"))
		w.Header().Set("Expires", time.Now().AddDate(60, 0, 0).Format(http.TimeFormat))
		w.Header().Set("Cache-Control", "max-age:290304000")
		w.Header().Set("Etag", *result.ETag)

		_, err = io.Copy(w, result.Body)
		if err != nil {
			g.logMessage(cf, ip, url, "500")
			notFound(w)
			return
		}

		g.logMessage(cf, ip, url, "200")
	}

	return http.HandlerFunc(fn)
}

func notFound(w http.ResponseWriter) {
	w.Header().Set("Cache-Control", "max-age:0, private")
	w.WriteHeader(http.StatusNotFound)
	w.Write([]byte("Not Found."))
}

func (g *Goos) logMessage(cf string, ip string, url string, status string) {
	if g.Logger == nil {
		return
	}
	g.Print("[" + cf + "|" + ip + "] " + "[" + url + "] " + "[" + status + "]")
}