WIP: implementing asset.Loader

the image loader is implemented, working on the archive loader, I left a
note in the HTTP code with that was left off.

Once implemented then the get asset methods in both http and gmi should
use the loader directly, rather than loading from the store and doing
transforms. This way gmi will also gain the image transformations and
archive loading that http has (though I dunno if it really needs it).
This commit is contained in:
Brian Picciano 2023-04-15 21:35:06 +02:00
parent 7872296b83
commit 961ab48ca7
4 changed files with 185 additions and 79 deletions

View File

@ -5,64 +5,17 @@ import (
"compress/gzip"
"errors"
"fmt"
"image"
"image/jpeg"
"image/png"
"io"
"io/fs"
"net/http"
"path/filepath"
"strings"
"time"
"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,38 +41,6 @@ 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,
@ -194,6 +115,8 @@ func (a *api) handleGetPostAssetArchive(
}
}
// TODO everything from here down needs to be moved to the archive loader
tarFS, err := tarfs.New(from)
if err != nil {

43
src/post/asset/loader.go Normal file
View File

@ -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)
}

View File

@ -0,0 +1,57 @@
package asset
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"strings"
)
type archiveLoader struct {
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)
}
}
}

View File

@ -0,0 +1,83 @@
package asset
import (
"bytes"
"fmt"
"image"
"image/jpeg"
"image/png"
"io"
"path/filepath"
"strings"
"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
}
}
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 !isImgResizable(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)
}
}