aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXe Iaso <me@xeiaso.net>2023-11-11 13:43:57 -0500
committerXe Iaso <me@xeiaso.net>2023-11-11 13:43:57 -0500
commit8e1ff26b3996b72942d49eba18d4c0e6e8328cd6 (patch)
treea78d0a56d86b7dbde29b27e9471069ad9c5eee2e
parent1d8b0a40144477bb2ca772fba5c612dc1d57df74 (diff)
downloadx-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.go187
-rw-r--r--cmd/xedn/main.go13
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()