Files
imgproxy/imagedata/factory.go
Victor Sokolov 37a6ea2bf7 IMG-49: Get rid of global watermark image and fetcher (#1515)
* Get rid of global watermark image

* imagedata.Factory, removed global Fetcher
2025-09-05 16:22:57 +02:00

182 lines
4.1 KiB
Go

package imagedata
import (
"bytes"
"context"
"encoding/base64"
"io"
"net/http"
"os"
"github.com/imgproxy/imgproxy/v3/asyncbuffer"
"github.com/imgproxy/imgproxy/v3/fetcher"
"github.com/imgproxy/imgproxy/v3/imagetype"
"github.com/imgproxy/imgproxy/v3/security"
)
// Factory represents ImageData factory
type Factory struct {
fetcher *fetcher.Fetcher
}
// NewFactory creates a new factory
func NewFactory(fetcher *fetcher.Fetcher) *Factory {
return &Factory{
fetcher: fetcher,
}
}
// NewFromBytesWithFormat creates a new ImageData instance from the provided format
// and byte slice.
func NewFromBytesWithFormat(format imagetype.Type, b []byte) ImageData {
return &imageDataBytes{
data: b,
format: format,
cancel: nil,
}
}
// NewFromBytes creates a new ImageData instance from the provided byte slice.
func (f *Factory) NewFromBytes(b []byte) (ImageData, error) {
r := bytes.NewReader(b)
format, err := imagetype.Detect(r)
if err != nil {
return nil, err
}
return NewFromBytesWithFormat(format, b), nil
}
// NewFromPath creates a new ImageData from an os.File
func (f *Factory) NewFromPath(path string) (ImageData, error) {
fl, err := os.Open(path)
if err != nil {
return nil, err
}
defer fl.Close()
b, err := io.ReadAll(fl)
if err != nil {
return nil, err
}
return f.NewFromBytes(b)
}
// NewFromBase64 creates a new ImageData from a base64 encoded byte slice
func (f *Factory) NewFromBase64(encoded string) (ImageData, error) {
b, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return nil, err
}
return f.NewFromBytes(b)
}
// sendRequest is a common logic between sync and async download.
func (f *Factory) sendRequest(ctx context.Context, url string, opts DownloadOptions) (*fetcher.Request, *http.Response, http.Header, error) {
h := make(http.Header)
// NOTE: This will be removed in the future when our test context gets better isolation
if len(redirectAllRequestsTo) > 0 {
url = redirectAllRequestsTo
}
req, err := f.fetcher.BuildRequest(ctx, url, opts.Header, opts.CookieJar)
if err != nil {
return req, nil, h, err
}
res, err := req.Fetch()
if res != nil {
h = res.Header.Clone()
}
if err != nil {
if res != nil {
res.Body.Close()
}
req.Cancel()
return req, nil, h, err
}
res, err = security.LimitResponseSize(res, opts.MaxSrcFileSize)
if err != nil {
if res != nil {
res.Body.Close()
}
req.Cancel()
return req, nil, h, err
}
return req, res, h, nil
}
// DownloadSync downloads the image synchronously and returns the ImageData and HTTP headers.
func (f *Factory) DownloadSync(ctx context.Context, imageURL, desc string, opts DownloadOptions) (ImageData, http.Header, error) {
if opts.DownloadFinished != nil {
defer opts.DownloadFinished()
}
req, res, h, err := f.sendRequest(ctx, imageURL, opts)
if res != nil {
defer res.Body.Close()
}
if req != nil {
defer req.Cancel()
}
if err != nil {
return nil, h, wrapDownloadError(err, desc)
}
b, err := io.ReadAll(res.Body)
if err != nil {
return nil, h, wrapDownloadError(err, desc)
}
format, err := imagetype.Detect(bytes.NewReader(b))
if err != nil {
return nil, h, wrapDownloadError(err, desc)
}
d := NewFromBytesWithFormat(format, b)
return d, h, nil
}
// DownloadAsync downloads the image asynchronously and returns the ImageData
// backed by AsyncBuffer and HTTP headers.
func (f *Factory) DownloadAsync(ctx context.Context, imageURL, desc string, opts DownloadOptions) (ImageData, http.Header, error) {
// We pass this responsibility to AsyncBuffer
//nolint:bodyclose
req, res, h, err := f.sendRequest(ctx, imageURL, opts)
if err != nil {
if opts.DownloadFinished != nil {
defer opts.DownloadFinished()
}
return nil, h, wrapDownloadError(err, desc)
}
b := asyncbuffer.New(res.Body, int(res.ContentLength), opts.DownloadFinished)
format, err := imagetype.Detect(b.Reader())
if err != nil {
b.Close()
req.Cancel()
return nil, h, wrapDownloadError(err, desc)
}
d := &imageDataAsyncBuffer{
b: b,
format: format,
desc: desc,
cancel: nil,
}
d.AddCancel(req.Cancel) // request will be closed when the image data is consumed
return d, h, nil
}