summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/cmd/mediocre-blog/main.go9
-rw-r--r--src/gmi/gmi.go23
-rw-r--r--src/http/assets.go222
-rw-r--r--src/http/http.go7
-rw-r--r--src/http/posts.go3
-rw-r--r--src/post/asset/loader.go43
-rw-r--r--src/post/asset/loader_archive.go99
-rw-r--r--src/post/asset/loader_image.go85
8 files changed, 281 insertions, 210 deletions
diff --git a/src/cmd/mediocre-blog/main.go b/src/cmd/mediocre-blog/main.go
index aff0f8e..ee09f92 100644
--- a/src/cmd/mediocre-blog/main.go
+++ b/src/cmd/mediocre-blog/main.go
@@ -101,7 +101,13 @@ func main() {
defer postSQLDB.Close()
postStore := post.NewStore(postSQLDB)
+
postAssetStore := asset.NewStore(postSQLDB)
+
+ postAssetLoader := asset.NewStoreLoader(postAssetStore)
+ postAssetLoader = asset.NewArchiveLoader(postAssetLoader)
+ postAssetLoader = asset.NewImageLoader(postAssetLoader)
+
postDraftStore := post.NewDraftStore(postSQLDB)
cache := cache.New(5000)
@@ -111,6 +117,7 @@ func main() {
httpParams.PowManager = powMgr
httpParams.PostStore = postStore
httpParams.PostAssetStore = postAssetStore
+ httpParams.PostAssetLoader = postAssetLoader
httpParams.PostDraftStore = postDraftStore
httpParams.MailingList = ml
httpParams.GeminiPublicURL = gmiParams.PublicURL
@@ -132,7 +139,7 @@ func main() {
gmiParams.Logger = logger.WithNamespace("gmi")
gmiParams.Cache = cache
gmiParams.PostStore = postStore
- gmiParams.PostAssetStore = postAssetStore
+ gmiParams.PostAssetLoader = postAssetLoader
gmiParams.HTTPPublicURL = httpParams.PublicURL
logger.Info(ctx, "starting gmi api")
diff --git a/src/gmi/gmi.go b/src/gmi/gmi.go
index 127f6b5..e0c4626 100644
--- a/src/gmi/gmi.go
+++ b/src/gmi/gmi.go
@@ -2,13 +2,16 @@
package gmi
import (
+ "bytes"
"context"
"errors"
"fmt"
+ "io"
"mime"
"net/url"
"os"
"path"
+ "path/filepath"
"strings"
"git.sr.ht/~adnano/go-gemini"
@@ -27,8 +30,8 @@ type Params struct {
Logger *mlog.Logger
Cache cache.Cache
- PostStore post.Store
- PostAssetStore asset.Store
+ PostStore post.Store
+ PostAssetLoader asset.Loader
PublicURL *url.URL
ListenAddr string
@@ -208,16 +211,18 @@ func (a *api) assetsMiddleware() gemini.Handler {
r *gemini.Request,
) {
- id := path.Base(r.URL.Path)
- mimeType := mime.TypeByExtension(path.Ext(id))
+ path := strings.TrimPrefix(r.URL.Path, "/assets/")
+ mimeType := mime.TypeByExtension(filepath.Ext(path))
- ctx = mctx.Annotate(ctx, "assetID", id, "mimeType", mimeType)
+ ctx = mctx.Annotate(ctx, "assetPath", path, "mimeType", mimeType)
if mimeType != "" {
rw.SetMediaType(mimeType)
}
- err := a.params.PostAssetStore.Get(id, rw)
+ buf := new(bytes.Buffer)
+
+ err := a.params.PostAssetLoader.Load(path, buf, asset.LoadOpts{})
if errors.Is(err, asset.ErrNotFound) {
rw.WriteHeader(gemini.StatusNotFound, "Asset not found, sorry!")
@@ -228,6 +233,12 @@ func (a *api) assetsMiddleware() gemini.Handler {
rw.WriteHeader(gemini.StatusTemporaryFailure, err.Error())
return
}
+
+ if _, err := io.Copy(rw, buf); err != nil {
+ a.params.Logger.Error(ctx, "error copying asset", err)
+ rw.WriteHeader(gemini.StatusTemporaryFailure, err.Error())
+ return
+ }
})
}
diff --git a/src/http/assets.go b/src/http/assets.go
index 1f5f0d6..1a8f520 100644
--- a/src/http/assets.go
+++ b/src/http/assets.go
@@ -2,14 +2,8 @@ package http
import (
"bytes"
- "compress/gzip"
"errors"
"fmt"
- "image"
- "image/jpeg"
- "image/png"
- "io"
- "io/fs"
"net/http"
"path/filepath"
"strings"
@@ -17,52 +11,8 @@ import (
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
"github.com/mediocregopher/blog.mediocregopher.com/srv/post/asset"
- "github.com/omeid/go-tarfs"
- "golang.org/x/image/draw"
)
-func isImgResizable(path string) bool {
- switch strings.ToLower(filepath.Ext(path)) {
- case ".jpg", ".jpeg", ".png":
- return true
- default:
- return false
- }
-}
-
-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) managePostAssetsHandler() http.Handler {
tpl := a.mustParseBasedTpl("post-assets-manage.html")
@@ -88,173 +38,47 @@ func (a *api) managePostAssetsHandler() http.Handler {
})
}
-type postAssetArchiveInfo struct {
- path string
- id string
- subPath string
- isGzipped bool
-}
-
-func extractPostAssetArchiveInfo(path string) (postAssetArchiveInfo, bool) {
-
- var info postAssetArchiveInfo
-
- info.path = strings.TrimPrefix(path, "/")
-
- info.id, info.subPath, _ = strings.Cut(info.path, "/")
-
- switch {
-
- case strings.HasSuffix(info.id, ".tar.gz"),
- strings.HasSuffix(info.id, ".tgz"):
- info.isGzipped = true
-
- case strings.HasSuffix(info.id, ".tar"):
- // ok
-
- default:
- // unsupported
- return postAssetArchiveInfo{}, false
- }
-
- return info, true
-}
-
-func (a *api) writePostAsset(
- rw http.ResponseWriter,
- r *http.Request,
- path string,
- from io.ReadSeeker,
-) {
-
- maxWidth, err := apiutil.StrToInt(r.FormValue("w"), 0)
- if err != nil {
- apiutil.BadRequest(rw, r, fmt.Errorf("invalid w parameter: %w", err))
- return
- }
-
- if maxWidth == 0 {
- http.ServeContent(rw, r, path, time.Time{}, from)
- return
- }
-
- if !isImgResizable(path) {
- apiutil.BadRequest(rw, r, fmt.Errorf("cannot resize asset %q", path))
- return
- }
-
- resizedBuf := new(bytes.Buffer)
-
- if err := resizeImage(resizedBuf, from, float64(maxWidth)); err != nil {
- apiutil.InternalServerError(
- rw, r,
- fmt.Errorf(
- "resizing image %q to size %d: %w",
- path, maxWidth, err,
- ),
- )
- }
-
- http.ServeContent(
- rw, r, path, time.Time{}, bytes.NewReader(resizedBuf.Bytes()),
- )
-}
-
-func (a *api) handleGetPostAssetArchive(
- rw http.ResponseWriter,
- r *http.Request,
- info postAssetArchiveInfo,
-) {
-
- buf := new(bytes.Buffer)
-
- err := a.params.PostAssetStore.Get(info.id, buf)
-
- if errors.Is(err, asset.ErrNotFound) {
- http.Error(rw, "asset not found", 404)
- return
- } else if err != nil {
- apiutil.InternalServerError(
- rw, r,
- fmt.Errorf("fetching archive asset with id %q: %w", info.id, err),
- )
- return
- }
-
- var from io.Reader = buf
-
- if info.isGzipped {
-
- if from, err = gzip.NewReader(from); err != nil {
- apiutil.InternalServerError(
- rw, r,
- fmt.Errorf("decompressing archive asset with id %q: %w", info.id, err),
- )
- return
- }
- }
-
- tarFS, err := tarfs.New(from)
-
- if err != nil {
- apiutil.InternalServerError(
- rw, r,
- fmt.Errorf("reading archive asset with id %q as fs: %w", info.id, err),
- )
- return
- }
-
- f, err := tarFS.Open(info.subPath)
-
- if errors.Is(err, fs.ErrExist) {
- http.Error(rw, "Asset not found", 404)
- return
-
- } else if err != nil {
-
- apiutil.InternalServerError(
- rw, r,
- fmt.Errorf(
- "opening path %q from archive asset with id %q as fs: %w",
- info.subPath, info.id, err,
- ),
- )
- return
- }
-
- defer f.Close()
-
- a.writePostAsset(rw, r, info.path, f)
-}
-
func (a *api) getPostAssetHandler() http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- archiveInfo, ok := extractPostAssetArchiveInfo(r.URL.Path)
-
- if ok {
- a.handleGetPostAssetArchive(rw, r, archiveInfo)
+ maxWidth, err := apiutil.StrToInt(r.FormValue("w"), 0)
+ if err != nil {
+ apiutil.BadRequest(rw, r, fmt.Errorf("invalid w parameter: %w", err))
return
}
- id := filepath.Base(r.URL.Path)
-
- buf := new(bytes.Buffer)
+ var (
+ path = strings.TrimPrefix(r.URL.Path, "/")
+ buf = new(bytes.Buffer)
+ )
- err := a.params.PostAssetStore.Get(id, buf)
+ err = a.params.PostAssetLoader.Load(
+ path,
+ buf,
+ asset.LoadOpts{
+ ImageWidth: maxWidth,
+ },
+ )
if errors.Is(err, asset.ErrNotFound) {
http.Error(rw, "Asset not found", 404)
return
+
+ } else if errors.Is(err, asset.ErrCannotResize) {
+ http.Error(rw, "Image resizing not supported", 400)
+ return
+
} else if err != nil {
apiutil.InternalServerError(
- rw, r, fmt.Errorf("fetching asset with id %q: %w", id, err),
+ rw, r, fmt.Errorf("fetching asset at path %q: %w", path, err),
)
return
}
- a.writePostAsset(rw, r, id, bytes.NewReader(buf.Bytes()))
+ http.ServeContent(
+ rw, r, path, time.Time{}, bytes.NewReader(buf.Bytes()),
+ )
})
}
diff --git a/src/http/http.go b/src/http/http.go
index dc2569a..ba81577 100644
--- a/src/http/http.go
+++ b/src/http/http.go
@@ -36,9 +36,10 @@ type Params struct {
PowManager pow.Manager
Cache cache.Cache
- PostStore post.Store
- PostAssetStore asset.Store
- PostDraftStore post.DraftStore
+ PostStore post.Store
+ PostAssetStore asset.Store
+ PostAssetLoader asset.Loader
+ PostDraftStore post.DraftStore
MailingList mailinglist.MailingList
diff --git a/src/http/posts.go b/src/http/posts.go
index 872ea89..5c7ac25 100644
--- a/src/http/posts.go
+++ b/src/http/posts.go
@@ -18,6 +18,7 @@ import (
"github.com/mediocregopher/blog.mediocregopher.com/srv/gmi"
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
+ "github.com/mediocregopher/blog.mediocregopher.com/srv/post/asset"
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
)
@@ -49,7 +50,7 @@ func (a *api) postPreprocessFuncImage(args ...string) (string, error) {
}{
ID: id,
Descr: descr,
- Resizable: isImgResizable(id),
+ Resizable: asset.IsImageResizable(id),
}
buf := new(bytes.Buffer)
diff --git a/src/post/asset/loader.go b/src/post/asset/loader.go
new file mode 100644
index 0000000..f7a0f5e
--- /dev/null
+++ b/src/post/asset/loader.go
@@ -0,0 +1,43 @@
+package asset
+
+import (
+ "errors"
+ "io"
+ "path/filepath"
+)
+
+var (
+ ErrCannotResize = errors.New("cannot resize")
+)
+
+// LoadOpts are optional parameters to Loader's Load method. Some may only apply
+// to specific Loader implementations.
+type LoadOpts struct {
+
+ // ImageWidth is used by the ImageLoader to resize images on the fly.
+ ImageWidth int
+}
+
+// Loader is used to load an asset and write its body into the given io.Writer.
+//
+// Errors:
+// - ErrNotFound
+// - ErrCannotResize (only if ImageLoader is used)
+type Loader interface {
+ Load(path string, into io.Writer, opts LoadOpts) error
+}
+
+type storeLoader struct {
+ store Store
+}
+
+// NewStoreLoader returns a Loader which loads assets directly from the given
+// Store, with no transformation.
+func NewStoreLoader(store Store) Loader {
+ return &storeLoader{store}
+}
+
+func (l *storeLoader) Load(path string, into io.Writer, opts LoadOpts) error {
+ id := filepath.Base(path)
+ return l.store.Get(id, into)
+}
diff --git a/src/post/asset/loader_archive.go b/src/post/asset/loader_archive.go
new file mode 100644
index 0000000..1c8697f
--- /dev/null
+++ b/src/post/asset/loader_archive.go
@@ -0,0 +1,99 @@
+package asset
+
+import (
+ "bytes"
+ "compress/gzip"
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "strings"
+
+ "github.com/omeid/go-tarfs"
+)
+
+type archiveLoader struct {
+ loader Loader
+}
+
+// NewArchiveLoader wraps an existing Loader in order to support extracting
+// files from within an archive file asset.
+//
+// For example, loading the path `foo.tgz/foo/bar.jpg` will call
+// `Load("foo.tgz")`, load that archive into memory, and serve the file
+// `./foo/bar.jpg` from within the archive.
+func NewArchiveLoader(loader Loader) Loader {
+ return &archiveLoader{loader: loader}
+}
+
+func (l *archiveLoader) Load(path string, into io.Writer, opts LoadOpts) error {
+
+ id, subPath, ok := strings.Cut(strings.TrimPrefix(path, "/"), "/")
+
+ if !ok {
+ return l.loader.Load(path, into, opts)
+ }
+
+ var isGzipped bool
+
+ switch {
+
+ case strings.HasSuffix(id, ".tar.gz"),
+ strings.HasSuffix(id, ".tgz"):
+ isGzipped = true
+
+ case strings.HasSuffix(id, ".tar"):
+ // ok
+
+ default:
+ // unsupported
+ return l.loader.Load(path, into, opts)
+ }
+
+ buf := new(bytes.Buffer)
+
+ if err := l.loader.Load(id, buf, opts); err != nil {
+ return fmt.Errorf("loading archive into buffer: %w", err)
+ }
+
+ var (
+ from io.Reader = buf
+ err error
+ )
+
+ if isGzipped {
+
+ if from, err = gzip.NewReader(from); err != nil {
+ return fmt.Errorf("decompressing archive asset with id %q: %w", id, err)
+ }
+ }
+
+ tarFS, err := tarfs.New(from)
+
+ if err != nil {
+ return fmt.Errorf("reading archive asset with id %q as fs: %w", id, err)
+ }
+
+ f, err := tarFS.Open(subPath)
+
+ if errors.Is(err, fs.ErrExist) {
+ return ErrNotFound
+
+ } else if err != nil {
+ return fmt.Errorf(
+ "opening path %q from archive asset with id %q as fs: %w",
+ subPath, id, err,
+ )
+ }
+
+ defer f.Close()
+
+ if _, err = io.Copy(into, f); err != nil {
+ return fmt.Errorf(
+ "reading %q from archive asset with id %q as fs: %w",
+ subPath, id, err,
+ )
+ }
+
+ return nil
+}
diff --git a/src/post/asset/loader_image.go b/src/post/asset/loader_image.go
new file mode 100644
index 0000000..dc996a9
--- /dev/null
+++ b/src/post/asset/loader_image.go
@@ -0,0 +1,85 @@
+package asset
+
+import (
+ "bytes"
+ "fmt"
+ "image"
+ "image/jpeg"
+ "image/png"
+ "io"
+ "path/filepath"
+ "strings"
+
+ "golang.org/x/image/draw"
+)
+
+// IsImageResizable returns whether or not an image can be resized, based on its
+// extension.
+func IsImageResizable(path string) bool {
+ switch strings.ToLower(filepath.Ext(path)) {
+ case ".jpg", ".jpeg", ".png":
+ return true
+ default:
+ return false
+ }
+}
+
+type imageLoader struct {
+ loader Loader
+}
+
+// NewImageLoader wraps an existing Loader in order to perform various
+// image-related transformations on any image assets being loaded. Non-image
+// assets are loaded as-is.
+func NewImageLoader(loader Loader) Loader {
+ return &imageLoader{loader}
+}
+
+func (l *imageLoader) Load(path string, into io.Writer, opts LoadOpts) error {
+
+ if opts.ImageWidth == 0 {
+ return l.loader.Load(path, into, opts)
+ }
+
+ if !IsImageResizable(path) {
+ return ErrCannotResize
+ }
+
+ buf := new(bytes.Buffer)
+
+ if err := l.loader.Load(path, buf, opts); err != nil {
+ return fmt.Errorf("loading image into buffer: %w", err)
+ }
+
+ img, format, err := image.Decode(buf)
+ if err != nil {
+ return fmt.Errorf("decoding image: %w", err)
+ }
+
+ maxWidth := float64(opts.ImageWidth)
+ 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(into, img, nil)
+ case "png":
+ return png.Encode(into, img)
+ default:
+ return fmt.Errorf("unknown image format %q", format)
+ }
+
+}