summaryrefslogtreecommitdiff
path: root/src/post/asset
diff options
context:
space:
mode:
Diffstat (limited to 'src/post/asset')
-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
3 files changed, 227 insertions, 0 deletions
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)
+ }
+
+}