This moved a bunch of logic out of http and into the asset package, making it available for gmit too.main
parent
7872296b83
commit
5559e01343
@ -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) |
||||||
|
} |
@ -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 |
||||||
|
} |
@ -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) |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue