mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-09-16 09:36:18 +02:00
IMG-13: http.Header, *ImageData -> ImageData (#1473)
* ImageData.Headers() * *ImageData -> ImageData * withmatt -> httpheaders of our own * .Clone() headers, nil * NewFromBytesWithFormat -> nil * svg.go -> do not Clone()
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||
"github.com/imgproxy/imgproxy/v3/options"
|
||||
)
|
||||
@@ -106,10 +107,10 @@ func (h *Handler) ImageEtagExpected() string {
|
||||
return h.imgEtagExpected
|
||||
}
|
||||
|
||||
func (h *Handler) SetActualImageData(imgdata *imagedata.ImageData) (bool, error) {
|
||||
func (h *Handler) SetActualImageData(imgdata imagedata.ImageData) (bool, error) {
|
||||
var haveActualImgETag bool
|
||||
h.imgEtagActual, haveActualImgETag = imgdata.Headers["ETag"]
|
||||
haveActualImgETag = haveActualImgETag && len(h.imgEtagActual) > 0
|
||||
h.imgEtagActual = imgdata.Headers().Get(httpheaders.Etag)
|
||||
haveActualImgETag = len(h.imgEtagActual) > 0
|
||||
|
||||
// Just in case server didn't check ETag properly and returned the same one
|
||||
// as we expected
|
||||
|
@@ -2,7 +2,6 @@ package etag
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -11,6 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||
"github.com/imgproxy/imgproxy/v3/options"
|
||||
)
|
||||
@@ -24,8 +24,8 @@ type EtagTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
po *options.ProcessingOptions
|
||||
imgWithETag *imagedata.ImageData
|
||||
imgWithoutETag *imagedata.ImageData
|
||||
imgWithETag imagedata.ImageData
|
||||
imgWithoutETag imagedata.ImageData
|
||||
|
||||
h Handler
|
||||
}
|
||||
@@ -37,10 +37,11 @@ func (s *EtagTestSuite) SetupSuite() {
|
||||
d, err := os.ReadFile("../testdata/test1.jpg")
|
||||
s.Require().NoError(err)
|
||||
|
||||
imgWithETag, err := imagedata.NewFromBytes(d, http.Header{"ETag": []string{`"loremipsumdolor"`}})
|
||||
imgWithETag, err := imagedata.NewFromBytes(d)
|
||||
s.Require().NoError(err)
|
||||
imgWithETag.Headers().Add(httpheaders.Etag, `"loremipsumdolor"`)
|
||||
|
||||
imgWithoutETag, err := imagedata.NewFromBytes(d, make(http.Header))
|
||||
imgWithoutETag, err := imagedata.NewFromBytes(d)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.imgWithETag = imgWithETag
|
||||
@@ -101,7 +102,7 @@ func (s *EtagTestSuite) TestImageETagExpectedPresent() {
|
||||
s.h.ParseExpectedETag(etagReq)
|
||||
|
||||
//nolint:testifylint // False-positive expected-actual
|
||||
s.Require().Equal(s.imgWithETag.Headers["ETag"], s.h.ImageEtagExpected())
|
||||
s.Require().Equal(s.imgWithETag.Headers().Get(httpheaders.Etag), s.h.ImageEtagExpected())
|
||||
}
|
||||
|
||||
func (s *EtagTestSuite) TestImageETagExpectedBlank() {
|
||||
|
1
go.mod
1
go.mod
@@ -205,7 +205,6 @@ require (
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.withmatt.com/httpheaders v1.0.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
|
2
go.sum
2
go.sum
@@ -560,8 +560,6 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.withmatt.com/httpheaders v1.0.0 h1:xZhtLWyIWCd8FT3CvUBRQLhQpgZaMmHNfIIT0wwNc1A=
|
||||
go.withmatt.com/httpheaders v1.0.0/go.mod h1:bKAYNgm9s2ViHIoGOnMKo4F2zJXBdvpfGuSEJQYF8pQ=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
|
64
httpheaders/headers.go
Normal file
64
httpheaders/headers.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Inspired by https://github.com/mattrobenolt/go-httpheaders
|
||||
// Thanks, Matt Robenolt!
|
||||
package httpheaders
|
||||
|
||||
const (
|
||||
Accept = "Accept"
|
||||
AcceptCharset = "Accept-Charset"
|
||||
AcceptEncoding = "Accept-Encoding"
|
||||
AcceptLanguage = "Accept-Language"
|
||||
AcceptRanges = "Accept-Ranges"
|
||||
AccessControlAllowCredentials = "Access-Control-Allow-Credentials"
|
||||
AccessControlAllowHeaders = "Access-Control-Allow-Headers"
|
||||
AccessControlAllowMethods = "Access-Control-Allow-Methods"
|
||||
AccessControlAllowOrigin = "Access-Control-Allow-Origin"
|
||||
AccessControlMaxAge = "Access-Control-Max-Age"
|
||||
Age = "Age"
|
||||
AltSvc = "Alt-Svc"
|
||||
Authorization = "Authorization"
|
||||
CacheControl = "Cache-Control"
|
||||
Connection = "Connection"
|
||||
ContentDisposition = "Content-Disposition"
|
||||
ContentEncoding = "Content-Encoding"
|
||||
ContentLanguage = "Content-Language"
|
||||
ContentLength = "Content-Length"
|
||||
ContentRange = "Content-Range"
|
||||
ContentSecurityPolicy = "Content-Security-Policy"
|
||||
ContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only"
|
||||
ContentType = "Content-Type"
|
||||
Cookie = "Cookie"
|
||||
Date = "Date"
|
||||
Dnt = "Dnt"
|
||||
Etag = "Etag"
|
||||
Expect = "Expect"
|
||||
ExpectCt = "Expect-Ct"
|
||||
Expires = "Expires"
|
||||
Forwarded = "Forwarded"
|
||||
Host = "Host"
|
||||
IfMatch = "If-Match"
|
||||
IfModifiedSince = "If-Modified-Since"
|
||||
IfNoneMatch = "If-None-Match"
|
||||
IfUnmodifiedSince = "If-Unmodified-Since"
|
||||
KeepAlive = "Keep-Alive"
|
||||
LastModified = "Last-Modified"
|
||||
Link = "Link"
|
||||
Location = "Location"
|
||||
Origin = "Origin"
|
||||
Pragma = "Pragma"
|
||||
Referer = "Referer"
|
||||
RequestId = "Request-Id"
|
||||
RetryAfter = "Retry-After"
|
||||
Server = "Server"
|
||||
SetCookie = "Set-Cookie"
|
||||
StrictTransportSecurity = "Strict-Transport-Security"
|
||||
Upgrade = "Upgrade"
|
||||
UserAgent = "User-Agent"
|
||||
Vary = "Vary"
|
||||
Via = "Via"
|
||||
WwwAuthenticate = "Www-Authenticate"
|
||||
XContentTypeOptions = "X-Content-Type-Options"
|
||||
XForwardedFor = "X-Forwarded-For"
|
||||
XForwardedHost = "X-Forwarded-Host"
|
||||
XForwardedProto = "X-Forwarded-Proto"
|
||||
XFrameOptions = "X-Frame-Options"
|
||||
)
|
@@ -37,7 +37,7 @@ func initDownloading() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func download(ctx context.Context, imageURL string, opts DownloadOptions, secopts security.Options) (*ImageData, error) {
|
||||
func download(ctx context.Context, imageURL string, opts DownloadOptions, secopts security.Options) (ImageData, error) {
|
||||
// We use this for testing
|
||||
if len(redirectAllRequestsTo) > 0 {
|
||||
imageURL = redirectAllRequestsTo
|
||||
@@ -70,23 +70,13 @@ func download(ctx context.Context, imageURL string, opts DownloadOptions, secopt
|
||||
return nil, ierrors.Wrap(err, 0)
|
||||
}
|
||||
|
||||
h := make(map[string]string)
|
||||
for k := range res.Header {
|
||||
value := res.Header.Get(k)
|
||||
h[k] = value
|
||||
|
||||
// This is temporary workaround, will be addressed in the subsequent PR
|
||||
if k == "Etag" {
|
||||
h["ETag"] = value
|
||||
}
|
||||
|
||||
if k == "ETag" {
|
||||
h["Etag"] = value
|
||||
// NOTE: This will be removed in the future in favor of headers/image data separation
|
||||
for k, v := range res.Header {
|
||||
for _, v := range v {
|
||||
imgdata.Headers().Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
imgdata.Headers = h
|
||||
|
||||
return imgdata, nil
|
||||
}
|
||||
|
||||
|
@@ -2,15 +2,37 @@ package imagedata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"os"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/imagemeta"
|
||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||
"github.com/imgproxy/imgproxy/v3/security"
|
||||
)
|
||||
|
||||
// NewFromBytesWithFormat creates a new ImageData instance from the provided format,
|
||||
// http headers and byte slice.
|
||||
func NewFromBytesWithFormat(format imagetype.Type, b []byte, headers http.Header) ImageData {
|
||||
var h http.Header
|
||||
|
||||
if headers == nil {
|
||||
h = make(http.Header)
|
||||
} else {
|
||||
h = headers.Clone()
|
||||
}
|
||||
|
||||
return &imageDataBytes{
|
||||
data: b,
|
||||
format: format,
|
||||
headers: h,
|
||||
cancel: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// NewFromBytes creates a new ImageData instance from the provided byte slice.
|
||||
func NewFromBytes(b []byte, headers http.Header) (*ImageData, error) {
|
||||
func NewFromBytes(b []byte) (ImageData, error) {
|
||||
r := bytes.NewReader(b)
|
||||
|
||||
meta, err := imagemeta.DecodeMeta(r)
|
||||
@@ -18,20 +40,64 @@ func NewFromBytes(b []byte, headers http.Header) (*ImageData, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewFromBytesWithFormat(meta.Format(), b, headers)
|
||||
return NewFromBytesWithFormat(meta.Format(), b, nil), nil
|
||||
}
|
||||
|
||||
// NewFromBytesWithFormat creates a new ImageData instance from the provided format and byte slice.
|
||||
func NewFromBytesWithFormat(format imagetype.Type, b []byte, headers http.Header) (*ImageData, error) {
|
||||
// Temporary workaround for the old ImageData interface
|
||||
h := make(map[string]string, len(headers))
|
||||
for k, v := range headers {
|
||||
h[k] = strings.Join(v, ", ")
|
||||
// NewFromPath creates a new ImageData from an os.File
|
||||
func NewFromPath(path string, secopts security.Options) (ImageData, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fr, err := security.LimitFileSize(f, secopts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ImageData{
|
||||
data: b,
|
||||
format: format,
|
||||
Headers: h,
|
||||
}, nil
|
||||
b, err := io.ReadAll(fr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := bytes.NewReader(b)
|
||||
|
||||
// NOTE: This will be removed in the future in favor of VIPS metadata extraction
|
||||
// It's here temporarily to maintain compatibility with existing code
|
||||
meta, err := imagemeta.DecodeMeta(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = security.CheckMeta(meta, secopts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewFromBytes(b)
|
||||
}
|
||||
|
||||
// NewFromBase64 creates a new ImageData from a base64 encoded byte slice
|
||||
func NewFromBase64(encoded string, secopts security.Options) (ImageData, error) {
|
||||
b, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := bytes.NewReader(b)
|
||||
|
||||
// NOTE: This will be removed in the future in favor of VIPS metadata extraction
|
||||
// It's here temporarily to maintain compatibility with existing code
|
||||
meta, err := imagemeta.DecodeMeta(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = security.CheckMeta(meta, secopts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewFromBytes(b)
|
||||
}
|
||||
|
@@ -3,11 +3,9 @@ package imagedata
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
@@ -17,23 +15,35 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
Watermark *ImageData
|
||||
FallbackImage *ImageData
|
||||
Watermark ImageData
|
||||
FallbackImage ImageData
|
||||
)
|
||||
|
||||
type ImageData struct {
|
||||
type ImageData interface {
|
||||
io.Closer // Close closes the image data and releases any resources held by it
|
||||
Reader() io.ReadSeeker // Reader returns a new ReadSeeker for the image data
|
||||
Format() imagetype.Type // Format returns the image format from the metadata (shortcut)
|
||||
Size() (int, error) // Size returns the size of the image data in bytes
|
||||
AddCancel(context.CancelFunc) // AddCancel attaches a cancel function to the image data
|
||||
|
||||
// This will be removed in the future
|
||||
Headers() http.Header // Headers returns the HTTP headers of the image data, will be removed in the future
|
||||
}
|
||||
|
||||
// imageDataBytes represents image data stored in a byte slice in memory
|
||||
type imageDataBytes struct {
|
||||
format imagetype.Type
|
||||
data []byte
|
||||
Headers map[string]string
|
||||
headers http.Header
|
||||
|
||||
cancel context.CancelFunc
|
||||
cancel []context.CancelFunc
|
||||
cancelOnce sync.Once
|
||||
}
|
||||
|
||||
func (d *ImageData) Close() error {
|
||||
func (d *imageDataBytes) Close() error {
|
||||
d.cancelOnce.Do(func() {
|
||||
if d.cancel != nil {
|
||||
d.cancel()
|
||||
for _, cancel := range d.cancel {
|
||||
cancel()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -41,23 +51,27 @@ func (d *ImageData) Close() error {
|
||||
}
|
||||
|
||||
// Format returns the image format based on the metadata
|
||||
func (d *ImageData) Format() imagetype.Type {
|
||||
func (d *imageDataBytes) Format() imagetype.Type {
|
||||
return d.format
|
||||
}
|
||||
|
||||
// Reader returns an io.ReadSeeker for the image data
|
||||
func (d *ImageData) Reader() io.ReadSeeker {
|
||||
func (d *imageDataBytes) Reader() io.ReadSeeker {
|
||||
return bytes.NewReader(d.data)
|
||||
}
|
||||
|
||||
// Size returns the size of the image data in bytes.
|
||||
// NOTE: asyncbuffer implementation will .Wait() for the data to be fully read
|
||||
func (d *ImageData) Size() (int, error) {
|
||||
func (d *imageDataBytes) Size() (int, error) {
|
||||
return len(d.data), nil
|
||||
}
|
||||
|
||||
func (d *ImageData) SetCancel(cancel context.CancelFunc) {
|
||||
d.cancel = cancel
|
||||
func (d *imageDataBytes) Headers() http.Header {
|
||||
return d.headers
|
||||
}
|
||||
|
||||
func (d *imageDataBytes) AddCancel(cancel context.CancelFunc) {
|
||||
d.cancel = append(d.cancel, cancel)
|
||||
}
|
||||
|
||||
func Init() error {
|
||||
@@ -78,20 +92,34 @@ func Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadWatermark() (err error) {
|
||||
if len(config.WatermarkData) > 0 {
|
||||
Watermark, err = FromBase64(config.WatermarkData, "watermark", security.DefaultOptions())
|
||||
return
|
||||
}
|
||||
func loadWatermark() error {
|
||||
var err error
|
||||
|
||||
if len(config.WatermarkPath) > 0 {
|
||||
Watermark, err = FromFile(config.WatermarkPath, "watermark", security.DefaultOptions())
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case len(config.WatermarkData) > 0:
|
||||
Watermark, err = NewFromBase64(config.WatermarkData, security.DefaultOptions())
|
||||
|
||||
if len(config.WatermarkURL) > 0 {
|
||||
// NOTE: this should be something like err = ierrors.Wrap(err).WithStackDeep(0).WithPrefix("watermark")
|
||||
// In the NewFromBase64 all errors should be wrapped to something like
|
||||
// .WithPrefix("load from base64")
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't load watermark from Base64"))
|
||||
}
|
||||
|
||||
case len(config.WatermarkPath) > 0:
|
||||
Watermark, err = NewFromPath(config.WatermarkPath, security.DefaultOptions())
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't read watermark from file"))
|
||||
}
|
||||
|
||||
case len(config.WatermarkURL) > 0:
|
||||
Watermark, err = Download(context.Background(), config.WatermarkURL, "watermark", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
|
||||
return
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't download from URL"))
|
||||
}
|
||||
|
||||
default:
|
||||
Watermark = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -100,57 +128,35 @@ func loadWatermark() (err error) {
|
||||
func loadFallbackImage() (err error) {
|
||||
switch {
|
||||
case len(config.FallbackImageData) > 0:
|
||||
FallbackImage, err = FromBase64(config.FallbackImageData, "fallback image", security.DefaultOptions())
|
||||
FallbackImage, err = NewFromBase64(config.FallbackImageData, security.DefaultOptions())
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't load fallback image from Base64"))
|
||||
}
|
||||
|
||||
case len(config.FallbackImagePath) > 0:
|
||||
FallbackImage, err = FromFile(config.FallbackImagePath, "fallback image", security.DefaultOptions())
|
||||
FallbackImage, err = NewFromPath(config.FallbackImagePath, security.DefaultOptions())
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't read fallback image from file"))
|
||||
}
|
||||
|
||||
case len(config.FallbackImageURL) > 0:
|
||||
FallbackImage, err = Download(context.Background(), config.FallbackImageURL, "fallback image", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't download from URL"))
|
||||
}
|
||||
|
||||
default:
|
||||
FallbackImage, err = nil, nil
|
||||
FallbackImage = nil
|
||||
}
|
||||
|
||||
if FallbackImage != nil && err == nil && config.FallbackImageTTL > 0 {
|
||||
if FallbackImage.Headers == nil {
|
||||
FallbackImage.Headers = make(map[string]string)
|
||||
}
|
||||
FallbackImage.Headers["Fallback-Image"] = "1"
|
||||
FallbackImage.Headers().Set("Fallback-Image", "1")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func FromBase64(encoded, desc string, secopts security.Options) (*ImageData, error) {
|
||||
dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encoded))
|
||||
size := 4 * (len(encoded)/3 + 1)
|
||||
|
||||
imgdata, err := readAndCheckImage(dec, size, secopts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Can't decode %s: %s", desc, err)
|
||||
}
|
||||
|
||||
return imgdata, nil
|
||||
}
|
||||
|
||||
func FromFile(path, desc string, secopts security.Options) (*ImageData, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Can't read %s: %s", desc, err)
|
||||
}
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Can't read %s: %s", desc, err)
|
||||
}
|
||||
|
||||
imgdata, err := readAndCheckImage(f, int(fi.Size()), secopts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Can't read %s: %s", desc, err)
|
||||
}
|
||||
|
||||
return imgdata, nil
|
||||
}
|
||||
|
||||
func Download(ctx context.Context, imageURL, desc string, opts DownloadOptions, secopts security.Options) (*ImageData, error) {
|
||||
func Download(ctx context.Context, imageURL, desc string, opts DownloadOptions, secopts security.Options) (ImageData, error) {
|
||||
imgdata, err := download(ctx, imageURL, opts, secopts)
|
||||
if err != nil {
|
||||
return nil, ierrors.Wrap(
|
||||
|
@@ -1,22 +0,0 @@
|
||||
package imagedata
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/imagemeta"
|
||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||
)
|
||||
|
||||
// NOTE: This is temporary naming, will be fixed in the subsequent PR
|
||||
// ImageDataI is an interface that defines methods for reading image data and metadata
|
||||
type ImageDataI interface {
|
||||
io.Closer // Close closes the image data and releases any resources held by it
|
||||
Reader() io.ReadSeeker // Reader returns a new ReadSeeker for the image data
|
||||
Meta() imagemeta.Meta // Meta returns the metadata of the image data
|
||||
Format() imagetype.Type // Format returns the image format from the metadata (shortcut)
|
||||
Size() (int, error) // Size returns the size of the image data in bytes
|
||||
|
||||
// This will be removed in the future
|
||||
Headers() http.Header // Headers returns the HTTP headers of the image data, will be removed in the future
|
||||
}
|
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||
"github.com/imgproxy/imgproxy/v3/security"
|
||||
"github.com/imgproxy/imgproxy/v3/testutil"
|
||||
)
|
||||
|
||||
type ImageDataTestSuite struct {
|
||||
@@ -94,7 +95,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusOK() {
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(imgdata)
|
||||
s.Require().Equal(s.defaultData, imgdata.data)
|
||||
s.Require().True(testutil.ReadersEqual(s.T(), bytes.NewReader(s.defaultData), imgdata.Reader()))
|
||||
s.Require().Equal(imagetype.JPEG, imgdata.Format())
|
||||
}
|
||||
|
||||
@@ -165,7 +166,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusPartialContent() {
|
||||
} else {
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(imgdata)
|
||||
s.Require().Equal(s.defaultData, imgdata.data)
|
||||
s.Require().True(testutil.ReadersEqual(s.T(), bytes.NewReader(s.defaultData), imgdata.Reader()))
|
||||
s.Require().Equal(imagetype.JPEG, imgdata.Format())
|
||||
}
|
||||
})
|
||||
@@ -278,27 +279,27 @@ func (s *ImageDataTestSuite) TestDownloadGzip() {
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(imgdata)
|
||||
s.Require().Equal(s.defaultData, imgdata.data)
|
||||
s.Require().True(testutil.ReadersEqual(s.T(), bytes.NewReader(s.defaultData), imgdata.Reader()))
|
||||
s.Require().Equal(imagetype.JPEG, imgdata.Format())
|
||||
}
|
||||
|
||||
func (s *ImageDataTestSuite) TestFromFile() {
|
||||
imgdata, err := FromFile("../testdata/test1.jpg", "Test image", security.DefaultOptions())
|
||||
imgdata, err := NewFromPath("../testdata/test1.jpg", security.DefaultOptions())
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(imgdata)
|
||||
s.Require().Equal(s.defaultData, imgdata.data)
|
||||
s.Require().True(testutil.ReadersEqual(s.T(), bytes.NewReader(s.defaultData), imgdata.Reader()))
|
||||
s.Require().Equal(imagetype.JPEG, imgdata.Format())
|
||||
}
|
||||
|
||||
func (s *ImageDataTestSuite) TestFromBase64() {
|
||||
b64 := base64.StdEncoding.EncodeToString(s.defaultData)
|
||||
|
||||
imgdata, err := FromBase64(b64, "Test image", security.DefaultOptions())
|
||||
imgdata, err := NewFromBase64(b64, security.DefaultOptions())
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(imgdata)
|
||||
s.Require().Equal(s.defaultData, imgdata.data)
|
||||
s.Require().True(testutil.ReadersEqual(s.T(), bytes.NewReader(s.defaultData), imgdata.Reader()))
|
||||
s.Require().Equal(imagetype.JPEG, imgdata.Format())
|
||||
}
|
||||
|
||||
|
@@ -19,7 +19,7 @@ func initRead() {
|
||||
downloadBufPool = bufpool.New("download", config.Workers, config.DownloadBufferSize)
|
||||
}
|
||||
|
||||
func readAndCheckImage(r io.Reader, contentLength int, secopts security.Options) (*ImageData, error) {
|
||||
func readAndCheckImage(r io.Reader, contentLength int, secopts security.Options) (ImageData, error) {
|
||||
buf := downloadBufPool.Get(contentLength, false)
|
||||
cancel := func() { downloadBufPool.Put(buf) }
|
||||
|
||||
@@ -49,11 +49,9 @@ func readAndCheckImage(r io.Reader, contentLength int, secopts security.Options)
|
||||
return nil, imagefetcher.WrapError(err)
|
||||
}
|
||||
|
||||
return &ImageData{
|
||||
data: buf.Bytes(),
|
||||
format: meta.Format(),
|
||||
cancel: cancel,
|
||||
}, nil
|
||||
i := NewFromBytesWithFormat(meta.Format(), buf.Bytes(), nil)
|
||||
i.AddCancel(cancel)
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func BorrowBuffer() (*bytes.Buffer, context.CancelFunc) {
|
||||
|
@@ -8,9 +8,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
||||
"github.com/imgproxy/imgproxy/v3/transport"
|
||||
"github.com/imgproxy/imgproxy/v3/transport/common"
|
||||
"go.withmatt.com/httpheaders"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@@ -12,7 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.withmatt.com/httpheaders"
|
||||
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
)
|
||||
|
||||
func applyFilters(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func applyFilters(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
if po.Blur == 0 && po.Sharpen == 0 && po.Pixelate <= 1 {
|
||||
return nil
|
||||
}
|
||||
|
@@ -32,7 +32,7 @@ func cropImage(img *vips.Image, cropWidth, cropHeight int, gravity *options.Grav
|
||||
return img.Crop(left, top, cropWidth, cropHeight)
|
||||
}
|
||||
|
||||
func crop(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func crop(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
width, height := pctx.cropWidth, pctx.cropHeight
|
||||
|
||||
opts := pctx.cropGravity
|
||||
@@ -47,6 +47,6 @@ func crop(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions,
|
||||
return cropImage(img, width, height, &opts, 1.0)
|
||||
}
|
||||
|
||||
func cropToResult(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func cropToResult(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
return cropImage(img, pctx.resultCropWidth, pctx.resultCropHeight, &po.Gravity, pctx.dprScale)
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
)
|
||||
|
||||
func exportColorProfile(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func exportColorProfile(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
keepProfile := !po.StripColorProfile && po.Format.SupportsColourProfile()
|
||||
|
||||
if img.IsLinear() {
|
||||
|
@@ -25,7 +25,7 @@ func extendImage(img *vips.Image, width, height int, gravity *options.GravityOpt
|
||||
return img.Embed(width, height, offX, offY)
|
||||
}
|
||||
|
||||
func extend(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func extend(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
if !po.Extend.Enabled {
|
||||
return nil
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func extend(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOption
|
||||
return extendImage(img, width, height, &po.Extend.Gravity, pctx.dprScale)
|
||||
}
|
||||
|
||||
func extendAspectRatio(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func extendAspectRatio(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
if !po.ExtendAspectRatio.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
@@ -91,7 +91,7 @@ func fixIcoSize(img *vips.Image) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func fixSize(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func fixSize(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
switch po.Format {
|
||||
case imagetype.WEBP:
|
||||
return fixWebpSize(img)
|
||||
|
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
)
|
||||
|
||||
func flatten(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func flatten(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
if !po.Flatten && po.Format.SupportsAlpha() {
|
||||
return nil
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
)
|
||||
|
||||
func importColorProfile(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func importColorProfile(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
if img.ColourProfileImported() {
|
||||
return nil
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
)
|
||||
|
||||
func padding(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func padding(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
if !po.Padding.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
@@ -60,10 +60,10 @@ type pipelineContext struct {
|
||||
extendAspectRatioHeight int
|
||||
}
|
||||
|
||||
type pipelineStep func(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error
|
||||
type pipelineStep func(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error
|
||||
type pipeline []pipelineStep
|
||||
|
||||
func (p pipeline) Run(ctx context.Context, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func (p pipeline) Run(ctx context.Context, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
pctx := pipelineContext{
|
||||
ctx: ctx,
|
||||
|
||||
|
@@ -248,7 +248,7 @@ func (pctx *pipelineContext) limitScale(widthToScale, heightToScale int, po *opt
|
||||
}
|
||||
}
|
||||
|
||||
func prepare(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func prepare(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
pctx.imgtype = imagetype.Unknown
|
||||
if imgdata != nil {
|
||||
pctx.imgtype = imgdata.Format()
|
||||
|
@@ -97,7 +97,7 @@ func getImageSize(img *vips.Image) (int, int) {
|
||||
return width, height
|
||||
}
|
||||
|
||||
func transformAnimated(ctx context.Context, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func transformAnimated(ctx context.Context, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
if po.Trim.Enabled {
|
||||
log.Warning("Trim is not supported for animated images")
|
||||
po.Trim.Enabled = false
|
||||
@@ -207,7 +207,7 @@ func transformAnimated(ctx context.Context, img *vips.Image, po *options.Process
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveImageToFitBytes(ctx context.Context, po *options.ProcessingOptions, img *vips.Image) (*imagedata.ImageData, error) {
|
||||
func saveImageToFitBytes(ctx context.Context, po *options.ProcessingOptions, img *vips.Image) (imagedata.ImageData, error) {
|
||||
var diff float64
|
||||
quality := po.GetQuality()
|
||||
|
||||
@@ -248,7 +248,7 @@ func saveImageToFitBytes(ctx context.Context, po *options.ProcessingOptions, img
|
||||
}
|
||||
}
|
||||
|
||||
func ProcessImage(ctx context.Context, imgdata *imagedata.ImageData, po *options.ProcessingOptions) (*imagedata.ImageData, error) {
|
||||
func ProcessImage(ctx context.Context, imgdata imagedata.ImageData, po *options.ProcessingOptions) (imagedata.ImageData, error) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
@@ -348,7 +348,7 @@ func ProcessImage(ctx context.Context, imgdata *imagedata.ImageData, po *options
|
||||
}
|
||||
|
||||
var (
|
||||
outData *imagedata.ImageData
|
||||
outData imagedata.ImageData
|
||||
err error
|
||||
)
|
||||
|
||||
@@ -359,13 +359,10 @@ func ProcessImage(ctx context.Context, imgdata *imagedata.ImageData, po *options
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if outData.Headers == nil {
|
||||
outData.Headers = make(map[string]string)
|
||||
}
|
||||
outData.Headers["X-Origin-Width"] = strconv.Itoa(originWidth)
|
||||
outData.Headers["X-Origin-Height"] = strconv.Itoa(originHeight)
|
||||
outData.Headers["X-Result-Width"] = strconv.Itoa(img.Width())
|
||||
outData.Headers["X-Result-Height"] = strconv.Itoa(img.Height())
|
||||
outData.Headers().Set("X-Origin-Width", strconv.Itoa(originWidth))
|
||||
outData.Headers().Set("X-Origin-Height", strconv.Itoa(originHeight))
|
||||
outData.Headers().Set("X-Result-Width", strconv.Itoa(img.Width()))
|
||||
outData.Headers().Set("X-Result-Height", strconv.Itoa(img.Height()))
|
||||
}
|
||||
|
||||
return outData, err
|
||||
|
@@ -31,7 +31,7 @@ func (s *ProcessingTestSuite) SetupSuite() {
|
||||
logrus.SetOutput(io.Discard)
|
||||
}
|
||||
|
||||
func (s *ProcessingTestSuite) openFile(name string) *imagedata.ImageData {
|
||||
func (s *ProcessingTestSuite) openFile(name string) imagedata.ImageData {
|
||||
secopts := security.Options{
|
||||
MaxSrcResolution: 10 * 1024 * 1024,
|
||||
MaxSrcFileSize: 10 * 1024 * 1024,
|
||||
@@ -43,13 +43,13 @@ func (s *ProcessingTestSuite) openFile(name string) *imagedata.ImageData {
|
||||
s.Require().NoError(err)
|
||||
path := filepath.Join(wd, "..", "testdata", name)
|
||||
|
||||
imagedata, err := imagedata.FromFile(path, "test image", secopts)
|
||||
imagedata, err := imagedata.NewFromPath(path, secopts)
|
||||
s.Require().NoError(err)
|
||||
|
||||
return imagedata
|
||||
}
|
||||
|
||||
func (s *ProcessingTestSuite) checkSize(imgdata *imagedata.ImageData, width, height int) {
|
||||
func (s *ProcessingTestSuite) checkSize(imgdata imagedata.ImageData, width, height int) {
|
||||
img := new(vips.Image)
|
||||
err := img.Load(imgdata, 1, 1, 1)
|
||||
s.Require().NoError(err)
|
||||
|
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
)
|
||||
|
||||
func rotateAndFlip(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func rotateAndFlip(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
if pctx.angle%360 == 0 && po.Rotate%360 == 0 && !pctx.flip {
|
||||
return nil
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
)
|
||||
|
||||
func scale(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func scale(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
if pctx.wscale == 1 && pctx.hscale == 1 {
|
||||
return nil
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
)
|
||||
|
||||
func canScaleOnLoad(pctx *pipelineContext, imgdata *imagedata.ImageData, scale float64) bool {
|
||||
func canScaleOnLoad(pctx *pipelineContext, imgdata imagedata.ImageData, scale float64) bool {
|
||||
if imgdata == nil || pctx.trimmed || scale == 1 {
|
||||
return false
|
||||
}
|
||||
@@ -45,7 +45,7 @@ func calcJpegShink(shrink float64) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func scaleOnLoad(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func scaleOnLoad(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
wshrink := float64(pctx.srcWidth) / float64(imath.Scale(pctx.srcWidth, pctx.wscale))
|
||||
hshrink := float64(pctx.srcHeight) / float64(imath.Scale(pctx.srcHeight, pctx.hscale))
|
||||
preshrink := math.Min(wshrink, hshrink)
|
||||
|
@@ -105,7 +105,7 @@ func stripXMP(img *vips.Image) []byte {
|
||||
return xmpData
|
||||
}
|
||||
|
||||
func stripMetadata(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func stripMetadata(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
if !po.StripMetadata {
|
||||
return nil
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
)
|
||||
|
||||
func trim(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func trim(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
if !po.Trim.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ var watermarkPipeline = pipeline{
|
||||
padding,
|
||||
}
|
||||
|
||||
func prepareWatermark(wm *vips.Image, wmData *imagedata.ImageData, opts *options.WatermarkOptions, imgWidth, imgHeight int, offsetScale float64, framesCount int) error {
|
||||
func prepareWatermark(wm *vips.Image, wmData imagedata.ImageData, opts *options.WatermarkOptions, imgWidth, imgHeight int, offsetScale float64, framesCount int) error {
|
||||
if err := wm.Load(wmData, 1, 1.0, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func prepareWatermark(wm *vips.Image, wmData *imagedata.ImageData, opts *options
|
||||
return wm.StripAll()
|
||||
}
|
||||
|
||||
func applyWatermark(img *vips.Image, wmData *imagedata.ImageData, opts *options.WatermarkOptions, offsetScale float64, framesCount int) error {
|
||||
func applyWatermark(img *vips.Image, wmData imagedata.ImageData, opts *options.WatermarkOptions, offsetScale float64, framesCount int) error {
|
||||
wm := new(vips.Image)
|
||||
defer wm.Clear()
|
||||
|
||||
@@ -162,7 +162,7 @@ func applyWatermark(img *vips.Image, wmData *imagedata.ImageData, opts *options.
|
||||
return nil
|
||||
}
|
||||
|
||||
func watermark(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
|
||||
func watermark(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata imagedata.ImageData) error {
|
||||
if !po.Watermark.Enabled || imagedata.Watermark == nil {
|
||||
return nil
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/cookies"
|
||||
"github.com/imgproxy/imgproxy/v3/errorreport"
|
||||
"github.com/imgproxy/imgproxy/v3/etag"
|
||||
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||
"github.com/imgproxy/imgproxy/v3/imagefetcher"
|
||||
@@ -66,7 +67,7 @@ func initProcessingHandler() {
|
||||
headerVaryValue = strings.Join(vary, ", ")
|
||||
}
|
||||
|
||||
func setCacheControl(rw http.ResponseWriter, force *time.Time, originHeaders map[string]string) {
|
||||
func setCacheControl(rw http.ResponseWriter, force *time.Time, originHeaders http.Header) {
|
||||
ttl := -1
|
||||
|
||||
if _, ok := originHeaders["Fallback-Image"]; ok && config.FallbackImageTTL > 0 {
|
||||
@@ -78,12 +79,12 @@ func setCacheControl(rw http.ResponseWriter, force *time.Time, originHeaders map
|
||||
}
|
||||
|
||||
if config.CacheControlPassthrough && ttl < 0 && originHeaders != nil {
|
||||
if val, ok := originHeaders["Cache-Control"]; ok && len(val) > 0 {
|
||||
rw.Header().Set("Cache-Control", val)
|
||||
if val := originHeaders.Get(httpheaders.CacheControl); len(val) > 0 {
|
||||
rw.Header().Set(httpheaders.CacheControl, val)
|
||||
return
|
||||
}
|
||||
|
||||
if val, ok := originHeaders["Expires"]; ok && len(val) > 0 {
|
||||
if val := originHeaders.Get(httpheaders.Expires); len(val) > 0 {
|
||||
if t, err := time.Parse(http.TimeFormat, val); err == nil {
|
||||
ttl = imath.Max(0, int(time.Until(t).Seconds()))
|
||||
}
|
||||
@@ -95,23 +96,23 @@ func setCacheControl(rw http.ResponseWriter, force *time.Time, originHeaders map
|
||||
}
|
||||
|
||||
if ttl > 0 {
|
||||
rw.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d, public", ttl))
|
||||
rw.Header().Set(httpheaders.CacheControl, fmt.Sprintf("max-age=%d, public", ttl))
|
||||
} else {
|
||||
rw.Header().Set("Cache-Control", "no-cache")
|
||||
rw.Header().Set(httpheaders.CacheControl, "no-cache")
|
||||
}
|
||||
}
|
||||
|
||||
func setLastModified(rw http.ResponseWriter, originHeaders map[string]string) {
|
||||
func setLastModified(rw http.ResponseWriter, originHeaders http.Header) {
|
||||
if config.LastModifiedEnabled {
|
||||
if val, ok := originHeaders["Last-Modified"]; ok && len(val) != 0 {
|
||||
rw.Header().Set("Last-Modified", val)
|
||||
if val := originHeaders.Get(httpheaders.LastModified); len(val) != 0 {
|
||||
rw.Header().Set(httpheaders.LastModified, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setVary(rw http.ResponseWriter) {
|
||||
if len(headerVaryValue) > 0 {
|
||||
rw.Header().Set("Vary", headerVaryValue)
|
||||
rw.Header().Set(httpheaders.Vary, headerVaryValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +125,7 @@ func setCanonical(rw http.ResponseWriter, originURL string) {
|
||||
}
|
||||
}
|
||||
|
||||
func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, statusCode int, resultData *imagedata.ImageData, po *options.ProcessingOptions, originURL string, originData *imagedata.ImageData) {
|
||||
func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, statusCode int, resultData imagedata.ImageData, po *options.ProcessingOptions, originURL string, originData imagedata.ImageData) {
|
||||
var contentDisposition string
|
||||
if len(po.Filename) > 0 {
|
||||
contentDisposition = resultData.Format().ContentDisposition(po.Filename, po.ReturnAttachment)
|
||||
@@ -135,8 +136,8 @@ func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, sta
|
||||
rw.Header().Set("Content-Type", resultData.Format().Mime())
|
||||
rw.Header().Set("Content-Disposition", contentDisposition)
|
||||
|
||||
setCacheControl(rw, po.Expires, originData.Headers)
|
||||
setLastModified(rw, originData.Headers)
|
||||
setCacheControl(rw, po.Expires, originData.Headers())
|
||||
setLastModified(rw, originData.Headers())
|
||||
setVary(rw)
|
||||
setCanonical(rw, originURL)
|
||||
|
||||
@@ -147,10 +148,10 @@ func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, sta
|
||||
}
|
||||
|
||||
rw.Header().Set("X-Origin-Content-Length", strconv.Itoa(originSize))
|
||||
rw.Header().Set("X-Origin-Width", resultData.Headers["X-Origin-Width"])
|
||||
rw.Header().Set("X-Origin-Height", resultData.Headers["X-Origin-Height"])
|
||||
rw.Header().Set("X-Result-Width", resultData.Headers["X-Result-Width"])
|
||||
rw.Header().Set("X-Result-Height", resultData.Headers["X-Result-Height"])
|
||||
rw.Header().Set("X-Origin-Width", resultData.Headers().Get("X-Origin-Width"))
|
||||
rw.Header().Set("X-Origin-Height", resultData.Headers().Get("X-Origin-Height"))
|
||||
rw.Header().Set("X-Result-Width", resultData.Headers().Get("X-Result-Width"))
|
||||
rw.Header().Set("X-Result-Height", resultData.Headers().Get("X-Result-Height"))
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Security-Policy", "script-src 'none'")
|
||||
@@ -184,7 +185,7 @@ func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, sta
|
||||
)
|
||||
}
|
||||
|
||||
func respondWithNotModified(reqID string, r *http.Request, rw http.ResponseWriter, po *options.ProcessingOptions, originURL string, originHeaders map[string]string) {
|
||||
func respondWithNotModified(reqID string, r *http.Request, rw http.ResponseWriter, po *options.ProcessingOptions, originURL string, originHeaders http.Header) {
|
||||
setCacheControl(rw, po.Expires, originHeaders)
|
||||
setVary(rw)
|
||||
|
||||
@@ -347,7 +348,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
statusCode := http.StatusOK
|
||||
|
||||
originData, err := func() (*imagedata.ImageData, error) {
|
||||
originData, err := func() (imagedata.ImageData, error) {
|
||||
defer metrics.StartDownloadingSegment(ctx, metrics.Meta{
|
||||
metrics.MetaSourceImageURL: metricsMeta[metrics.MetaSourceImageURL],
|
||||
metrics.MetaSourceImageOrigin: metricsMeta[metrics.MetaSourceImageOrigin],
|
||||
@@ -374,15 +375,10 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
case errors.As(err, &nmErr):
|
||||
if config.ETagEnabled && len(etagHandler.ImageEtagExpected()) != 0 {
|
||||
rw.Header().Set("ETag", etagHandler.GenerateExpectedETag())
|
||||
rw.Header().Set(httpheaders.Etag, etagHandler.GenerateExpectedETag())
|
||||
}
|
||||
|
||||
h := make(map[string]string)
|
||||
for k := range nmErr.Headers() {
|
||||
h[k] = nmErr.Headers().Get(k)
|
||||
}
|
||||
|
||||
respondWithNotModified(reqID, r, rw, po, imageURL, h)
|
||||
respondWithNotModified(reqID, r, rw, po, imageURL, nmErr.Headers())
|
||||
return
|
||||
|
||||
default:
|
||||
@@ -426,7 +422,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("ETag", etagHandler.GenerateActualETag())
|
||||
|
||||
if imgDataMatch && etagHandler.ProcessingOptionsMatch() {
|
||||
respondWithNotModified(reqID, r, rw, po, imageURL, originData.Headers)
|
||||
respondWithNotModified(reqID, r, rw, po, imageURL, originData.Headers())
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -471,7 +467,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||
))
|
||||
}
|
||||
|
||||
resultData, err := func() (*imagedata.ImageData, error) {
|
||||
resultData, err := func() (imagedata.ImageData, error) {
|
||||
defer metrics.StartProcessingSegment(ctx, metrics.Meta{
|
||||
metrics.MetaProcessingOptions: metricsMeta[metrics.MetaProcessingOptions],
|
||||
})()
|
||||
|
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/config/configurators"
|
||||
"github.com/imgproxy/imgproxy/v3/etag"
|
||||
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||
"github.com/imgproxy/imgproxy/v3/imagemeta"
|
||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||
@@ -86,26 +87,26 @@ func (s *ProcessingHandlerTestSuite) readTestFile(name string) []byte {
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) readTestImageData(name string) *imagedata.ImageData {
|
||||
func (s *ProcessingHandlerTestSuite) readTestImageData(name string) imagedata.ImageData {
|
||||
wd, err := os.Getwd()
|
||||
s.Require().NoError(err)
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(wd, "testdata", name))
|
||||
s.Require().NoError(err)
|
||||
|
||||
imgdata, err := imagedata.NewFromBytes(data, make(http.Header))
|
||||
imgdata, err := imagedata.NewFromBytes(data)
|
||||
s.Require().NoError(err)
|
||||
|
||||
return imgdata
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) readImageData(imgdata *imagedata.ImageData) []byte {
|
||||
func (s *ProcessingHandlerTestSuite) readImageData(imgdata imagedata.ImageData) []byte {
|
||||
data, err := io.ReadAll(imgdata.Reader())
|
||||
s.Require().NoError(err)
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, *imagedata.ImageData, string) {
|
||||
func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, imagedata.ImageData, string) {
|
||||
poStr := "rs:fill:4:4"
|
||||
|
||||
po := options.NewProcessingOptions()
|
||||
@@ -116,7 +117,7 @@ func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, *im
|
||||
imgdata := s.readTestImageData("test1.png")
|
||||
|
||||
if len(imgETag) != 0 {
|
||||
imgdata.Headers = map[string]string{"ETag": imgETag}
|
||||
imgdata.Headers().Set(httpheaders.Etag, imgETag)
|
||||
}
|
||||
|
||||
var h etag.Handler
|
||||
@@ -416,7 +417,7 @@ func (s *ProcessingHandlerTestSuite) TestETagReqNoIfNotModified() {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
s.Empty(r.Header.Get("If-None-Match"))
|
||||
|
||||
rw.Header().Set("ETag", imgdata.Headers["ETag"])
|
||||
rw.Header().Set("ETag", imgdata.Headers().Get(httpheaders.Etag))
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(s.readTestFile("test1.png"))
|
||||
}))
|
||||
@@ -455,7 +456,7 @@ func (s *ProcessingHandlerTestSuite) TestETagReqMatch() {
|
||||
poStr, imgdata, etag := s.sampleETagData(`"loremipsumdolor"`)
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
s.Equal(imgdata.Headers["ETag"], r.Header.Get("If-None-Match"))
|
||||
s.Equal(imgdata.Headers().Get(httpheaders.Etag), r.Header.Get(httpheaders.IfNoneMatch))
|
||||
|
||||
rw.WriteHeader(304)
|
||||
}))
|
||||
@@ -503,7 +504,7 @@ func (s *ProcessingHandlerTestSuite) TestETagReqNotMatch() {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
s.Equal(`"loremipsum"`, r.Header.Get("If-None-Match"))
|
||||
|
||||
rw.Header().Set("ETag", imgdata.Headers["ETag"])
|
||||
rw.Header().Set("ETag", imgdata.Headers().Get(httpheaders.Etag))
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(s.readImageData(imgdata))
|
||||
}))
|
||||
@@ -554,7 +555,7 @@ func (s *ProcessingHandlerTestSuite) TestETagProcessingOptionsNotMatch() {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
s.Empty(r.Header.Get("If-None-Match"))
|
||||
|
||||
rw.Header().Set("ETag", imgdata.Headers["ETag"])
|
||||
rw.Header().Set("ETag", imgdata.Headers().Get(httpheaders.Etag))
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(s.readImageData(imgdata))
|
||||
}))
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/imgproxy/imgproxy/v3/imagemeta"
|
||||
"github.com/imgproxy/imgproxy/v3/imath"
|
||||
)
|
||||
|
||||
@@ -19,3 +20,7 @@ func CheckDimensions(width, height, frames int, opts Options) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckMeta(meta imagemeta.Meta, opts Options) error {
|
||||
return CheckDimensions(meta.Width(), meta.Height(), 1, opts)
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package security
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
// hardLimitReadCloser is a wrapper around io.ReadCloser
|
||||
@@ -49,3 +50,23 @@ func LimitResponseSize(r *http.Response, opts Options) (*http.Response, error) {
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// LimitFileSize limits the size of the file to MaxSrcFileSize (if set).
|
||||
// It calls f.Stat() to get the file to get its size and returns an error
|
||||
// if the size exceeds MaxSrcFileSize.
|
||||
func LimitFileSize(f *os.File, opts Options) (*os.File, error) {
|
||||
if opts.MaxSrcFileSize == 0 {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
s, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if int(s.Size()) > opts.MaxSrcFileSize {
|
||||
return nil, newFileSizeError()
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
@@ -113,10 +113,7 @@ func streamOriginImage(ctx context.Context, reqID string, r *http.Request, rw ht
|
||||
rw.Header().Set("Content-Disposition", imagetype.ContentDisposition(filename, ext, po.ReturnAttachment))
|
||||
}
|
||||
|
||||
setCacheControl(rw, po.Expires, map[string]string{
|
||||
"Cache-Control": res.Header.Get("Cache-Control"),
|
||||
"Expires": res.Header.Get("Expires"),
|
||||
})
|
||||
setCacheControl(rw, po.Expires, res.Header)
|
||||
setCanonical(rw, imageURL)
|
||||
rw.Header().Set("Content-Security-Policy", "script-src 'none'")
|
||||
|
||||
|
21
svg/svg.go
21
svg/svg.go
@@ -2,7 +2,6 @@ package svg
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/parse/v2"
|
||||
@@ -12,16 +11,7 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||
)
|
||||
|
||||
func cloneHeaders(src map[string]string) http.Header {
|
||||
h := make(http.Header, len(src))
|
||||
for k, v := range src {
|
||||
h.Set(k, v)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func Sanitize(data *imagedata.ImageData) (*imagedata.ImageData, error) {
|
||||
func Sanitize(data imagedata.ImageData) (imagedata.ImageData, error) {
|
||||
r := data.Reader()
|
||||
l := xml.NewLexer(parse.NewInput(r))
|
||||
|
||||
@@ -55,15 +45,12 @@ func Sanitize(data *imagedata.ImageData) (*imagedata.ImageData, error) {
|
||||
return nil, l.Err()
|
||||
}
|
||||
|
||||
newData, err := imagedata.NewFromBytesWithFormat(
|
||||
newData := imagedata.NewFromBytesWithFormat(
|
||||
imagetype.SVG,
|
||||
buf.Bytes(),
|
||||
cloneHeaders(data.Headers),
|
||||
data.Headers(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newData.SetCancel(cancel)
|
||||
newData.AddCancel(cancel)
|
||||
|
||||
return newData, nil
|
||||
case xml.StartTagToken:
|
||||
|
@@ -1,15 +1,14 @@
|
||||
package svg
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"go.withmatt.com/httpheaders"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||
"github.com/imgproxy/imgproxy/v3/testutil"
|
||||
)
|
||||
@@ -25,18 +24,16 @@ func (s *SvgTestSuite) SetupSuite() {
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (s *SvgTestSuite) readTestFile(name string) *imagedata.ImageData {
|
||||
func (s *SvgTestSuite) readTestFile(name string) imagedata.ImageData {
|
||||
wd, err := os.Getwd()
|
||||
s.Require().NoError(err)
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(wd, "..", "testdata", name))
|
||||
s.Require().NoError(err)
|
||||
|
||||
h := make(http.Header)
|
||||
h.Set(httpheaders.ContentType, "image/svg+xml")
|
||||
h.Set(httpheaders.CacheControl, "public, max-age=12345")
|
||||
|
||||
d, err := imagedata.NewFromBytes(data, h)
|
||||
d, err := imagedata.NewFromBytes(data)
|
||||
d.Headers().Set(httpheaders.ContentType, "image/svg+xml")
|
||||
d.Headers().Set(httpheaders.CacheControl, "public, max-age=12345")
|
||||
s.Require().NoError(err)
|
||||
|
||||
return d
|
||||
@@ -49,7 +46,7 @@ func (s *SvgTestSuite) TestSanitize() {
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), actual.Reader()))
|
||||
s.Require().Equal(origin.Headers, actual.Headers)
|
||||
s.Require().Equal(origin.Headers(), actual.Headers())
|
||||
}
|
||||
|
||||
func TestSvg(t *testing.T) {
|
||||
|
17
vips/vips.go
17
vips/vips.go
@@ -353,7 +353,7 @@ func (img *Image) Pages() int {
|
||||
return p
|
||||
}
|
||||
|
||||
func (img *Image) Load(imgdata *imagedata.ImageData, shrink int, scale float64, pages int) error {
|
||||
func (img *Image) Load(imgdata imagedata.ImageData, shrink int, scale float64, pages int) error {
|
||||
var tmp *C.VipsImage
|
||||
|
||||
err := C.int(0)
|
||||
@@ -403,7 +403,7 @@ func (img *Image) Load(imgdata *imagedata.ImageData, shrink int, scale float64,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (img *Image) LoadThumbnail(imgdata *imagedata.ImageData) error {
|
||||
func (img *Image) LoadThumbnail(imgdata imagedata.ImageData) error {
|
||||
if imgdata.Format() != imagetype.HEIC && imgdata.Format() != imagetype.AVIF {
|
||||
return newVipsError("Usupported image type to load thumbnail")
|
||||
}
|
||||
@@ -423,7 +423,7 @@ func (img *Image) LoadThumbnail(imgdata *imagedata.ImageData) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (img *Image) Save(imgtype imagetype.Type, quality int) (*imagedata.ImageData, error) {
|
||||
func (img *Image) Save(imgtype imagetype.Type, quality int) (imagedata.ImageData, error) {
|
||||
target := C.vips_target_new_to_memory()
|
||||
|
||||
cancel := func() {
|
||||
@@ -470,15 +470,10 @@ func (img *Image) Save(imgtype imagetype.Type, quality int) (*imagedata.ImageDat
|
||||
|
||||
b := ptrToBytes(ptr, int(imgsize))
|
||||
|
||||
imgdata, ierr := imagedata.NewFromBytesWithFormat(imgtype, b, make(http.Header))
|
||||
if ierr != nil {
|
||||
cancel()
|
||||
return nil, ierr
|
||||
}
|
||||
i := imagedata.NewFromBytesWithFormat(imgtype, b, nil)
|
||||
i.AddCancel(cancel)
|
||||
|
||||
imgdata.SetCancel(cancel)
|
||||
|
||||
return imgdata, nil
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (img *Image) Clear() {
|
||||
|
Reference in New Issue
Block a user