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