Eric Bower
·
2026-06-13
1package storage
2
3import (
4 "crypto/hmac"
5 "crypto/sha256"
6 "encoding/base64"
7 "encoding/hex"
8 "fmt"
9 "net/http"
10 "net/http/httputil"
11 "net/url"
12 "os"
13 "strconv"
14 "strings"
15
16 "github.com/picosh/pico/pkg/shared/mime"
17)
18
19type ImgProxy struct {
20 url string
21 salt string
22 key string
23 filepath string
24 opts *ImgProcessOpts
25}
26
27func NewImgProxy(fp string, opts *ImgProcessOpts) *ImgProxy {
28 img := &ImgProxy{
29 url: os.Getenv("IMGPROXY_URL"),
30 salt: os.Getenv("IMGPROXY_SALT"),
31 key: os.Getenv("IMGPROXY_KEY"),
32 filepath: fp,
33 opts: opts,
34 }
35 return img
36}
37
38func (img *ImgProxy) CanServe() error {
39 if img.url == "" {
40 return fmt.Errorf("no imgproxy url provided")
41 }
42 if img.opts == nil {
43 return fmt.Errorf("no image options provided")
44 }
45 mimeType := mime.GetMimeType(img.filepath)
46 if !strings.HasPrefix(mimeType, "image/") {
47 return fmt.Errorf("file mimetype not an image")
48 }
49 return nil
50}
51
52func (img *ImgProxy) GetSig(ppath []byte) string {
53 signature := "_"
54 imgProxySalt := img.salt
55 imgProxyKey := img.key
56 if imgProxySalt == "" || imgProxyKey == "" {
57 return signature
58 }
59
60 keyBin, err := hex.DecodeString(imgProxyKey)
61 if err != nil {
62 return signature
63 }
64
65 saltBin, err := hex.DecodeString(imgProxySalt)
66 if err != nil {
67 return signature
68 }
69
70 mac := hmac.New(sha256.New, keyBin)
71 mac.Write(saltBin)
72 mac.Write(ppath)
73 return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
74}
75
76func (img *ImgProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
77 dataURL := fmt.Sprintf("local:///%s", img.filepath)
78 imgProxyURL := img.url
79 processOpts := img.opts.String()
80 processPath := fmt.Sprintf(
81 "%s/%s",
82 processOpts,
83 base64.StdEncoding.EncodeToString([]byte(dataURL)),
84 )
85 sig := img.GetSig([]byte(processPath))
86
87 rurl := fmt.Sprintf("%s/%s%s", imgProxyURL, sig, processPath)
88 destUrl, err := url.Parse(rurl)
89 if err != nil {
90 msg := fmt.Sprintf("could not parse url: %s", rurl)
91 http.Error(w, msg, http.StatusInternalServerError)
92 return
93 }
94 proxy := &httputil.ReverseProxy{
95 Rewrite: func(r *httputil.ProxyRequest) {
96 r.SetURL(destUrl)
97 r.Out.URL.Path = destUrl.Path
98 r.Out.URL.RawPath = destUrl.RawPath
99 },
100 }
101 proxy.ServeHTTP(w, r)
102}
103
104func UriToImgProcessOpts(uri string) (*ImgProcessOpts, error) {
105 opts := &ImgProcessOpts{}
106 parts := strings.Split(uri, "/")
107
108 for _, part := range parts {
109 ratio, err := GetRatio(part)
110 if err != nil {
111 return opts, err
112 }
113
114 if ratio != nil {
115 opts.Ratio = ratio
116 }
117
118 if strings.HasPrefix(part, "s:") {
119 segs := strings.SplitN(part, ":", 4)
120 r := &Ratio{}
121 for idx, sg := range segs {
122 if sg == "" {
123 continue
124 }
125 switch idx {
126 case 1:
127 r.Width, err = strconv.Atoi(sg)
128 if err != nil {
129 return opts, err
130 }
131 case 2:
132 r.Height, err = strconv.Atoi(sg)
133 if err != nil {
134 return opts, err
135 }
136 }
137 }
138 opts.Ratio = r
139 }
140
141 if strings.HasPrefix(part, "q:") {
142 quality := strings.Replace(part, "q:", "", 1)
143 opts.Quality, err = strconv.Atoi(quality)
144 if err != nil {
145 return opts, err
146 }
147 }
148
149 if strings.HasPrefix(part, "rt:") {
150 angle := strings.Replace(part, "rt:", "", 1)
151 opts.Rotate, err = strconv.Atoi(angle)
152 if err != nil {
153 return opts, err
154 }
155 }
156
157 if strings.HasPrefix(part, "ext:") {
158 ext := strings.Replace(part, "ext:", "", 1)
159 opts.Ext = ext
160 if err != nil {
161 return opts, err
162 }
163 }
164 }
165
166 return opts, nil
167}
168
169type ImgProcessOpts struct {
170 Quality int
171 Ratio *Ratio
172 Rotate int
173 Ext string
174 NoRaw bool
175}
176
177func (img *ImgProcessOpts) String() string {
178 processOpts := ""
179
180 // https://docs.imgproxy.net/usage/processing#quality
181 if img.Quality != 0 {
182 processOpts = fmt.Sprintf("%s/q:%d", processOpts, img.Quality)
183 }
184
185 // https://docs.imgproxy.net/usage/processing#size
186 if img.Ratio != nil {
187 processOpts = fmt.Sprintf(
188 "%s/size:%d:%d",
189 processOpts,
190 img.Ratio.Width,
191 img.Ratio.Height,
192 )
193 }
194
195 // https://docs.imgproxy.net/usage/processing#rotate
196 // Only 0, 90, 180, 270, etc., degree angles are supported.
197 if img.Rotate != 0 {
198 rot := img.Rotate
199 if rot == 90 || rot == 180 || rot == 270 {
200 processOpts = fmt.Sprintf(
201 "%s/rotate:%d",
202 processOpts,
203 rot,
204 )
205 }
206 }
207
208 // https://docs.imgproxy.net/usage/processing#format
209 if img.Ext != "" {
210 processOpts = fmt.Sprintf("%s/ext:%s", processOpts, img.Ext)
211 }
212
213 if processOpts == "" && !img.NoRaw {
214 processOpts = fmt.Sprintf("%s/raw:true", processOpts)
215 }
216
217 return processOpts
218}