- 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
+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,
+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
+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
+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 )
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
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+}
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:])