diff --git a/srv/src/api/api.go b/srv/src/api/api.go index 79979be..37ea1fc 100644 --- a/srv/src/api/api.go +++ b/srv/src/api/api.go @@ -26,7 +26,8 @@ type Params struct { Logger *mlog.Logger PowManager pow.Manager - PostStore post.Store + PostStore post.Store + PostAssetStore post.AssetStore MailingList mailinglist.MailingList @@ -192,6 +193,8 @@ func (a *api) handler() http.Handler { mux.Handle("/v2/posts/", a.renderPostHandler()) mux.Handle("/v2/", a.renderIndexHandler()) + mux.Handle("/v2/assets/", a.servePostAssetHandler()) + var globalHandler http.Handler = mux globalHandler = setLoggerMiddleware(a.params.Logger, globalHandler) diff --git a/srv/src/api/assets.go b/srv/src/api/assets.go new file mode 100644 index 0000000..e94d324 --- /dev/null +++ b/srv/src/api/assets.go @@ -0,0 +1,113 @@ +package api + +import ( + "bytes" + "errors" + "fmt" + "image" + "image/jpeg" + "image/png" + "io" + "net/http" + "path/filepath" + "strings" + + "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil" + "github.com/mediocregopher/blog.mediocregopher.com/srv/post" + "golang.org/x/image/draw" +) + +func resizeImage(out io.Writer, in io.Reader, maxWidth float64) error { + + img, format, err := image.Decode(in) + if err != nil { + return fmt.Errorf("decoding image: %w", err) + } + + imgRect := img.Bounds() + imgW, imgH := float64(imgRect.Dx()), float64(imgRect.Dy()) + + if imgW > maxWidth { + + newH := imgH * maxWidth / imgW + newImg := image.NewRGBA(image.Rect(0, 0, int(maxWidth), int(newH))) + + // Resize + draw.BiLinear.Scale( + newImg, newImg.Bounds(), img, img.Bounds(), draw.Over, nil, + ) + + img = newImg + } + + switch format { + case "jpeg": + return jpeg.Encode(out, img, nil) + case "png": + return png.Encode(out, img) + default: + return fmt.Errorf("unknown image format %q", format) + } +} + +func (a *api) servePostAssetHandler() http.Handler { + + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + + id := filepath.Base(r.URL.Path) + + maxWidth, err := apiutil.StrToInt(r.FormValue("w"), 0) + if err != nil { + apiutil.BadRequest(rw, r, fmt.Errorf("invalid w parameter: %w", err)) + return + } + + buf := new(bytes.Buffer) + + err = a.params.PostAssetStore.Get(id, buf) + + if errors.Is(err, post.ErrAssetNotFound) { + http.Error(rw, "Asset not found", 404) + return + } else if err != nil { + apiutil.InternalServerError( + rw, r, fmt.Errorf("fetching asset with id %q: %w", id, err), + ) + return + } + + if maxWidth == 0 { + + if _, err := io.Copy(rw, buf); err != nil { + apiutil.InternalServerError( + rw, r, + fmt.Errorf( + "copying asset with id %q to response writer: %w", + id, err, + ), + ) + } + + return + } + + switch ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(id), ".")); ext { + case "jpg", "jpeg", "png": + + if err := resizeImage(rw, buf, float64(maxWidth)); err != nil { + apiutil.InternalServerError( + rw, r, + fmt.Errorf( + "resizing image with id %q to size %d: %w", + id, maxWidth, err, + ), + ) + } + + default: + apiutil.BadRequest(rw, r, fmt.Errorf("cannot resize file with extension %q", ext)) + return + } + + }) +} diff --git a/srv/src/cmd/mediocre-blog/main.go b/srv/src/cmd/mediocre-blog/main.go index bdcd1b9..2660ea4 100644 --- a/srv/src/cmd/mediocre-blog/main.go +++ b/srv/src/cmd/mediocre-blog/main.go @@ -120,10 +120,12 @@ func main() { defer postSQLDB.Close() postStore := post.NewStore(postSQLDB) + postAssetStore := post.NewAssetStore(postSQLDB) apiParams.Logger = logger.WithNamespace("api") apiParams.PowManager = powMgr apiParams.PostStore = postStore + apiParams.PostAssetStore = postAssetStore apiParams.MailingList = ml apiParams.GlobalRoom = chatGlobalRoom apiParams.UserIDCalculator = chatUserIDCalc diff --git a/srv/src/go.mod b/srv/src/go.mod index 2f9bf4b..be8d39e 100644 --- a/srv/src/go.mod +++ b/srv/src/go.mod @@ -17,6 +17,7 @@ require ( github.com/tilinna/clock v1.1.0 github.com/ziutek/mymysql v1.5.4 // indirect golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 + golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect ) diff --git a/srv/src/go.sum b/srv/src/go.sum index 358c3d6..c0b0014 100644 --- a/srv/src/go.sum +++ b/srv/src/go.sum @@ -184,6 +184,8 @@ golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE= +golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -218,6 +220,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=