repos / pico

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

pico / pkg / storage
Eric Bower  ·  2026-04-20

proxy.go

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