|
|
|
@ -2,67 +2,17 @@ package http |
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
"bytes" |
|
|
|
|
"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" |
|
|
|
|
"github.com/omeid/go-tarfs" |
|
|
|
|
"golang.org/x/image/draw" |
|
|
|
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/post/asset" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
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,173 +38,47 @@ 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, |
|
|
|
|
path string, |
|
|
|
|
from io.ReadSeeker, |
|
|
|
|
) { |
|
|
|
|
|
|
|
|
|
maxWidth, err := apiutil.StrToInt(r.FormValue("w"), 0) |
|
|
|
|
if err != nil { |
|
|
|
|
apiutil.BadRequest(rw, r, fmt.Errorf("invalid w parameter: %w", err)) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if maxWidth == 0 { |
|
|
|
|
http.ServeContent(rw, r, path, time.Time{}, from) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if !isImgResizable(path) { |
|
|
|
|
apiutil.BadRequest(rw, r, fmt.Errorf("cannot resize asset %q", path)) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
resizedBuf := new(bytes.Buffer) |
|
|
|
|
|
|
|
|
|
if err := resizeImage(resizedBuf, from, float64(maxWidth)); err != nil { |
|
|
|
|
apiutil.InternalServerError( |
|
|
|
|
rw, r, |
|
|
|
|
fmt.Errorf( |
|
|
|
|
"resizing image %q to size %d: %w", |
|
|
|
|
path, maxWidth, err, |
|
|
|
|
), |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
http.ServeContent( |
|
|
|
|
rw, r, path, time.Time{}, bytes.NewReader(resizedBuf.Bytes()), |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (a *api) handleGetPostAssetArchive( |
|
|
|
|
rw http.ResponseWriter, |
|
|
|
|
r *http.Request, |
|
|
|
|
info postAssetArchiveInfo, |
|
|
|
|
) { |
|
|
|
|
|
|
|
|
|
buf := new(bytes.Buffer) |
|
|
|
|
|
|
|
|
|
err := a.params.PostAssetStore.Get(info.id, buf) |
|
|
|
|
|
|
|
|
|
if errors.Is(err, post.ErrAssetNotFound) { |
|
|
|
|
http.Error(rw, "asset not found", 404) |
|
|
|
|
return |
|
|
|
|
} else if err != nil { |
|
|
|
|
apiutil.InternalServerError( |
|
|
|
|
rw, r, |
|
|
|
|
fmt.Errorf("fetching archive asset with id %q: %w", info.id, err), |
|
|
|
|
) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var from io.Reader = buf |
|
|
|
|
func (a *api) getPostAssetHandler() http.Handler { |
|
|
|
|
|
|
|
|
|
if info.isGzipped { |
|
|
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { |
|
|
|
|
|
|
|
|
|
if from, err = gzip.NewReader(from); err != nil { |
|
|
|
|
apiutil.InternalServerError( |
|
|
|
|
rw, r, |
|
|
|
|
fmt.Errorf("decompressing archive asset with id %q: %w", info.id, err), |
|
|
|
|
) |
|
|
|
|
maxWidth, err := apiutil.StrToInt(r.FormValue("w"), 0) |
|
|
|
|
if err != nil { |
|
|
|
|
apiutil.BadRequest(rw, r, fmt.Errorf("invalid w parameter: %w", err)) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
tarFS, err := tarfs.New(from) |
|
|
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
apiutil.InternalServerError( |
|
|
|
|
rw, r, |
|
|
|
|
fmt.Errorf("reading archive asset with id %q as fs: %w", info.id, err), |
|
|
|
|
var ( |
|
|
|
|
path = strings.TrimPrefix(r.URL.Path, "/") |
|
|
|
|
buf = new(bytes.Buffer) |
|
|
|
|
) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
f, err := tarFS.Open(info.subPath) |
|
|
|
|
|
|
|
|
|
if errors.Is(err, fs.ErrExist) { |
|
|
|
|
http.Error(rw, "Asset not found", 404) |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
} else if err != nil { |
|
|
|
|
|
|
|
|
|
apiutil.InternalServerError( |
|
|
|
|
rw, r, |
|
|
|
|
fmt.Errorf( |
|
|
|
|
"opening path %q from archive asset with id %q as fs: %w", |
|
|
|
|
info.subPath, info.id, err, |
|
|
|
|
), |
|
|
|
|
err = a.params.PostAssetLoader.Load( |
|
|
|
|
path, |
|
|
|
|
buf, |
|
|
|
|
asset.LoadOpts{ |
|
|
|
|
ImageWidth: maxWidth, |
|
|
|
|
}, |
|
|
|
|
) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
defer f.Close() |
|
|
|
|
|
|
|
|
|
a.writePostAsset(rw, r, info.path, f) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (a *api) getPostAssetHandler() http.Handler { |
|
|
|
|
|
|
|
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { |
|
|
|
|
|
|
|
|
|
archiveInfo, ok := extractPostAssetArchiveInfo(r.URL.Path) |
|
|
|
|
|
|
|
|
|
if ok { |
|
|
|
|
a.handleGetPostAssetArchive(rw, r, archiveInfo) |
|
|
|
|
if errors.Is(err, asset.ErrNotFound) { |
|
|
|
|
http.Error(rw, "Asset not found", 404) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
id := filepath.Base(r.URL.Path) |
|
|
|
|
|
|
|
|
|
buf := new(bytes.Buffer) |
|
|
|
|
|
|
|
|
|
err := a.params.PostAssetStore.Get(id, buf) |
|
|
|
|
|
|
|
|
|
if errors.Is(err, post.ErrAssetNotFound) { |
|
|
|
|
http.Error(rw, "Asset not found", 404) |
|
|
|
|
} else if errors.Is(err, asset.ErrCannotResize) { |
|
|
|
|
http.Error(rw, "Image resizing not supported", 400) |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
} else if err != nil { |
|
|
|
|
apiutil.InternalServerError( |
|
|
|
|
rw, r, fmt.Errorf("fetching asset with id %q: %w", id, err), |
|
|
|
|
rw, r, fmt.Errorf("fetching asset at path %q: %w", path, err), |
|
|
|
|
) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
a.writePostAsset(rw, r, id, bytes.NewReader(buf.Bytes())) |
|
|
|
|
http.ServeContent( |
|
|
|
|
rw, r, path, time.Time{}, bytes.NewReader(buf.Bytes()), |
|
|
|
|
) |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -297,7 +121,7 @@ func (a *api) deletePostAssetHandler() http.Handler { |
|
|
|
|
|
|
|
|
|
err := a.params.PostAssetStore.Delete(id) |
|
|
|
|
|
|
|
|
|
if errors.Is(err, post.ErrAssetNotFound) { |
|
|
|
|
if errors.Is(err, asset.ErrNotFound) { |
|
|
|
|
http.Error(rw, "Asset not found", 404) |
|
|
|
|
return |
|
|
|
|
} else if err != nil { |
|
|
|
|