You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
312 lines
6.1 KiB
312 lines
6.1 KiB
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/asset"
|
|
"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 {
|
|
|
|
tpl := a.mustParseBasedTpl("post-assets-manage.html")
|
|
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
ids, err := a.params.PostAssetStore.List()
|
|
|
|
if err != nil {
|
|
apiutil.InternalServerError(
|
|
rw, r, fmt.Errorf("getting list of asset ids: %w", err),
|
|
)
|
|
return
|
|
}
|
|
|
|
tplPayload := struct {
|
|
IDs []string
|
|
}{
|
|
IDs: ids,
|
|
}
|
|
|
|
executeTemplate(rw, r, tpl, tplPayload)
|
|
})
|
|
}
|
|
|
|
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, asset.ErrNotFound) {
|
|
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
|
|
|
|
if info.isGzipped {
|
|
|
|
if from, err = gzip.NewReader(from); err != nil {
|
|
apiutil.InternalServerError(
|
|
rw, r,
|
|
fmt.Errorf("decompressing archive asset with id %q: %w", info.id, 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),
|
|
)
|
|
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,
|
|
),
|
|
)
|
|
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)
|
|
return
|
|
}
|
|
|
|
id := filepath.Base(r.URL.Path)
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
err := a.params.PostAssetStore.Get(id, buf)
|
|
|
|
if errors.Is(err, asset.ErrNotFound) {
|
|
http.Error(rw, "Asset not found", 404)
|
|
return
|
|
} else if err != nil {
|
|
apiutil.InternalServerError(
|
|
rw, r, fmt.Errorf("fetching asset with id %q: %w", id, err),
|
|
)
|
|
return
|
|
}
|
|
|
|
a.writePostAsset(rw, r, id, bytes.NewReader(buf.Bytes()))
|
|
})
|
|
}
|
|
|
|
func (a *api) postPostAssetHandler() http.Handler {
|
|
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
id := r.PostFormValue("id")
|
|
if id == "" {
|
|
apiutil.BadRequest(rw, r, errors.New("id is required"))
|
|
return
|
|
}
|
|
|
|
file, _, err := r.FormFile("file")
|
|
if err != nil {
|
|
apiutil.BadRequest(rw, r, fmt.Errorf("reading multipart file: %w", err))
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
if err := a.params.PostAssetStore.Set(id, file); err != nil {
|
|
apiutil.InternalServerError(rw, r, fmt.Errorf("storing file: %w", err))
|
|
return
|
|
}
|
|
|
|
a.executeRedirectTpl(rw, r, a.manageAssetsURL(false))
|
|
})
|
|
}
|
|
|
|
func (a *api) deletePostAssetHandler() http.Handler {
|
|
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
id := filepath.Base(r.URL.Path)
|
|
|
|
if id == "/" {
|
|
apiutil.BadRequest(rw, r, errors.New("id is required"))
|
|
return
|
|
}
|
|
|
|
err := a.params.PostAssetStore.Delete(id)
|
|
|
|
if errors.Is(err, asset.ErrNotFound) {
|
|
http.Error(rw, "Asset not found", 404)
|
|
return
|
|
} else if err != nil {
|
|
apiutil.InternalServerError(
|
|
rw, r, fmt.Errorf("deleting asset with id %q: %w", id, err),
|
|
)
|
|
return
|
|
}
|
|
|
|
a.executeRedirectTpl(rw, r, a.manageAssetsURL(false))
|
|
})
|
|
}
|
|
|