refactor(10.2-01): nest framework packages under modules
- Move remaining beach packages and embedded admin assets\n- Rewrite framework, example, build, and gate paths
This commit is contained in:
177
modules/lagoon/attach/thumb.go
Normal file
177
modules/lagoon/attach/thumb.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package attach
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"gocloud.dev/blob"
|
||||
)
|
||||
|
||||
const (
|
||||
maxThumbEdge = 4096
|
||||
maxThumbSourceBytes = 32 << 20
|
||||
maxThumbSourcePixels = 4096 * 4096
|
||||
)
|
||||
|
||||
// thumbToken is the alphabet allowed for the mode and extension segments of a
|
||||
// thumb filename. Both are interpolated into a blob key, which fileblob maps
|
||||
// to a filesystem path, so separators and dots must never reach it.
|
||||
var thumbToken = regexp.MustCompile(`^[a-z0-9]+$`)
|
||||
|
||||
// ThumbFilename is Winter File::getThumbFilename: thumb_<id>_<w>_<h>_<ox>_<oy>_<mode>.<ext>.
|
||||
// A mode or ext outside [a-z0-9]+ is coerced to "auto" / "jpg" so the result
|
||||
// is always a single safe path element; File.Thumb rejects such input instead.
|
||||
func ThumbFilename(id uint, w, h int, offsetX, offsetY int, mode, ext string) string {
|
||||
if !thumbToken.MatchString(mode) {
|
||||
mode = "auto"
|
||||
}
|
||||
if !thumbToken.MatchString(ext) {
|
||||
ext = "jpg"
|
||||
}
|
||||
return fmt.Sprintf("thumb_%d_%d_%d_%d_%d_%s.%s", id, w, h, offsetX, offsetY, mode, ext)
|
||||
}
|
||||
|
||||
// PartitionDirectory is Winter File::getPartitionDirectory: first 9 chars of
|
||||
// disk_name split into 3 groups of 3, joined by '/', with a trailing slash.
|
||||
func PartitionDirectory(diskName string) string {
|
||||
var groups []string
|
||||
for i := 0; i < len(diskName) && len(groups) < 3; i += 3 {
|
||||
end := i + 3
|
||||
if end > len(diskName) {
|
||||
end = len(diskName)
|
||||
}
|
||||
groups = append(groups, diskName[i:end])
|
||||
}
|
||||
return strings.Join(groups, "/") + "/"
|
||||
}
|
||||
|
||||
// BlobKey is the Winter on-disk key for an original file: partition + disk_name.
|
||||
func BlobKey(diskName string) string {
|
||||
return PartitionDirectory(diskName) + diskName
|
||||
}
|
||||
|
||||
func fileExt(diskName string) string {
|
||||
ext := strings.TrimPrefix(path.Ext(diskName), ".")
|
||||
if ext == "" {
|
||||
return "jpg"
|
||||
}
|
||||
return strings.ToLower(ext)
|
||||
}
|
||||
|
||||
func publicURL(key string) string {
|
||||
prefix := strings.TrimRight(PublicPathPrefix(), "/")
|
||||
key = strings.TrimLeft(key, "/")
|
||||
if prefix == "" {
|
||||
return "/" + key
|
||||
}
|
||||
return prefix + "/" + key
|
||||
}
|
||||
|
||||
func defaultResizeImage(src image.Image, w, h int, mode string) image.Image {
|
||||
switch strings.ToLower(mode) {
|
||||
case "crop":
|
||||
return imaging.Fill(src, w, h, imaging.Center, imaging.Lanczos)
|
||||
case "exact":
|
||||
return imaging.Resize(src, w, h, imaging.Lanczos)
|
||||
default:
|
||||
return imaging.Fit(src, w, h, imaging.Lanczos)
|
||||
}
|
||||
}
|
||||
|
||||
var resizeImage = defaultResizeImage
|
||||
|
||||
func defaultEncodeImage(w io.Writer, img image.Image, ext string) error {
|
||||
format := imaging.JPEG
|
||||
switch strings.ToLower(ext) {
|
||||
case "png":
|
||||
format = imaging.PNG
|
||||
case "gif":
|
||||
format = imaging.GIF
|
||||
}
|
||||
return imaging.Encode(w, img, format)
|
||||
}
|
||||
|
||||
var encodeImage = defaultEncodeImage
|
||||
|
||||
// Thumb returns the public URL of a lazily generated thumbnail. The second
|
||||
// call for the same dimensions hits the existing blob and does not resize.
|
||||
func (f *File) Thumb(ctx context.Context, bucket *blob.Bucket, w, h int, mode string) (string, error) {
|
||||
if f == nil {
|
||||
return "", fmt.Errorf("attach: file is nil")
|
||||
}
|
||||
if bucket == nil {
|
||||
return "", fmt.Errorf("attach: bucket is nil")
|
||||
}
|
||||
if mode == "" {
|
||||
mode = "auto"
|
||||
}
|
||||
mode = strings.ToLower(mode)
|
||||
if !thumbToken.MatchString(mode) {
|
||||
return "", fmt.Errorf("attach: invalid thumb mode %q", mode)
|
||||
}
|
||||
if w <= 0 || h <= 0 || w > maxThumbEdge || h > maxThumbEdge {
|
||||
return "", fmt.Errorf("attach: thumb size %dx%d is out of range", w, h)
|
||||
}
|
||||
ext := fileExt(f.DiskName)
|
||||
if !thumbToken.MatchString(ext) {
|
||||
return "", fmt.Errorf("attach: invalid thumb extension %q", ext)
|
||||
}
|
||||
thumbName := ThumbFilename(f.ID, w, h, 0, 0, mode, ext)
|
||||
part := PartitionDirectory(f.DiskName)
|
||||
thumbKey := part + thumbName
|
||||
exists, err := bucket.Exists(ctx, thumbKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("attach: thumb exists: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return publicURL(thumbKey), nil
|
||||
}
|
||||
origKey := part + f.DiskName
|
||||
r, err := bucket.NewReader(ctx, origKey, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("attach: read original: %w", err)
|
||||
}
|
||||
src, _, err := image.Decode(io.LimitReader(r, maxThumbSourceBytes))
|
||||
closeErr := r.Close()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("attach: decode original: %w", err)
|
||||
}
|
||||
if closeErr != nil {
|
||||
return "", closeErr
|
||||
}
|
||||
bounds := src.Bounds()
|
||||
if int64(bounds.Dx())*int64(bounds.Dy()) > maxThumbSourcePixels {
|
||||
return "", fmt.Errorf("attach: original image is too large")
|
||||
}
|
||||
resized := resizeImage(src, w, h, mode)
|
||||
contentType := "image/jpeg"
|
||||
switch ext {
|
||||
case "png":
|
||||
contentType = "image/png"
|
||||
case "gif":
|
||||
contentType = "image/gif"
|
||||
}
|
||||
wr, err := bucket.NewWriter(ctx, thumbKey, &blob.WriterOptions{ContentType: contentType})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("attach: thumb writer: %w", err)
|
||||
}
|
||||
encErr := encodeImage(wr, resized, ext)
|
||||
closeErr = wr.Close()
|
||||
if encErr != nil || closeErr != nil {
|
||||
_ = deleteKey(ctx, bucket, thumbKey)
|
||||
if encErr != nil {
|
||||
return "", encErr
|
||||
}
|
||||
return "", closeErr
|
||||
}
|
||||
return publicURL(thumbKey), nil
|
||||
}
|
||||
Reference in New Issue
Block a user