diff options
| author | Xe Iaso <me@xeiaso.net> | 2023-11-11 13:43:57 -0500 |
|---|---|---|
| committer | Xe Iaso <me@xeiaso.net> | 2023-11-11 13:43:57 -0500 |
| commit | 8e1ff26b3996b72942d49eba18d4c0e6e8328cd6 (patch) | |
| tree | a78d0a56d86b7dbde29b27e9471069ad9c5eee2e | |
| parent | 1d8b0a40144477bb2ca772fba5c612dc1d57df74 (diff) | |
| download | x-8e1ff26b3996b72942d49eba18d4c0e6e8328cd6.tar.xz x-8e1ff26b3996b72942d49eba18d4c0e6e8328cd6.zip | |
cmd/xedn: add image uploading logic for internal api use only
Signed-off-by: Xe Iaso <me@xeiaso.net>
| -rw-r--r-- | cmd/xedn/imgoptimize.go | 187 | ||||
| -rw-r--r-- | cmd/xedn/main.go | 13 |
2 files changed, 198 insertions, 2 deletions
diff --git a/cmd/xedn/imgoptimize.go b/cmd/xedn/imgoptimize.go index 38f6ae9..d1129d1 100644 --- a/cmd/xedn/imgoptimize.go +++ b/cmd/xedn/imgoptimize.go @@ -4,19 +4,28 @@ import ( "bytes" "encoding/json" "errors" + "flag" "fmt" "image" + "image/jpeg" "image/png" + "io" + "log" "log/slog" "net/http" "os" "path/filepath" + "runtime" "strconv" "strings" "time" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/chai2010/webp" "github.com/disintegration/imaging" + "github.com/google/uuid" "go.etcd.io/bbolt" "golang.org/x/sync/singleflight" "tailscale.com/metrics" @@ -33,6 +42,16 @@ type OptimizedImageServer struct { var ( OISFileConversions = metrics.LabelMap{Label: "format"} OISFileHits = metrics.LabelMap{Label: "path"} + + b2KeyID = flag.String("b2-key-id", "", "Backblaze B2 application key ID") + b2KeySecret = flag.String("b2-application-key", "", "Backblaze B2 application secret") + + avifQuality = flag.Int("avif-quality", 24, "AVIF quality (higher is worse quality)") + avifEncoderSpeed = flag.Int("avif-encoder-speed", 0, "AVIF encoder speed (higher is faster)") + + jpegQuality = flag.Int("jpeg-quality", 85, "JPEG quality (lower means lower file size)") + + webpQuality = flag.Int("webp-quality", 25, "WEBP quality (higher is worse quality)") ) func (ois *OptimizedImageServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -138,7 +157,7 @@ func (ois *OptimizedImageServer) ResizeTo(widthPixels int, character, mood, form return nil, err } case "webp": - if err := webp.Encode(&result, dstImg, &webp.Options{Quality: 70}); err != nil { + if err := webp.Encode(&result, dstImg, &webp.Options{Quality: 95}); err != nil { return nil, err } case "avif": @@ -235,3 +254,169 @@ func (ois *OptimizedImageServer) Purge(w http.ResponseWriter, r *http.Request) { return } } + +type ImageUploader struct { + s3 *s3.Client +} + +func (iu *ImageUploader) CreateImage(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id := uuid.New().String() + + // Ten megabytes should be enough for anybody + reader := http.MaxBytesReader(w, r.Body, 1024*1024*10) + + buf := bytes.NewBuffer(make([]byte, r.ContentLength)) + + if _, err := io.Copy(buf, reader); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + slog.Error("cannot copy image to buffer", "err", err) + return + } + + img, _, err := image.Decode(buf) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + slog.Error("cannot decode image", "err", err) + return + } + + directory, err := os.MkdirTemp(*staticDir, "uploud") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + slog.Error("cannot create temp directory", "err", err) + return + } + defer os.RemoveAll(directory) + + if err := doAVIF(img, filepath.Join(directory, "image.avif")); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + slog.Error("cannot encode AVIF", "err", err) + return + } + + if err := doWEBP(img, filepath.Join(directory, "image.webp")); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + slog.Error("cannot encode WEBP", "err", err) + return + } + + if err := doJPEG(img, filepath.Join(directory, "image.jpg")); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + slog.Error("cannot encode JPEG", "err", err) + return + } + + files, err := os.ReadDir(directory) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + slog.Error("cannot read directory", "err", err) + return + } + + s3c := mkS3Client() + + for _, finfo := range files { + log.Printf("uploading %s", finfo.Name()) + fin, err := os.Open(filepath.Join(directory, finfo.Name())) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + slog.Error("cannot read file", "err", err) + return + } + defer fin.Close() + + _, err = s3c.PutObject(ctx, &s3.PutObjectInput{ + Body: fin, + Bucket: b2Backend, + Key: aws.String("xedn/dynamic/" + id + "/" + finfo.Name()), + ContentType: aws.String(mimeTypes[filepath.Ext(finfo.Name())]), + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + slog.Error("cannot upload file", "err", err) + return + } + } + + json.NewEncoder(w).Encode(map[string]string{ + "avif": "https://cdn.xeiaso.net/file/christine-static/xedn/dynamic/" + id + "/image.avif", + "webp": "https://cdn.xeiaso.net/file/christine-static/xedn/dynamic/" + id + "/image.webp", + "jpeg": "https://cdn.xeiaso.net/file/christine-static/xedn/dynamic/" + id + "/image.jpg", + }) +} + +var mimeTypes = map[string]string{ + ".avif": "image/avif", + ".webp": "image/webp", + ".jpg": "image/jpeg", + ".png": "image/png", + ".wasm": "application/wasm", + ".css": "text/css", +} + +func mkS3Client() *s3.Client { + s3Config := aws.Config{ + Credentials: credentials.NewStaticCredentialsProvider(*b2KeyID, *b2KeySecret, ""), + BaseEndpoint: aws.String("https://s3.us-west-001.backblazeb2.com"), + Region: "us-west-001", + } + s3Client := s3.NewFromConfig(s3Config, (func(o *s3.Options) { + o.UsePathStyle = true + })) + return s3Client +} + +func doAVIF(src image.Image, dstPath string) error { + dst, err := os.Create(dstPath) + if err != nil { + log.Fatalf("Can't create destination file: %v", err) + } + defer dst.Close() + + err = avif.Encode(dst, src, &avif.Options{ + Threads: runtime.GOMAXPROCS(0), + Speed: *avifEncoderSpeed, + Quality: *avifQuality, + }) + if err != nil { + return err + } + + log.Printf("Encoded AVIF at %s", dstPath) + + return nil +} + +func doWEBP(src image.Image, dstPath string) error { + fout, err := os.Create(dstPath) + if err != nil { + return err + } + defer fout.Close() + + err = webp.Encode(fout, src, &webp.Options{Quality: float32(*webpQuality)}) + if err != nil { + return err + } + + log.Printf("Encoded WEBP at %s", dstPath) + + return nil +} + +func doJPEG(src image.Image, dstPath string) error { + fout, err := os.Create(dstPath) + if err != nil { + return err + } + defer fout.Close() + + if err := jpeg.Encode(fout, src, &jpeg.Options{Quality: *jpegQuality}); err != nil { + return err + } + + log.Printf("Encoded JPEG at %s", dstPath) + + return nil +} diff --git a/cmd/xedn/main.go b/cmd/xedn/main.go index 69c8646..124aaf6 100644 --- a/cmd/xedn/main.go +++ b/cmd/xedn/main.go @@ -124,6 +124,10 @@ func main() { group: &singleflight.Group{}, } + iu := &ImageUploader{ + s3: mkS3Client(), + } + os.MkdirAll(filepath.Join(*dir, "xesite"), 0700) zs, err := xesite.NewZipServer(filepath.Join(*dir, "xesite", "latest.zip"), *dir) if err != nil { @@ -157,7 +161,14 @@ func main() { os.MkdirAll(*dir, 0700) - go http.ListenAndServe(*metricsAddr, http.HandlerFunc(tsweb.VarzHandler)) + { + mux := http.NewServeMux() + + mux.HandleFunc("/metrics", tsweb.VarzHandler) + mux.HandleFunc("/xedn/optimize", iu.CreateImage) + + go http.ListenAndServe(*metricsAddr, http.HandlerFunc(tsweb.VarzHandler)) + } cdn := http.NewServeMux() |
