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)) }) }