repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

commit
28b4a62
parent
10035cd
author
Mac Chaffee
date
2024-12-21 16:58:34 -0500 EST
feat(pgs): forward etag and last-modified headers from imgproxy
7 files changed,  +76, -42
M .env.example
+2, -0
1@@ -22,6 +22,8 @@ IMGPROXY_URL=http://imgproxy:8080
2 IMGPROXY_ALLOWED_SOURCES=s3://,local://
3 IMGPROXY_LOCAL_FILESYSTEM_ROOT=/storage
4 IMGPROXY_USE_S3=true
5+IMGPROXY_USE_LAST_MODIFIED=true
6+IMGPROXY_USE_ETAG=true
7 IMGPROXY_S3_ENDPOINT=$MINIO_URL
8 IMGPROXY_KEY=6465616462656566 # deadbeef
9 IMGPROXY_SALT=6465616462656566 # deadbeef
M pgs/web_asset_handler.go
+6, -13
 1@@ -133,18 +133,13 @@ func (h *ApiAssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 2 		}
 3 
 4 		attempts = append(attempts, fp.Filepath)
 5-		mimeType := storage.GetMimeType(fp.Filepath)
 6 		logger = logger.With("filename", fp.Filepath)
 7 		var c io.ReadCloser
 8-		if strings.HasPrefix(mimeType, "image/") {
 9-			c, contentType, err = h.Storage.ServeObject(
10-				h.Bucket,
11-				fp.Filepath,
12-				h.ImgProcessOpts,
13-			)
14-		} else {
15-			c, info, err = h.Storage.GetObject(h.Bucket, fp.Filepath)
16-		}
17+		c, _, err = h.Storage.ServeObject(
18+			h.Bucket,
19+			fp.Filepath,
20+			h.ImgProcessOpts,
21+		)
22 		if err == nil {
23 			contents = c
24 			assetFilepath = fp.Filepath
25@@ -164,9 +159,7 @@ func (h *ApiAssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
26 	}
27 	defer contents.Close()
28 
29-	if contentType == "" {
30-		contentType = storage.GetMimeType(assetFilepath)
31-	}
32+	contentType = info.Metadata.Get("content-type")
33 
34 	var headers []*HeaderRule
35 	headersFp, headersInfo, err := h.Storage.GetObject(h.Bucket, filepath.Join(h.ProjectDir, "_headers"))
M shared/storage/fs.go
+18, -9
 1@@ -5,6 +5,7 @@ import (
 2 	"io"
 3 	"os"
 4 	"path/filepath"
 5+	"strings"
 6 
 7 	sst "github.com/picosh/pobj/storage"
 8 )
 9@@ -21,14 +22,22 @@ func NewStorageFS(dir string) (*StorageFS, error) {
10 	return &StorageFS{st}, nil
11 }
12 
13-func (s *StorageFS) ServeObject(bucket sst.Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, string, error) {
14-	if opts == nil || os.Getenv("IMGPROXY_URL") == "" {
15-		contentType := GetMimeType(fpath)
16-		rc, _, err := s.GetObject(bucket, fpath)
17-		return rc, contentType, err
18+func (s *StorageFS) ServeObject(bucket sst.Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, *sst.ObjectInfo, error) {
19+	var rc io.ReadCloser
20+	var info *sst.ObjectInfo
21+	var err error
22+	mimeType := GetMimeType(fpath)
23+	if !strings.HasPrefix(mimeType, "image/") || opts == nil || os.Getenv("IMGPROXY_URL") == "" {
24+		rc, info, err = s.GetObject(bucket, fpath)
25+		// StorageFS never returns a content-type.
26+		info.Metadata.Set("content-type", mimeType)
27+	} else {
28+		filePath := filepath.Join(bucket.Name, fpath)
29+		dataURL := fmt.Sprintf("s3://%s", filePath)
30+		rc, info, err = HandleProxy(dataURL, opts)
31 	}
32-
33-	filePath := filepath.Join(bucket.Path, fpath)
34-	dataURL := fmt.Sprintf("local://%s", filePath)
35-	return HandleProxy(dataURL, opts)
36+	if err != nil {
37+		return nil, nil, err
38+	}
39+	return rc, info, err
40 }
M shared/storage/memory.go
+5, -3
 1@@ -18,7 +18,9 @@ func NewStorageMemory(sto map[string]map[string]string) (*StorageMemory, error)
 2 	return &StorageMemory{st}, nil
 3 }
 4 
 5-func (s *StorageMemory) ServeObject(bucket sst.Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, string, error) {
 6-	obj, _, err := s.GetObject(bucket, fpath)
 7-	return obj, GetMimeType(fpath), err
 8+func (s *StorageMemory) ServeObject(bucket sst.Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, *sst.ObjectInfo, error) {
 9+	obj, info, err := s.GetObject(bucket, fpath)
10+	mimeType := GetMimeType(fpath)
11+	info.Metadata.Set("content-type", mimeType)
12+	return obj, info, err
13 }
M shared/storage/minio.go
+18, -9
 1@@ -5,6 +5,7 @@ import (
 2 	"io"
 3 	"os"
 4 	"path/filepath"
 5+	"strings"
 6 
 7 	sst "github.com/picosh/pobj/storage"
 8 )
 9@@ -21,14 +22,22 @@ func NewStorageMinio(address, user, pass string) (*StorageMinio, error) {
10 	return &StorageMinio{st}, nil
11 }
12 
13-func (s *StorageMinio) ServeObject(bucket sst.Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, string, error) {
14-	if opts == nil || os.Getenv("IMGPROXY_URL") == "" {
15-		contentType := GetMimeType(fpath)
16-		rc, _, err := s.GetObject(bucket, fpath)
17-		return rc, contentType, err
18+func (s *StorageMinio) ServeObject(bucket sst.Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, *sst.ObjectInfo, error) {
19+	var rc io.ReadCloser
20+	var info *sst.ObjectInfo
21+	var err error
22+	mimeType := GetMimeType(fpath)
23+	if !strings.HasPrefix(mimeType, "image/") || opts == nil || os.Getenv("IMGPROXY_URL") == "" {
24+		rc, info, err = s.GetObject(bucket, fpath)
25+		// Minio always returns application/octet-stream which needs to be overridden.
26+		info.Metadata.Set("content-type", mimeType)
27+	} else {
28+		filePath := filepath.Join(bucket.Name, fpath)
29+		dataURL := fmt.Sprintf("s3://%s", filePath)
30+		rc, info, err = HandleProxy(dataURL, opts)
31 	}
32-
33-	filePath := filepath.Join(bucket.Name, fpath)
34-	dataURL := fmt.Sprintf("s3://%s", filePath)
35-	return HandleProxy(dataURL, opts)
36+	if err != nil {
37+		return nil, nil, err
38+	}
39+	return rc, info, err
40 }
M shared/storage/proxy.go
+26, -7
 1@@ -12,6 +12,9 @@ import (
 2 	"path/filepath"
 3 	"strconv"
 4 	"strings"
 5+	"time"
 6+
 7+	"github.com/picosh/pobj/storage"
 8 )
 9 
10 func GetMimeType(fpath string) string {
11@@ -199,7 +202,7 @@ func (img *ImgProcessOpts) String() string {
12 	return processOpts
13 }
14 
15-func HandleProxy(dataURL string, opts *ImgProcessOpts) (io.ReadCloser, string, error) {
16+func HandleProxy(dataURL string, opts *ImgProcessOpts) (io.ReadCloser, *storage.ObjectInfo, error) {
17 	imgProxyURL := os.Getenv("IMGPROXY_URL")
18 	imgProxySalt := os.Getenv("IMGPROXY_SALT")
19 	imgProxyKey := os.Getenv("IMGPROXY_KEY")
20@@ -213,12 +216,12 @@ func HandleProxy(dataURL string, opts *ImgProcessOpts) (io.ReadCloser, string, e
21 	if imgProxySalt != "" && imgProxyKey != "" {
22 		keyBin, err := hex.DecodeString(imgProxyKey)
23 		if err != nil {
24-			return nil, "", err
25+			return nil, nil, err
26 		}
27 
28 		saltBin, err := hex.DecodeString(imgProxySalt)
29 		if err != nil {
30-			return nil, "", err
31+			return nil, nil, err
32 		}
33 
34 		mac := hmac.New(sha256.New, keyBin)
35@@ -226,17 +229,33 @@ func HandleProxy(dataURL string, opts *ImgProcessOpts) (io.ReadCloser, string, e
36 		mac.Write([]byte(processPath))
37 		signature = base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
38 	}
39-
40 	proxyAddress := fmt.Sprintf("%s/%s%s", imgProxyURL, signature, processPath)
41 
42 	res, err := http.Get(proxyAddress)
43 	if err != nil {
44-		return nil, "", err
45+		return nil, nil, err
46 	}
47 
48 	if res.StatusCode < 200 || res.StatusCode >= 300 {
49-		return nil, "", fmt.Errorf("%s", res.Status)
50+		return nil, nil, fmt.Errorf("imgproxy returned %s", res.Status)
51+	}
52+	lastModified := res.Header.Get("Last-Modified")
53+	parsedTime, err := time.Parse(time.RFC1123, lastModified)
54+	if err != nil {
55+		return nil, nil, fmt.Errorf("decoding last-modified: %w", err)
56 	}
57+	info := &storage.ObjectInfo{
58+		Size:         res.ContentLength,
59+		LastModified: parsedTime,
60+		ETag:         trimEtag(res.Header.Get("etag")),
61+		Metadata:     res.Header,
62+	}
63+
64+	return res.Body, info, nil
65+}
66 
67-	return res.Body, res.Header.Get("Content-Type"), nil
68+// trimEtag removes quotes from the etag header, which matches the behavior of the minio-go SDK
69+func trimEtag(etag string) string {
70+	etag = strings.TrimPrefix(etag, "\"")
71+	return strings.TrimSuffix(etag, "\"")
72 }
M shared/storage/storage.go
+1, -1
1@@ -8,5 +8,5 @@ import (
2 
3 type StorageServe interface {
4 	sst.ObjectStorage
5-	ServeObject(bucket sst.Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, string, error)
6+	ServeObject(bucket sst.Bucket, fpath string, opts *ImgProcessOpts) (io.ReadCloser, *sst.ObjectInfo, error)
7 }