repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

commit
a2488d8
parent
a895946
author
Eric Bower
date
2024-01-31 05:00:45 +0000 UTC
feat: support more imgproxy options (#73)

New options in any combinations:

- `/s:w:h`
- `/q:1-100`
- `/rt:angle`

We also still support our imgs api version of sizing.
7 files changed,  +226, -15
M filehandlers/assets/handler.go
+13, -2
 1@@ -47,7 +47,12 @@ func getStorageSize(s ssh.Session) uint64 {
 2 
 3 func incrementStorageSize(s ssh.Session, fileSize int64) uint64 {
 4 	curSize := getStorageSize(s)
 5-	nextStorageSize := curSize + uint64(fileSize)
 6+	var nextStorageSize uint64
 7+	if fileSize < 0 {
 8+		nextStorageSize = curSize - uint64(fileSize)
 9+	} else {
10+		nextStorageSize = curSize + uint64(fileSize)
11+	}
12 	s.Context().SetValue(ctxStorageSizeKey{}, nextStorageSize)
13 	return nextStorageSize
14 }
15@@ -305,7 +310,13 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *utils.FileEntry) (strin
16 
17 func (h *UploadAssetHandler) validateAsset(data *FileData) (bool, error) {
18 	storageMax := data.FeatureFlag.Data.StorageMax
19-	if data.StorageSize+uint64(data.DeltaFileSize) >= storageMax {
20+	var nextStorageSize uint64
21+	if data.DeltaFileSize < 0 {
22+		nextStorageSize = data.StorageSize - uint64(data.DeltaFileSize)
23+	} else {
24+		nextStorageSize = data.StorageSize + uint64(data.DeltaFileSize)
25+	}
26+	if nextStorageSize >= storageMax {
27 		return false, fmt.Errorf(
28 			"ERROR: user (%s) has exceeded (%d bytes) max (%d bytes)",
29 			data.User.Name,
M imgs/api.go
+11, -9
 1@@ -180,20 +180,22 @@ func ImgRequest(w http.ResponseWriter, r *http.Request) {
 2 		return
 3 	}
 4 
 5-	var dimes string
 6+	var imgOpts string
 7 	var slug string
 8 	if !cfg.IsSubdomains() || subdomain == "" {
 9 		slug, _ = url.PathUnescape(shared.GetField(r, 1))
10-		dimes, _ = url.PathUnescape(shared.GetField(r, 2))
11+		imgOpts, _ = url.PathUnescape(shared.GetField(r, 2))
12 	} else {
13 		slug, _ = url.PathUnescape(shared.GetField(r, 0))
14-		dimes, _ = url.PathUnescape(shared.GetField(r, 1))
15+		imgOpts, _ = url.PathUnescape(shared.GetField(r, 1))
16 	}
17 
18-	ratio, _ := storage.GetRatio(dimes)
19-	opts := &storage.ImgProcessOpts{
20-		Quality: 80,
21-		Ratio:   ratio,
22+	opts, err := storage.UriToImgProcessOpts(imgOpts)
23+	if err != nil {
24+		errMsg := fmt.Sprintf("error processing img options: %s", err.Error())
25+		logger.Infof(errMsg)
26+		http.Error(w, errMsg, http.StatusUnprocessableEntity)
27+		return
28 	}
29 
30 	ext := filepath.Ext(slug)
31@@ -255,7 +257,7 @@ func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
32 		shared.NewRoute("GET", "/([^/]+)", redirectHandler),
33 		shared.NewRoute("GET", "/([^/]+)/o/([^/]+)", ImgRequest),
34 		shared.NewRoute("GET", "/([^/]+)/([^/]+)", ImgRequest),
35-		shared.NewRoute("GET", "/([^/]+)/([^/]+)/([a-z0-9]+)", ImgRequest),
36+		shared.NewRoute("GET", "/([^/]+)/([^/]+)/(.+)", ImgRequest),
37 	)
38 
39 	return routes
40@@ -274,7 +276,7 @@ func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
41 		shared.NewRoute("GET", "/", redirectHandler),
42 		shared.NewRoute("GET", "/o/([^/]+)", ImgRequest),
43 		shared.NewRoute("GET", "/([^/]+)", ImgRequest),
44-		shared.NewRoute("GET", "/([^/]+)/([a-z0-9]+)", ImgRequest),
45+		shared.NewRoute("GET", "/([^/]+)/(.+)", ImgRequest),
46 	)
47 
48 	return routes
M pgs/api.go
+15, -0
 1@@ -366,6 +366,20 @@ func ServeAsset(fname string, opts *storage.ImgProcessOpts, fromImgs bool, w htt
 2 	asset.handle(w)
 3 }
 4 
 5+func ImgAssetRequest(w http.ResponseWriter, r *http.Request) {
 6+	logger := shared.GetLogger(r)
 7+	fname, _ := url.PathUnescape(shared.GetField(r, 0))
 8+	imgOpts, _ := url.PathUnescape(shared.GetField(r, 1))
 9+	opts, err := storage.UriToImgProcessOpts(imgOpts)
10+	if err != nil {
11+		errMsg := fmt.Sprintf("error processing img options: %s", err.Error())
12+		logger.Infof(errMsg)
13+		http.Error(w, errMsg, http.StatusUnprocessableEntity)
14+	}
15+
16+	ServeAsset(fname, opts, false, w, r)
17+}
18+
19 func AssetRequest(w http.ResponseWriter, r *http.Request) {
20 	fname, _ := url.PathUnescape(shared.GetField(r, 0))
21 	ServeAsset(fname, nil, false, w, r)
22@@ -410,6 +424,7 @@ func StartApiServer() {
23 	}
24 	subdomainRoutes := []shared.Route{
25 		shared.NewRoute("GET", "/", AssetRequest),
26+		shared.NewRoute("GET", "(/.+.(?:jpg|jpeg|png|gif|webp|svg))(/.+)", ImgAssetRequest),
27 		shared.NewRoute("GET", "/(.+)", AssetRequest),
28 	}
29 
M prose/api.go
+4, -4
 1@@ -827,8 +827,8 @@ func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
 2 		shared.NewRoute("GET", "/([^/]+)/feed.xml", rssBlogHandler),
 3 		shared.NewRoute("GET", "/([^/]+)/_styles.css", blogStyleHandler),
 4 		shared.NewRoute("GET", "/raw/([^/]+)/(.+)", postRawHandler),
 5-		shared.NewRoute("GET", "/([^/]+)/(.+)/([a-z0-9]+)", imgs.ImgRequest),
 6-		shared.NewRoute("GET", "/([^/]+)/(.+).(jpg|jpeg|png|gif|webp|svg)", imgs.ImgRequest),
 7+		shared.NewRoute("GET", "/([^/]+)/(.+)/(.+)", imgs.ImgRequest),
 8+		shared.NewRoute("GET", "/([^/]+)/(.+).(?:jpg|jpeg|png|gif|webp|svg)$", imgs.ImgRequest),
 9 		shared.NewRoute("GET", "/([^/]+)/i", imgs.ImgsListHandler),
10 		shared.NewRoute("GET", "/([^/]+)/(.+)", postHandler),
11 	)
12@@ -856,8 +856,8 @@ func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
13 	routes = append(
14 		routes,
15 		shared.NewRoute("GET", "/raw/(.+)", postRawHandler),
16-		shared.NewRoute("GET", "/(.+)/([a-z0-9]+)", imgs.ImgRequest),
17-		shared.NewRoute("GET", "/(.+).(jpg|jpeg|png|gif|webp|svg)", imgs.ImgRequest),
18+		shared.NewRoute("GET", "/([^/]+)/(.+)", imgs.ImgRequest),
19+		shared.NewRoute("GET", "/(.+).(?:jpg|jpeg|png|gif|webp|svg)$", imgs.ImgRequest),
20 		shared.NewRoute("GET", "/i", imgs.ImgsListHandler),
21 		shared.NewRoute("GET", "/(.+)", postHandler),
22 	)
M shared/storage/proxy.go
+74, -0
  1@@ -10,6 +10,8 @@ import (
  2 	"net/http"
  3 	"os"
  4 	"path/filepath"
  5+	"strconv"
  6+	"strings"
  7 )
  8 
  9 func GetMimeType(fpath string) string {
 10@@ -61,18 +63,77 @@ func GetMimeType(fpath string) string {
 11 	return "text/plain"
 12 }
 13 
 14+func UriToImgProcessOpts(uri string) (*ImgProcessOpts, error) {
 15+	opts := &ImgProcessOpts{}
 16+	parts := strings.Split(uri, "/")
 17+
 18+	for _, part := range parts {
 19+		ratio, err := GetRatio(part)
 20+		if err != nil {
 21+			return opts, err
 22+		}
 23+
 24+		if ratio != nil {
 25+			opts.Ratio = ratio
 26+		}
 27+
 28+		if strings.HasPrefix(part, "s:") {
 29+			segs := strings.SplitN(part, ":", 4)
 30+			r := &Ratio{}
 31+			for idx, sg := range segs {
 32+				if sg == "" {
 33+					continue
 34+				}
 35+				if idx == 1 {
 36+					r.Width, err = strconv.Atoi(sg)
 37+					if err != nil {
 38+						return opts, err
 39+					}
 40+				} else if idx == 2 {
 41+					r.Height, err = strconv.Atoi(sg)
 42+					if err != nil {
 43+						return opts, err
 44+					}
 45+				}
 46+			}
 47+			opts.Ratio = r
 48+		}
 49+
 50+		if strings.HasPrefix(part, "q:") {
 51+			quality := strings.Replace(part, "q:", "", 1)
 52+			opts.Quality, err = strconv.Atoi(quality)
 53+			if err != nil {
 54+				return opts, err
 55+			}
 56+		}
 57+
 58+		if strings.HasPrefix(part, "rt:") {
 59+			angle := strings.Replace(part, "rt:", "", 1)
 60+			opts.Rotate, err = strconv.Atoi(angle)
 61+			if err != nil {
 62+				return opts, err
 63+			}
 64+		}
 65+	}
 66+
 67+	return opts, nil
 68+}
 69+
 70 type ImgProcessOpts struct {
 71 	Quality int
 72 	Ratio   *Ratio
 73+	Rotate  int
 74 }
 75 
 76 func (img *ImgProcessOpts) String() string {
 77 	processOpts := ""
 78 
 79+	// https://docs.imgproxy.net/usage/processing#quality
 80 	if img.Quality != 0 {
 81 		processOpts = fmt.Sprintf("%s/q:%d", processOpts, img.Quality)
 82 	}
 83 
 84+	// https://docs.imgproxy.net/usage/processing#size
 85 	if img.Ratio != nil {
 86 		processOpts = fmt.Sprintf(
 87 			"%s/s:%d:%d",
 88@@ -82,6 +143,19 @@ func (img *ImgProcessOpts) String() string {
 89 		)
 90 	}
 91 
 92+	// https://docs.imgproxy.net/usage/processing#rotate
 93+	// Only 0, 90, 180, 270, etc., degree angles are supported.
 94+	if img.Rotate != 0 {
 95+		rot := img.Rotate
 96+		if rot == 90 || rot == 180 || rot == 280 {
 97+			processOpts = fmt.Sprintf(
 98+				"%s/rotate:%d",
 99+				processOpts,
100+				rot,
101+			)
102+		}
103+	}
104+
105 	return processOpts
106 }
107 
A shared/storage/proxy_test.go
+104, -0
  1@@ -0,0 +1,104 @@
  2+package storage
  3+
  4+import (
  5+	"testing"
  6+
  7+	"github.com/google/go-cmp/cmp"
  8+)
  9+
 10+type Fixture struct {
 11+	name   string
 12+	input  string
 13+	expect *ImgProcessOpts
 14+}
 15+
 16+func TestUriToImgProcessOpts(t *testing.T) {
 17+	fixtures := []Fixture{
 18+		{
 19+			name:  "imgs_api_height",
 20+			input: "/x500",
 21+			expect: &ImgProcessOpts{
 22+				Ratio: &Ratio{
 23+					Width:  0,
 24+					Height: 500,
 25+				},
 26+			},
 27+		},
 28+		{
 29+			name:  "imgs_api_width",
 30+			input: "/500x",
 31+			expect: &ImgProcessOpts{
 32+				Ratio: &Ratio{
 33+					Width:  500,
 34+					Height: 0,
 35+				},
 36+			},
 37+		},
 38+		{
 39+			name:  "imgs_api_both",
 40+			input: "/500x600",
 41+			expect: &ImgProcessOpts{
 42+				Ratio: &Ratio{
 43+					Width:  500,
 44+					Height: 600,
 45+				},
 46+			},
 47+		},
 48+		{
 49+			name:  "imgproxy_height",
 50+			input: "/s::500",
 51+			expect: &ImgProcessOpts{
 52+				Ratio: &Ratio{
 53+					Width:  0,
 54+					Height: 500,
 55+				},
 56+			},
 57+		},
 58+		{
 59+			name:  "imgproxy_width",
 60+			input: "/s:500",
 61+			expect: &ImgProcessOpts{
 62+				Ratio: &Ratio{
 63+					Width:  500,
 64+					Height: 0,
 65+				},
 66+			},
 67+		},
 68+		{
 69+			name:  "imgproxy_both",
 70+			input: "/s:500:600",
 71+			expect: &ImgProcessOpts{
 72+				Ratio: &Ratio{
 73+					Width:  500,
 74+					Height: 600,
 75+				},
 76+			},
 77+		},
 78+		{
 79+			name:  "imgproxy_quality",
 80+			input: "/q:80",
 81+			expect: &ImgProcessOpts{
 82+				Quality: 80,
 83+			},
 84+		},
 85+		{
 86+			name:  "imgproxy_rotate",
 87+			input: "/rt:90",
 88+			expect: &ImgProcessOpts{
 89+				Rotate: 90,
 90+			},
 91+		},
 92+	}
 93+
 94+	for _, fixture := range fixtures {
 95+		t.Run(fixture.name, func(t *testing.T) {
 96+			results, err := UriToImgProcessOpts(fixture.input)
 97+			if err != nil {
 98+				t.Error(err)
 99+			}
100+			if cmp.Equal(results, fixture.expect) == false {
101+				t.Fatalf(cmp.Diff(fixture.expect, results))
102+			}
103+		})
104+	}
105+}
M shared/storage/ratio.go
+5, -0
 1@@ -16,6 +16,11 @@ func GetRatio(dimes string) (*Ratio, error) {
 2 		return nil, nil
 3 	}
 4 
 5+	// bail if we detect imgproxy options
 6+	if strings.Contains(dimes, ":") {
 7+		return nil, nil
 8+	}
 9+
10 	// dimes = x250 -- width is auto scaled and height is 250
11 	if strings.HasPrefix(dimes, "x") {
12 		height, err := strconv.Atoi(dimes[1:])