diff options
| author | Xe <me@christine.website> | 2023-01-21 15:22:27 -0500 |
|---|---|---|
| committer | Xe <me@christine.website> | 2023-01-21 15:36:30 -0500 |
| commit | 255d527c651c2a5b1ba82969d13b6df7a33517c7 (patch) | |
| tree | bc9aab56ad743493a79644599eb32de22b10f6a4 /cmd | |
| parent | 4e67062641cbaf7b3a08248ca1cc7661187d9c16 (diff) | |
| download | x-255d527c651c2a5b1ba82969d13b6df7a33517c7.tar.xz x-255d527c651c2a5b1ba82969d13b6df7a33517c7.zip | |
cmd/xedn: resizing of stickers
Signed-off-by: Xe <me@christine.website>
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/xedn/build.go | 8 | ||||
| -rw-r--r-- | cmd/xedn/fly.toml | 2 | ||||
| -rw-r--r-- | cmd/xedn/imgoptimize.go | 158 | ||||
| -rw-r--r-- | cmd/xedn/main.go | 55 |
4 files changed, 210 insertions, 13 deletions
diff --git a/cmd/xedn/build.go b/cmd/xedn/build.go index 38f6335..ded63d7 100644 --- a/cmd/xedn/build.go +++ b/cmd/xedn/build.go @@ -8,7 +8,6 @@ import ( "os" "within.website/x/internal" - "within.website/x/internal/appsluggr" "within.website/x/internal/yeet" ) @@ -19,9 +18,8 @@ func main() { defer cancel() env := append(os.Environ(), []string{"CGO_ENABLED=0", "GOOS=linux"}...) - yeet.ShouldWork(ctx, env, yeet.WD, "go", "build", "-v", "-o=web") - appsluggr.Must("./web", "./slug.tar.gz") - os.Remove("./web") + yeet.ShouldWork(ctx, env, yeet.WD, "nix", "build", ".#xedn-docker") + yeet.DockerLoadResult(ctx, "./result") + yeet.DockerPush(ctx, "registry.fly.io/xedn:latest") yeet.ShouldWork(ctx, env, yeet.WD, "flyctl", "deploy", "--now") - os.Remove("./slug.tar.gz") } diff --git a/cmd/xedn/fly.toml b/cmd/xedn/fly.toml index 4ac79df..a84af65 100644 --- a/cmd/xedn/fly.toml +++ b/cmd/xedn/fly.toml @@ -7,7 +7,7 @@ kill_timeout = 5 processes = [] [build] - dockerfile = "./Dockerfile" + image = "registry.fly.io/xedn:latest" [env] XEDN_STATE = "/data/xedn" diff --git a/cmd/xedn/imgoptimize.go b/cmd/xedn/imgoptimize.go new file mode 100644 index 0000000..8a16408 --- /dev/null +++ b/cmd/xedn/imgoptimize.go @@ -0,0 +1,158 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "image" + "image/png" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/chai2010/webp" + "github.com/disintegration/imaging" + "go.etcd.io/bbolt" + "within.website/ln" + "within.website/x/internal/avif" +) + +type OptimizedImageServer struct { + DB *bbolt.DB + Cache *Cache + PNGEnc *png.Encoder +} + +func (ois *OptimizedImageServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // /sticker/character/mood/width + acc := strings.Split(r.Header.Get("Accept"), ",") + var format = "png" + for _, acceptFormat := range acc { + _, theirFormat, ok := strings.Cut(acceptFormat, "image/") + if !ok { + continue + } + + switch theirFormat { + case "avif", "webp", "png": + format = theirFormat + } + } + + pathParts := strings.Split(r.URL.Path, "/") + if len(pathParts) != 5 { + http.Error(w, "usage: /sticker/:character/:mood/:width", http.StatusBadRequest) + return + } + + character := pathParts[2] + mood := pathParts[3] + widthStr := pathParts[4] + + width, err := strconv.Atoi(widthStr) + if err != nil { + http.Error(w, "width must be an int", http.StatusBadRequest) + return + } + + if width > 256 { + http.Error(w, "width must be less than 257", http.StatusBadRequest) + return + } + + data, err := ois.ResizeTo(width, character, mood, format) + if err != nil { + ln.Error(r.Context(), err) + http.Error(w, "can't convert image", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "image/"+format) + w.Header().Set("Content-Length", strconv.Itoa(len(data))) + w.Header().Set("Cache-Control", "max-age:604800") + w.Header().Set("Expires", time.Now().Add(604800*time.Second).Format(http.TimeFormat)) + w.Header().Set("Etag", fmt.Sprintf(`W/"%s"`, Hash(r.URL.Path))) + w.WriteHeader(http.StatusOK) + w.Write(data) +} + +func (ois *OptimizedImageServer) ResizeTo(widthPixels int, character, mood, format string) ([]byte, error) { + if widthPixels <= 0 { + return nil, errors.New("widthPixels must be greater than 0") + } + + var result bytes.Buffer + boltPath := []byte(filepath.Join(character, mood, strconv.Itoa(widthPixels), format)) + + err := ois.DB.Update(func(tx *bbolt.Tx) error { + bkt, err := tx.CreateBucketIfNotExists([]byte("sticker_cache")) + if err != nil { + return err + } + + (&result).Write(bkt.Get(boltPath)) + + return nil + }) + if err != nil { + return nil, err + } + + if result.Len() != 0 /* because found in boltdb */ { + return result.Bytes(), nil + } + + // /file/christine-static/stickers/aoi/yawn.png + path := fmt.Sprintf("/file/christine-static/stickers/%s/%s.png", character, mood) + data, err := ois.Cache.LoadBytesOrFetch(path) + if err != nil { + return nil, fmt.Errorf("can't fetch: %w", err) + } + + os.WriteFile("foo.png", data, 0666) + + img, _, err := image.Decode(bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("can't decode image: %w", err) + } + + dstImg := imaging.Resize(img, widthPixels, 0, imaging.Lanczos) + + switch format { + case "png": + if err := ois.PNGEnc.Encode(&result, dstImg); err != nil { + return nil, err + } + case "webp": + if err := webp.Encode(&result, dstImg, &webp.Options{Quality: 70}); err != nil { + return nil, err + } + case "avif": + if err := avif.Encode(&result, dstImg, &avif.Options{Quality: 48, Speed: avif.MaxSpeed}); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("I don't know how to render to %s yet, sorry", format) + } + + err = ois.DB.Update(func(tx *bbolt.Tx) error { + bkt, err := tx.CreateBucketIfNotExists([]byte("sticker_cache")) + if err != nil { + return err + } + + if err := bkt.Put(boltPath, result.Bytes()); err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("can't write to database: %w", err) + } + + return result.Bytes(), nil +} diff --git a/cmd/xedn/main.go b/cmd/xedn/main.go index cd3b5e2..c663967 100644 --- a/cmd/xedn/main.go +++ b/cmd/xedn/main.go @@ -2,6 +2,7 @@ package main import ( + "bytes" "context" "crypto/md5" _ "embed" @@ -10,6 +11,7 @@ import ( "expvar" "flag" "fmt" + "image/png" "io" "log" "net/http" @@ -144,7 +146,7 @@ func (dc *Cache) Save(dir string, resp *http.Response) error { var ErrNotCached = errors.New("data is not cached") -func (dc *Cache) Load(dir string, w http.ResponseWriter) error { +func (dc *Cache) Load(dir string, w io.Writer) error { return dc.DB.View(func(tx *bbolt.Tx) error { bkt := tx.Bucket([]byte(dir)) if bkt == nil { @@ -182,9 +184,11 @@ func (dc *Cache) Load(dir string, w http.ResponseWriter) error { return ErrNotCached } - for k, vs := range h { - for _, v := range vs { - w.Header().Add(k, v) + if rw, ok := w.(http.ResponseWriter); ok { + for k, vs := range h { + for _, v := range vs { + rw.Header().Add(k, v) + } } } @@ -196,6 +200,35 @@ func (dc *Cache) Load(dir string, w http.ResponseWriter) error { }) } +func (dc *Cache) LoadBytesOrFetch(path string) ([]byte, error) { + buf := bytes.NewBuffer(nil) + err := dc.Load(path, buf) + if err != nil { + if err == ErrNotCached { + resp, err := dc.Client.Get(fmt.Sprintf("https://%s%s", dc.ActualHost, path)) + if err != nil { + cacheErrors.Add(1) + return nil, err + } + + if resp.StatusCode != http.StatusOK { + cacheErrors.Add(1) + return nil, web.NewError(http.StatusOK, resp) + } + + err = dc.Save(path, resp) + if err != nil { + cacheErrors.Add(1) + return nil, err + } + + return dc.LoadBytesOrFetch(path) + } + return nil, err + } + return buf.Bytes(), nil +} + func (dc *Cache) GetFile(w http.ResponseWriter, r *http.Request) error { dir := filepath.Join(r.URL.Path) @@ -241,9 +274,9 @@ var ( etagMatches = expvar.NewInt("counter_xedn_etag_matches") - referers = metrics.LabelMap{Label: "url"} - fileHits = metrics.LabelMap{Label: "path"} - fileDeaths = metrics.LabelMap{Label: "path"} + referers = metrics.LabelMap{Label: "url"} + fileHits = metrics.LabelMap{Label: "path"} + fileDeaths = metrics.LabelMap{Label: "path"} fileMimeTypes = metrics.LabelMap{Label: "type"} etags map[string]string @@ -271,6 +304,12 @@ func main() { DB: db, } + ois := &OptimizedImageServer{ + DB: db, + Cache: dc, + PNGEnc: &png.Encoder{CompressionLevel: png.BestCompression}, + } + go func() { srv := &tsnet.Server{ Hostname: "xedn-" + os.Getenv("FLY_REGION"), @@ -315,6 +354,8 @@ func main() { w.Write(indexHTML) }) + mux.Handle("/sticker/", ois) + hdlr := func(w http.ResponseWriter, r *http.Request) { etagLock.RLock() etag, ok := etags[r.URL.Path] |
