aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorXe <me@christine.website>2023-01-21 15:22:27 -0500
committerXe <me@christine.website>2023-01-21 15:36:30 -0500
commit255d527c651c2a5b1ba82969d13b6df7a33517c7 (patch)
treebc9aab56ad743493a79644599eb32de22b10f6a4 /cmd
parent4e67062641cbaf7b3a08248ca1cc7661187d9c16 (diff)
downloadx-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.go8
-rw-r--r--cmd/xedn/fly.toml2
-rw-r--r--cmd/xedn/imgoptimize.go158
-rw-r--r--cmd/xedn/main.go55
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]