- 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
+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
+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"))
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 }
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 }
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 }
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 }
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 }