From 68f3215df6e2e4f345076dd5b20b9bf5867353cf Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sat, 15 Apr 2023 20:47:50 +0200 Subject: Add support for http loading files from an asset archive --- src/http/assets.go | 189 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 152 insertions(+), 37 deletions(-) (limited to 'src/http/assets.go') diff --git a/src/http/assets.go b/src/http/assets.go index 9c6c67e..5b26a2e 100644 --- a/src/http/assets.go +++ b/src/http/assets.go @@ -2,23 +2,27 @@ 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" ) -func isImgResizable(id string) bool { - switch strings.ToLower(filepath.Ext(id)) { +func isImgResizable(path string) bool { + switch strings.ToLower(filepath.Ext(path)) { case ".jpg", ".jpeg", ".png": return true default: @@ -84,62 +88,173 @@ func (a *api) managePostAssetsHandler() http.Handler { }) } -func (a *api) getPostAssetHandler() http.Handler { +type postAssetArchiveInfo struct { + path string + id string + subPath string + isGzipped bool +} - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { +func extractPostAssetArchiveInfo(path string) (postAssetArchiveInfo, bool) { - id := filepath.Base(r.URL.Path) + var info postAssetArchiveInfo - maxWidth, err := apiutil.StrToInt(r.FormValue("w"), 0) - if err != nil { - apiutil.BadRequest(rw, r, fmt.Errorf("invalid w parameter: %w", err)) - return - } + info.path = strings.TrimPrefix(path, "/") - buf := new(bytes.Buffer) + info.id, info.subPath, _ = strings.Cut(info.path, "/") - err = a.params.PostAssetStore.Get(id, buf) + switch { - if errors.Is(err, post.ErrAssetNotFound) { - http.Error(rw, "Asset not found", 404) - return - } else if err != nil { + 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 + + if info.isGzipped { + + if from, err = gzip.NewReader(from); err != nil { apiutil.InternalServerError( - rw, r, fmt.Errorf("fetching asset with id %q: %w", id, err), + rw, r, + fmt.Errorf("decompressing archive asset with id %q: %w", info.id, err), ) return } + } - if maxWidth == 0 { + tarFS, err := tarfs.New(from) - if _, err := io.Copy(rw, buf); err != nil { - apiutil.InternalServerError( - rw, r, - fmt.Errorf( - "copying asset with id %q to response writer: %w", - id, err, - ), - ) - } + if err != nil { + apiutil.InternalServerError( + rw, r, + fmt.Errorf("reading archive asset with id %q as fs: %w", info.id, err), + ) + return + } - return - } + f, err := tarFS.Open(info.subPath) + + if errors.Is(err, fs.ErrExist) { + http.Error(rw, "Asset not found", 404) + return - if !isImgResizable(id) { - apiutil.BadRequest(rw, r, fmt.Errorf("cannot resize file %q", id)) + } 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 } - if err := resizeImage(rw, buf, float64(maxWidth)); err != nil { + 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) + return + } else if err != nil { apiutil.InternalServerError( - rw, r, - fmt.Errorf( - "resizing image with id %q to size %d: %w", - id, maxWidth, err, - ), + rw, r, fmt.Errorf("fetching asset with id %q: %w", id, err), ) + return } + a.writePostAsset(rw, r, id, bytes.NewReader(buf.Bytes())) }) } -- cgit v1.2.3