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:
parent
7872296b83
commit
961ab48ca7
@ -5,64 +5,17 @@ import (
|
|||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
"image/jpeg"
|
|
||||||
"image/png"
|
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/post/asset"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/post/asset"
|
||||||
"github.com/omeid/go-tarfs"
|
"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 {
|
func (a *api) managePostAssetsHandler() http.Handler {
|
||||||
|
|
||||||
tpl := a.mustParseBasedTpl("post-assets-manage.html")
|
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(
|
func (a *api) writePostAsset(
|
||||||
rw http.ResponseWriter,
|
rw http.ResponseWriter,
|
||||||
r *http.Request,
|
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)
|
tarFS, err := tarfs.New(from)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
43
src/post/asset/loader.go
Normal file
43
src/post/asset/loader.go
Normal 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)
|
||||||
|
}
|
57
src/post/asset/loader_archive.go
Normal file
57
src/post/asset/loader_archive.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
83
src/post/asset/loader_image.go
Normal file
83
src/post/asset/loader_image.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user