mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-09-27 20:17:20 +02:00
IMG-57: introduce processing options factory (#1526)
* Intoduced options.Factory * ProcessingOptionsFactory in processing and watermark * Clone with testutil.Helper
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/fetcher"
|
||||
processinghandler "github.com/imgproxy/imgproxy/v3/handlers/processing"
|
||||
streamhandler "github.com/imgproxy/imgproxy/v3/handlers/stream"
|
||||
"github.com/imgproxy/imgproxy/v3/options"
|
||||
"github.com/imgproxy/imgproxy/v3/security"
|
||||
"github.com/imgproxy/imgproxy/v3/server"
|
||||
"github.com/imgproxy/imgproxy/v3/workers"
|
||||
@@ -26,6 +27,7 @@ type Config struct {
|
||||
Handlers HandlerConfigs
|
||||
Server server.Config
|
||||
Security security.Config
|
||||
Options options.Config
|
||||
}
|
||||
|
||||
// NewDefaultConfig creates a new default configuration
|
||||
@@ -41,6 +43,7 @@ func NewDefaultConfig() Config {
|
||||
},
|
||||
Server: server.NewDefaultConfig(),
|
||||
Security: security.NewDefaultConfig(),
|
||||
Options: options.NewDefaultConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +81,11 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := security.LoadConfigFromEnv(&c.Security); err != nil {
|
||||
if _, err = security.LoadConfigFromEnv(&c.Security); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err = options.LoadConfigFromEnv(&c.Options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
7
go.mod
7
go.mod
@@ -11,6 +11,7 @@ require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1
|
||||
github.com/DarthSim/godotenv v1.3.1
|
||||
github.com/DataDog/datadog-agent/pkg/trace v0.67.0
|
||||
github.com/DataDog/datadog-go/v5 v5.6.0
|
||||
github.com/DataDog/dd-trace-go/v2 v2.0.1
|
||||
github.com/airbrake/gobrake/v5 v5.6.2
|
||||
@@ -42,6 +43,7 @@ require (
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tdewolff/parse/v2 v2.8.1
|
||||
github.com/trimmer-io/go-xmp v1.0.0
|
||||
github.com/urfave/cli/v3 v3.4.1
|
||||
go.opentelemetry.io/contrib/detectors/aws/ec2 v1.37.0
|
||||
go.opentelemetry.io/contrib/detectors/aws/ecs v1.37.0
|
||||
go.opentelemetry.io/contrib/detectors/aws/eks v1.37.0
|
||||
@@ -58,7 +60,6 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0
|
||||
go.opentelemetry.io/otel/trace v1.37.0
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
golang.org/x/image v0.28.0
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/sync v0.15.0
|
||||
golang.org/x/sys v0.33.0
|
||||
@@ -82,7 +83,6 @@ require (
|
||||
github.com/DataDog/datadog-agent/pkg/obfuscate v0.67.0 // indirect
|
||||
github.com/DataDog/datadog-agent/pkg/proto v0.67.0 // indirect
|
||||
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.67.0 // indirect
|
||||
github.com/DataDog/datadog-agent/pkg/trace v0.67.0 // indirect
|
||||
github.com/DataDog/datadog-agent/pkg/util/log v0.67.0 // indirect
|
||||
github.com/DataDog/datadog-agent/pkg/util/scrubber v0.67.0 // indirect
|
||||
github.com/DataDog/datadog-agent/pkg/version v0.67.0 // indirect
|
||||
@@ -119,7 +119,6 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 // indirect
|
||||
@@ -175,7 +174,6 @@ require (
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect
|
||||
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 // indirect
|
||||
@@ -184,7 +182,6 @@ require (
|
||||
github.com/tinylib/msgp v1.3.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/urfave/cli/v3 v3.4.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zeebo/errs v1.4.0 // indirect
|
||||
|
9
go.sum
9
go.sum
@@ -44,7 +44,6 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/DarthSim/godotenv v1.3.1 h1:NMWdswlRx2M9uPY4Ux8p/Q/rDs7A97OG89fECiQ/Tz0=
|
||||
github.com/DarthSim/godotenv v1.3.1/go.mod h1:B3ySe1HYTUFFR6+TPyHyxPWjUdh48il0Blebg9p1cCc=
|
||||
github.com/DataDog/appsec-internal-go v1.13.0 h1:aO6DmHYsAU8BNFuvYJByhMKGgcQT3WAbj9J/sgAJxtA=
|
||||
@@ -173,8 +172,6 @@ github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI=
|
||||
github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -397,8 +394,6 @@ github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3 h1:4+LEVO
|
||||
github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3/go.mod h1:vl5+MqJ1nBINuSsUI2mGgH79UweUT/B5Fy8857PqyyI=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc=
|
||||
@@ -446,8 +441,6 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/trimmer-io/go-xmp v1.0.0 h1:zY8bolSga5kOjBAaHS6hrdxLgEoYuT875xTy0QDwZWs=
|
||||
github.com/trimmer-io/go-xmp v1.0.0/go.mod h1:Aaptr9sp1lLv7UnCAdQ+gSHZyY2miYaKmcNVj7HRBwA=
|
||||
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
|
||||
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
|
||||
github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
|
||||
github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
|
||||
github.com/vmihailenco/msgpack/v4 v4.3.13 h1:A2wsiTbvp63ilDaWmsk2wjx6xZdxQOvpiNlKBGKKXKI=
|
||||
@@ -582,8 +575,6 @@ golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
||||
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
|
@@ -26,6 +26,7 @@ type HandlerContext interface {
|
||||
WatermarkImage() auximageprovider.Provider
|
||||
ImageDataFactory() *imagedata.Factory
|
||||
Security() *security.Checker
|
||||
OptionsFactory() *options.Factory
|
||||
}
|
||||
|
||||
// Handler handles image processing requests
|
||||
@@ -108,7 +109,7 @@ func (h *Handler) newRequest(
|
||||
}
|
||||
|
||||
// parse image url and processing options
|
||||
po, imageURL, err := options.ParsePath(path, req.Header)
|
||||
po, imageURL, err := h.OptionsFactory().ParsePath(path, req.Header)
|
||||
if err != nil {
|
||||
return "", nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryPathParsing))
|
||||
}
|
||||
|
12
imgproxy.go
12
imgproxy.go
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||
"github.com/imgproxy/imgproxy/v3/memory"
|
||||
"github.com/imgproxy/imgproxy/v3/monitoring/prometheus"
|
||||
"github.com/imgproxy/imgproxy/v3/options"
|
||||
"github.com/imgproxy/imgproxy/v3/security"
|
||||
"github.com/imgproxy/imgproxy/v3/server"
|
||||
"github.com/imgproxy/imgproxy/v3/workers"
|
||||
@@ -41,6 +42,7 @@ type Imgproxy struct {
|
||||
imageDataFactory *imagedata.Factory
|
||||
handlers ImgproxyHandlers
|
||||
security *security.Checker
|
||||
optionsFactory *options.Factory
|
||||
config *Config
|
||||
}
|
||||
|
||||
@@ -73,6 +75,11 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
processingOptionsFactory, err := options.NewFactory(&config.Options, security)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imgproxy := &Imgproxy{
|
||||
workers: workers,
|
||||
fallbackImage: fallbackImage,
|
||||
@@ -81,6 +88,7 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
|
||||
imageDataFactory: idf,
|
||||
config: config,
|
||||
security: security,
|
||||
optionsFactory: processingOptionsFactory,
|
||||
}
|
||||
|
||||
imgproxy.handlers.Health = healthhandler.New()
|
||||
@@ -199,3 +207,7 @@ func (i *Imgproxy) ImageDataFactory() *imagedata.Factory {
|
||||
func (i *Imgproxy) Security() *security.Checker {
|
||||
return i.security
|
||||
}
|
||||
|
||||
func (i *Imgproxy) OptionsFactory() *options.Factory {
|
||||
return i.optionsFactory
|
||||
}
|
||||
|
11
init.go
11
init.go
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/gliblog"
|
||||
"github.com/imgproxy/imgproxy/v3/logger"
|
||||
"github.com/imgproxy/imgproxy/v3/monitoring"
|
||||
"github.com/imgproxy/imgproxy/v3/options"
|
||||
"github.com/imgproxy/imgproxy/v3/processing"
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
"go.uber.org/automaxprocs/maxprocs"
|
||||
@@ -53,16 +52,6 @@ func Init() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := options.ParsePresets(config.Presets); err != nil {
|
||||
vips.Shutdown()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := options.ValidatePresets(); err != nil {
|
||||
vips.Shutdown()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -168,7 +168,7 @@ func (s *ProcessingHandlerTestSuite) TestResultingFormatNotSupported() {
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingConfig() {
|
||||
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
||||
s.Config().Options.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
||||
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||
|
||||
@@ -184,7 +184,7 @@ func (s *ProcessingHandlerTestSuite) TestSkipProcessingPO() {
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
|
||||
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
||||
s.Config().Options.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
||||
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
|
||||
|
||||
@@ -477,7 +477,7 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvg() {
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithEnforceAvif() {
|
||||
config.AlwaysRasterizeSvg = true
|
||||
config.EnforceWebp = true
|
||||
s.Config().Options.EnforceWebp = true
|
||||
|
||||
res := s.GET("/unsafe/plain/local:///test1.svg", http.Header{"Accept": []string{"image/webp"}})
|
||||
|
||||
@@ -487,7 +487,7 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithEnforceAvif() {
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgDisabled() {
|
||||
config.AlwaysRasterizeSvg = false
|
||||
config.EnforceWebp = true
|
||||
s.Config().Options.EnforceWebp = true
|
||||
|
||||
res := s.GET("/unsafe/plain/local:///test1.svg")
|
||||
|
||||
@@ -497,7 +497,7 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgDisabled() {
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithFormat() {
|
||||
config.AlwaysRasterizeSvg = true
|
||||
config.SkipProcessingFormats = []imagetype.Type{imagetype.SVG}
|
||||
s.Config().Options.SkipProcessingFormats = []imagetype.Type{imagetype.SVG}
|
||||
|
||||
res := s.GET("/unsafe/plain/local:///test1.svg@svg")
|
||||
|
||||
@@ -506,7 +506,7 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithFormat() {
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestMaxSrcFileSizeGlobal() {
|
||||
config.MaxSrcFileSize = 1
|
||||
s.Config().Security.DefaultOptions.MaxSrcFileSize = 1
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(200)
|
||||
|
168
options/config.go
Normal file
168
options/config.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/ensure"
|
||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||
)
|
||||
|
||||
// URLReplacement represents a URL replacement configuration
|
||||
type URLReplacement = config.URLReplacement
|
||||
|
||||
// Config represents the configuration for options processing
|
||||
type Config struct {
|
||||
// Processing behavior defaults
|
||||
StripMetadata bool // Whether to strip metadata by default
|
||||
KeepCopyright bool // Whether to keep copyright information when stripping metadata
|
||||
StripColorProfile bool // Whether to strip color profile by default
|
||||
AutoRotate bool // Whether to auto-rotate images by default
|
||||
EnforceThumbnail bool // Whether to enforce thumbnail extraction by default
|
||||
ReturnAttachment bool // Whether to return images as attachments by default
|
||||
|
||||
// Image processing formats
|
||||
SkipProcessingFormats []imagetype.Type // List of formats to skip processing for
|
||||
|
||||
// Presets configuration
|
||||
Presets []string // Available presets
|
||||
OnlyPresets bool // Whether to allow only presets
|
||||
|
||||
// Quality settings
|
||||
Quality int // Default quality for image processing
|
||||
FormatQuality map[imagetype.Type]int // Quality settings per image format
|
||||
|
||||
// Security and validation
|
||||
AllowedProcessingOptions []string // List of allowed processing options
|
||||
|
||||
// Format preference and enforcement
|
||||
AutoWebp bool // Whether to automatically serve WebP when supported
|
||||
EnforceWebp bool // Whether to enforce WebP format
|
||||
AutoAvif bool // Whether to automatically serve AVIF when supported
|
||||
EnforceAvif bool // Whether to enforce AVIF format
|
||||
AutoJxl bool // Whether to automatically serve JXL when supported
|
||||
EnforceJxl bool // Whether to enforce JXL format
|
||||
|
||||
// Client hints
|
||||
EnableClientHints bool // Whether to enable client hints support
|
||||
|
||||
// URL processing
|
||||
ArgumentsSeparator string // Separator for URL arguments
|
||||
BaseURL string // Base URL for relative URLs
|
||||
URLReplacements []URLReplacement // URL replacement rules
|
||||
Base64URLIncludesFilename bool // Whether base64 URLs include filename
|
||||
}
|
||||
|
||||
// NewDefaultConfig creates a new default configuration for options processing
|
||||
func NewDefaultConfig() Config {
|
||||
return Config{
|
||||
// Processing behavior defaults (copied from global config defaults)
|
||||
StripMetadata: true,
|
||||
KeepCopyright: true,
|
||||
StripColorProfile: true,
|
||||
AutoRotate: true,
|
||||
EnforceThumbnail: false,
|
||||
ReturnAttachment: false,
|
||||
|
||||
OnlyPresets: false,
|
||||
|
||||
// Quality settings (copied from global config defaults)
|
||||
Quality: 80,
|
||||
FormatQuality: map[imagetype.Type]int{
|
||||
imagetype.WEBP: 79,
|
||||
imagetype.AVIF: 63,
|
||||
imagetype.JXL: 77,
|
||||
},
|
||||
|
||||
// Format preference and enforcement (copied from global config defaults)
|
||||
AutoWebp: false,
|
||||
EnforceWebp: false,
|
||||
AutoAvif: false,
|
||||
EnforceAvif: false,
|
||||
AutoJxl: false,
|
||||
EnforceJxl: false,
|
||||
|
||||
// Client hints
|
||||
EnableClientHints: false,
|
||||
|
||||
// URL processing (copied from global config defaults)
|
||||
ArgumentsSeparator: ":",
|
||||
BaseURL: "",
|
||||
Base64URLIncludesFilename: false,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadConfigFromEnv loads configuration from global config variables
|
||||
func LoadConfigFromEnv(c *Config) (*Config, error) {
|
||||
c = ensure.Ensure(c, NewDefaultConfig)
|
||||
|
||||
// Copy from global config variables
|
||||
c.StripMetadata = config.StripMetadata
|
||||
c.KeepCopyright = config.KeepCopyright
|
||||
c.StripColorProfile = config.StripColorProfile
|
||||
c.AutoRotate = config.AutoRotate
|
||||
c.EnforceThumbnail = config.EnforceThumbnail
|
||||
c.ReturnAttachment = config.ReturnAttachment
|
||||
|
||||
// Image processing formats
|
||||
c.SkipProcessingFormats = slices.Clone(config.SkipProcessingFormats)
|
||||
|
||||
// Presets configuration
|
||||
c.Presets = slices.Clone(config.Presets)
|
||||
c.OnlyPresets = config.OnlyPresets
|
||||
|
||||
// Quality settings
|
||||
c.Quality = config.Quality
|
||||
c.FormatQuality = maps.Clone(config.FormatQuality)
|
||||
|
||||
// Security and validation
|
||||
c.AllowedProcessingOptions = slices.Clone(config.AllowedProcessingOptions)
|
||||
|
||||
// Format preference and enforcement
|
||||
c.AutoWebp = config.AutoWebp
|
||||
c.EnforceWebp = config.EnforceWebp
|
||||
c.AutoAvif = config.AutoAvif
|
||||
c.EnforceAvif = config.EnforceAvif
|
||||
c.AutoJxl = config.AutoJxl
|
||||
c.EnforceJxl = config.EnforceJxl
|
||||
|
||||
// Client hints
|
||||
c.EnableClientHints = config.EnableClientHints
|
||||
|
||||
// URL processing
|
||||
c.ArgumentsSeparator = config.ArgumentsSeparator
|
||||
c.BaseURL = config.BaseURL
|
||||
c.URLReplacements = slices.Clone(config.URLReplacements)
|
||||
c.Base64URLIncludesFilename = config.Base64URLIncludesFilename
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Validate validates the configuration values
|
||||
func (c *Config) Validate() error {
|
||||
// Quality validation (copied from global config validation)
|
||||
if c.Quality <= 0 {
|
||||
return fmt.Errorf("quality should be greater than 0, now - %d", c.Quality)
|
||||
} else if c.Quality > 100 {
|
||||
return fmt.Errorf("quality can't be greater than 100, now - %d", c.Quality)
|
||||
}
|
||||
|
||||
// Format quality validation
|
||||
for format, quality := range c.FormatQuality {
|
||||
if quality <= 0 {
|
||||
return fmt.Errorf("format quality for %s should be greater than 0, now - %d", format, quality)
|
||||
} else if quality > 100 {
|
||||
return fmt.Errorf("format quality for %s can't be greater than 100, now - %d", format, quality)
|
||||
}
|
||||
}
|
||||
|
||||
// Arguments separator validation
|
||||
if c.ArgumentsSeparator == "" {
|
||||
return errors.New("arguments separator cannot be empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
45
options/factory.go
Normal file
45
options/factory.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/imgproxy/imgproxy/v3/security"
|
||||
)
|
||||
|
||||
// Presets is a map of preset names to their corresponding urlOptions
|
||||
type Presets = map[string]urlOptions
|
||||
|
||||
// Factory creates ProcessingOptions instances
|
||||
type Factory struct {
|
||||
config *Config // Factory configuration
|
||||
security *security.Checker // Security checker for generating security options
|
||||
presets Presets // Parsed presets
|
||||
defaultPO *ProcessingOptions // Default processing options
|
||||
}
|
||||
|
||||
// NewFactory creates new Factory instance
|
||||
func NewFactory(config *Config, security *security.Checker) (*Factory, error) {
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f := &Factory{
|
||||
config: config,
|
||||
security: security,
|
||||
presets: make(map[string]urlOptions),
|
||||
defaultPO: newDefaultProcessingOptions(config, security),
|
||||
}
|
||||
|
||||
if err := f.parsePresets(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := f.validatePresets(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// NewProcessingOptions creates new ProcessingOptions instance
|
||||
func (f *Factory) NewProcessingOptions() *ProcessingOptions {
|
||||
return f.defaultPO.clone()
|
||||
}
|
@@ -5,11 +5,10 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var presets map[string]urlOptions
|
||||
|
||||
func ParsePresets(presetStrs []string) error {
|
||||
for _, presetStr := range presetStrs {
|
||||
if err := parsePreset(presetStr); err != nil {
|
||||
// parsePresets parses presets from the config and fills the presets map
|
||||
func (f *Factory) parsePresets() error {
|
||||
for _, presetStr := range f.config.Presets {
|
||||
if err := f.parsePreset(presetStr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -17,7 +16,8 @@ func ParsePresets(presetStrs []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePreset(presetStr string) error {
|
||||
// parsePreset parses a preset string and returns the name and options
|
||||
func (f *Factory) parsePreset(presetStr string) error {
|
||||
presetStr = strings.Trim(presetStr, " ")
|
||||
|
||||
if len(presetStr) == 0 || strings.HasPrefix(presetStr, "#") {
|
||||
@@ -27,39 +27,41 @@ func parsePreset(presetStr string) error {
|
||||
parts := strings.Split(presetStr, "=")
|
||||
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("Invalid preset string: %s", presetStr)
|
||||
return fmt.Errorf("invalid preset string: %s", presetStr)
|
||||
}
|
||||
|
||||
name := strings.Trim(parts[0], " ")
|
||||
if len(name) == 0 {
|
||||
return fmt.Errorf("Empty preset name: %s", presetStr)
|
||||
return fmt.Errorf("empty preset name: %s", presetStr)
|
||||
}
|
||||
|
||||
value := strings.Trim(parts[1], " ")
|
||||
if len(value) == 0 {
|
||||
return fmt.Errorf("Empty preset value: %s", presetStr)
|
||||
return fmt.Errorf("empty preset value: %s", presetStr)
|
||||
}
|
||||
|
||||
optsStr := strings.Split(value, "/")
|
||||
|
||||
opts, rest := parseURLOptions(optsStr)
|
||||
opts, rest := f.parseURLOptions(optsStr)
|
||||
|
||||
if len(rest) > 0 {
|
||||
return fmt.Errorf("Invalid preset value: %s", presetStr)
|
||||
return fmt.Errorf("invalid preset value: %s", presetStr)
|
||||
}
|
||||
|
||||
if presets == nil {
|
||||
presets = make(map[string]urlOptions)
|
||||
if f.presets == nil {
|
||||
f.presets = make(Presets)
|
||||
}
|
||||
presets[name] = opts
|
||||
|
||||
f.presets[name] = opts
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidatePresets() error {
|
||||
for name, opts := range presets {
|
||||
po := NewProcessingOptions()
|
||||
if err := applyURLOptions(po, opts, true, name); err != nil {
|
||||
// validatePresets validates all presets by applying them to a new ProcessingOptions instance
|
||||
func (f *Factory) validatePresets() error {
|
||||
for name, opts := range f.presets {
|
||||
po := f.NewProcessingOptions()
|
||||
if err := f.applyURLOptions(po, opts, true, name); err != nil {
|
||||
return fmt.Errorf("Error in preset `%s`: %s", name, err)
|
||||
}
|
||||
}
|
||||
|
@@ -1,101 +1,93 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/security"
|
||||
"github.com/imgproxy/imgproxy/v3/testutil"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
)
|
||||
|
||||
type PresetsTestSuite struct{ suite.Suite }
|
||||
type PresetsTestSuite struct {
|
||||
testutil.LazySuite
|
||||
|
||||
func (s *PresetsTestSuite) SetupTest() {
|
||||
config.Reset()
|
||||
// Reset presets
|
||||
presets = make(map[string]urlOptions)
|
||||
security *security.Checker
|
||||
}
|
||||
|
||||
func (s *PresetsTestSuite) SetupSuite() {
|
||||
c := security.NewDefaultConfig()
|
||||
security, err := security.New(&c)
|
||||
s.Require().NoError(err)
|
||||
s.security = security
|
||||
}
|
||||
|
||||
func (s *PresetsTestSuite) newFactory(presets ...string) (*Factory, error) {
|
||||
c := NewDefaultConfig()
|
||||
c.Presets = presets
|
||||
return NewFactory(&c, s.security)
|
||||
}
|
||||
|
||||
func (s *PresetsTestSuite) TestParsePreset() {
|
||||
err := parsePreset("test=resize:fit:100:200/sharpen:2")
|
||||
f, err := s.newFactory("test=resize:fit:100:200/sharpen:2")
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().Equal(urlOptions{
|
||||
urlOption{Name: "resize", Args: []string{"fit", "100", "200"}},
|
||||
urlOption{Name: "sharpen", Args: []string{"2"}},
|
||||
}, presets["test"])
|
||||
}, f.presets["test"])
|
||||
}
|
||||
|
||||
func (s *PresetsTestSuite) TestParsePresetInvalidString() {
|
||||
presetStr := "resize:fit:100:200/sharpen:2"
|
||||
err := parsePreset(presetStr)
|
||||
_, err := s.newFactory(presetStr)
|
||||
|
||||
s.Require().Equal(fmt.Errorf("Invalid preset string: %s", presetStr), err)
|
||||
s.Require().Empty(presets)
|
||||
s.Require().Error(err, "invalid preset string: %s", presetStr)
|
||||
}
|
||||
|
||||
func (s *PresetsTestSuite) TestParsePresetEmptyName() {
|
||||
presetStr := "=resize:fit:100:200/sharpen:2"
|
||||
err := parsePreset(presetStr)
|
||||
_, err := s.newFactory(presetStr)
|
||||
|
||||
s.Require().Equal(fmt.Errorf("Empty preset name: %s", presetStr), err)
|
||||
s.Require().Empty(presets)
|
||||
s.Require().Error(err, "empty preset name: %s", presetStr)
|
||||
}
|
||||
|
||||
func (s *PresetsTestSuite) TestParsePresetEmptyValue() {
|
||||
presetStr := "test="
|
||||
err := parsePreset(presetStr)
|
||||
_, err := s.newFactory(presetStr)
|
||||
|
||||
s.Require().Equal(fmt.Errorf("Empty preset value: %s", presetStr), err)
|
||||
s.Require().Empty(presets)
|
||||
s.Require().Error(err, "empty preset value: %s", presetStr)
|
||||
}
|
||||
|
||||
func (s *PresetsTestSuite) TestParsePresetInvalidValue() {
|
||||
presetStr := "test=resize:fit:100:200/sharpen:2/blur"
|
||||
err := parsePreset(presetStr)
|
||||
_, err := s.newFactory(presetStr)
|
||||
|
||||
s.Require().Equal(fmt.Errorf("Invalid preset value: %s", presetStr), err)
|
||||
s.Require().Empty(presets)
|
||||
s.Require().Error(err, "invalid preset value: %s", presetStr)
|
||||
}
|
||||
|
||||
func (s *PresetsTestSuite) TestParsePresetEmptyString() {
|
||||
err := parsePreset(" ")
|
||||
f, err := s.newFactory(" ")
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Empty(presets)
|
||||
s.Require().Empty(f.presets)
|
||||
}
|
||||
|
||||
func (s *PresetsTestSuite) TestParsePresetComment() {
|
||||
err := parsePreset("# test=resize:fit:100:200/sharpen:2")
|
||||
f, err := s.newFactory("# test=resize:fit:100:200/sharpen:2")
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Empty(presets)
|
||||
s.Require().Empty(f.presets)
|
||||
}
|
||||
|
||||
func (s *PresetsTestSuite) TestValidatePresets() {
|
||||
presets = map[string]urlOptions{
|
||||
"test": {
|
||||
urlOption{Name: "resize", Args: []string{"fit", "100", "200"}},
|
||||
urlOption{Name: "sharpen", Args: []string{"2"}},
|
||||
},
|
||||
}
|
||||
|
||||
err := ValidatePresets()
|
||||
f, err := s.newFactory("test=resize:fit:100:200/sharpen:2")
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotEmpty(f.presets)
|
||||
}
|
||||
|
||||
func (s *PresetsTestSuite) TestValidatePresetsInvalid() {
|
||||
presets = map[string]urlOptions{
|
||||
"test": {
|
||||
urlOption{Name: "resize", Args: []string{"fit", "-1", "-2"}},
|
||||
urlOption{Name: "sharpen", Args: []string{"2"}},
|
||||
},
|
||||
}
|
||||
|
||||
err := ValidatePresets()
|
||||
_, err := s.newFactory("test=resize:fit:-1:-2/sharpen:2")
|
||||
|
||||
s.Require().Error(err)
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ package options
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||
"github.com/imgproxy/imgproxy/v3/imath"
|
||||
@@ -118,34 +117,10 @@ type ProcessingOptions struct {
|
||||
SecurityOptions security.Options
|
||||
|
||||
defaultQuality int
|
||||
defaultOptions *ProcessingOptions
|
||||
}
|
||||
|
||||
func NewProcessingOptions() *ProcessingOptions {
|
||||
// NOTE: This is temporary hack until ProcessingOptions does not have Factory
|
||||
securityCfg, err := security.LoadConfigFromEnv(nil)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
// NOTE: This is a temporary workaround for logrus bug that deadlocks
|
||||
// if log is used within another log (issue 1448)
|
||||
if len(securityCfg.Salts) == 0 {
|
||||
securityCfg.Salts = [][]byte{[]byte("logrusbugworkaround")}
|
||||
}
|
||||
|
||||
if len(securityCfg.Keys) == 0 {
|
||||
securityCfg.Keys = [][]byte{[]byte("logrusbugworkaround")}
|
||||
}
|
||||
// END OF WORKAROUND
|
||||
|
||||
security, err := security.New(securityCfg)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
securityOptions := security.NewOptions()
|
||||
// NOTE: This is temporary hack until ProcessingOptions does not have Factory
|
||||
|
||||
func newDefaultProcessingOptions(config *Config, security *security.Checker) *ProcessingOptions {
|
||||
po := ProcessingOptions{
|
||||
ResizingType: ResizeFit,
|
||||
Width: 0,
|
||||
@@ -160,6 +135,7 @@ func NewProcessingOptions() *ProcessingOptions {
|
||||
Trim: TrimOptions{Enabled: false, Threshold: 10, Smart: true},
|
||||
Rotate: 0,
|
||||
Quality: 0,
|
||||
FormatQuality: maps.Clone(config.FormatQuality),
|
||||
MaxBytes: 0,
|
||||
Format: imagetype.Unknown,
|
||||
Background: vips.Color{R: 255, G: 255, B: 255},
|
||||
@@ -174,20 +150,13 @@ func NewProcessingOptions() *ProcessingOptions {
|
||||
EnforceThumbnail: config.EnforceThumbnail,
|
||||
ReturnAttachment: config.ReturnAttachment,
|
||||
|
||||
SkipProcessingFormats: append([]imagetype.Type(nil), config.SkipProcessingFormats...),
|
||||
UsedPresets: make([]string, 0, len(config.Presets)),
|
||||
SkipProcessingFormats: slices.Clone(config.SkipProcessingFormats),
|
||||
|
||||
SecurityOptions: securityOptions,
|
||||
SecurityOptions: security.NewOptions(),
|
||||
|
||||
// Basically, we need this to update ETag when `IMGPROXY_QUALITY` is changed
|
||||
defaultQuality: config.Quality,
|
||||
}
|
||||
|
||||
po.FormatQuality = make(map[imagetype.Type]int, len(config.FormatQuality))
|
||||
for k, v := range config.FormatQuality {
|
||||
po.FormatQuality[k] = v
|
||||
}
|
||||
|
||||
return &po
|
||||
}
|
||||
|
||||
@@ -206,7 +175,7 @@ func (po *ProcessingOptions) GetQuality() int {
|
||||
}
|
||||
|
||||
func (po *ProcessingOptions) Diff() structdiff.Entries {
|
||||
return structdiff.Diff(NewProcessingOptions(), po)
|
||||
return structdiff.Diff(po.defaultOptions, po)
|
||||
}
|
||||
|
||||
func (po *ProcessingOptions) String() string {
|
||||
@@ -217,6 +186,34 @@ func (po *ProcessingOptions) MarshalJSON() ([]byte, error) {
|
||||
return po.Diff().MarshalJSON()
|
||||
}
|
||||
|
||||
// Default returns the ProcessingOptions instance with defaults set
|
||||
func (po *ProcessingOptions) Default() *ProcessingOptions {
|
||||
return po.defaultOptions.clone()
|
||||
}
|
||||
|
||||
// clone clones ProcessingOptions struct and its slices and maps
|
||||
func (po *ProcessingOptions) clone() *ProcessingOptions {
|
||||
clone := *po
|
||||
|
||||
clone.FormatQuality = maps.Clone(po.FormatQuality)
|
||||
clone.SkipProcessingFormats = slices.Clone(po.SkipProcessingFormats)
|
||||
clone.UsedPresets = slices.Clone(po.UsedPresets)
|
||||
|
||||
if po.Expires != nil {
|
||||
poExipres := *po.Expires
|
||||
clone.Expires = &poExipres
|
||||
}
|
||||
|
||||
// Copy the pointer to the default options struct from parent.
|
||||
// Nil means that we have just cloned the default options struct itself
|
||||
// so we set it as default options.
|
||||
if clone.defaultOptions == nil {
|
||||
clone.defaultOptions = po
|
||||
}
|
||||
|
||||
return &clone
|
||||
}
|
||||
|
||||
func parseDimension(d *int, name, arg string) error {
|
||||
if v, err := strconv.Atoi(arg); err == nil && v >= 0 {
|
||||
*d = v
|
||||
@@ -707,9 +704,9 @@ func applyPixelateOption(po *ProcessingOptions, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyPresetOption(po *ProcessingOptions, args []string, usedPresets ...string) error {
|
||||
func (f *Factory) applyPresetOption(po *ProcessingOptions, args []string, usedPresets ...string) error {
|
||||
for _, preset := range args {
|
||||
if p, ok := presets[preset]; ok {
|
||||
if p, ok := f.presets[preset]; ok {
|
||||
if slices.Contains(usedPresets, preset) {
|
||||
log.Warningf("Recursive preset usage is detected: %s", preset)
|
||||
continue
|
||||
@@ -717,7 +714,7 @@ func applyPresetOption(po *ProcessingOptions, args []string, usedPresets ...stri
|
||||
|
||||
po.UsedPresets = append(po.UsedPresets, preset)
|
||||
|
||||
if err := applyURLOptions(po, p, true, append(usedPresets, preset)...); err != nil {
|
||||
if err := f.applyURLOptions(po, p, true, append(usedPresets, preset)...); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@@ -1010,7 +1007,7 @@ func applyMaxResultDimensionOption(po *ProcessingOptions, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyURLOption(po *ProcessingOptions, name string, args []string, usedPresets ...string) error {
|
||||
func (f *Factory) applyURLOption(po *ProcessingOptions, name string, args []string, usedPresets ...string) error {
|
||||
switch name {
|
||||
case "resize", "rs":
|
||||
return applyResizeOption(po, args)
|
||||
@@ -1090,7 +1087,7 @@ func applyURLOption(po *ProcessingOptions, name string, args []string, usedPrese
|
||||
return applyReturnAttachmentOption(po, args)
|
||||
// Presets
|
||||
case "preset", "pr":
|
||||
return applyPresetOption(po, args, usedPresets...)
|
||||
return f.applyPresetOption(po, args, usedPresets...)
|
||||
// Security
|
||||
case "max_src_resolution", "msr":
|
||||
return applyMaxSrcResolutionOption(po, args)
|
||||
@@ -1107,15 +1104,15 @@ func applyURLOption(po *ProcessingOptions, name string, args []string, usedPrese
|
||||
return newUnknownOptionError("processing", name)
|
||||
}
|
||||
|
||||
func applyURLOptions(po *ProcessingOptions, options urlOptions, allowAll bool, usedPresets ...string) error {
|
||||
allowAll = allowAll || len(config.AllowedProcessingOptions) == 0
|
||||
func (f *Factory) applyURLOptions(po *ProcessingOptions, options urlOptions, allowAll bool, usedPresets ...string) error {
|
||||
allowAll = allowAll || len(f.config.AllowedProcessingOptions) == 0
|
||||
|
||||
for _, opt := range options {
|
||||
if !allowAll && !slices.Contains(config.AllowedProcessingOptions, opt.Name) {
|
||||
if !allowAll && !slices.Contains(f.config.AllowedProcessingOptions, opt.Name) {
|
||||
return newForbiddenOptionError("processing", opt.Name)
|
||||
}
|
||||
|
||||
if err := applyURLOption(po, opt.Name, opt.Args, usedPresets...); err != nil {
|
||||
if err := f.applyURLOption(po, opt.Name, opt.Args, usedPresets...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1123,27 +1120,27 @@ func applyURLOptions(po *ProcessingOptions, options urlOptions, allowAll bool, u
|
||||
return nil
|
||||
}
|
||||
|
||||
func defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
|
||||
po := NewProcessingOptions()
|
||||
func (f *Factory) defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
|
||||
po := f.NewProcessingOptions()
|
||||
|
||||
headerAccept := headers.Get("Accept")
|
||||
|
||||
if strings.Contains(headerAccept, "image/webp") {
|
||||
po.PreferWebP = config.AutoWebp || config.EnforceWebp
|
||||
po.EnforceWebP = config.EnforceWebp
|
||||
po.PreferWebP = f.config.AutoWebp || f.config.EnforceWebp
|
||||
po.EnforceWebP = f.config.EnforceWebp
|
||||
}
|
||||
|
||||
if strings.Contains(headerAccept, "image/avif") {
|
||||
po.PreferAvif = config.AutoAvif || config.EnforceAvif
|
||||
po.EnforceAvif = config.EnforceAvif
|
||||
po.PreferAvif = f.config.AutoAvif || f.config.EnforceAvif
|
||||
po.EnforceAvif = f.config.EnforceAvif
|
||||
}
|
||||
|
||||
if strings.Contains(headerAccept, "image/jxl") {
|
||||
po.PreferJxl = config.AutoJxl || config.EnforceJxl
|
||||
po.EnforceJxl = config.EnforceJxl
|
||||
po.PreferJxl = f.config.AutoJxl || f.config.EnforceJxl
|
||||
po.EnforceJxl = f.config.EnforceJxl
|
||||
}
|
||||
|
||||
if config.EnableClientHints {
|
||||
if f.config.EnableClientHints {
|
||||
headerDPR := headers.Get("Sec-CH-DPR")
|
||||
if len(headerDPR) == 0 {
|
||||
headerDPR = headers.Get("DPR")
|
||||
@@ -1165,8 +1162,8 @@ func defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := presets["default"]; ok {
|
||||
if err := applyPresetOption(po, []string{"default"}); err != nil {
|
||||
if _, ok := f.presets["default"]; ok {
|
||||
if err := f.applyPresetOption(po, []string{"default"}); err != nil {
|
||||
return po, err
|
||||
}
|
||||
}
|
||||
@@ -1174,80 +1171,21 @@ func defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
|
||||
return po, nil
|
||||
}
|
||||
|
||||
func parsePathOptions(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
|
||||
if _, ok := resizeTypes[parts[0]]; ok {
|
||||
return nil, "", newInvalidURLError("It looks like you're using the deprecated basic URL format")
|
||||
}
|
||||
|
||||
po, err := defaultProcessingOptions(headers)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
options, urlParts := parseURLOptions(parts)
|
||||
|
||||
if err = applyURLOptions(po, options, false); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
url, extension, err := DecodeURL(urlParts)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if !po.Raw && len(extension) > 0 {
|
||||
if err = applyFormatOption(po, []string{extension}); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
return po, url, nil
|
||||
}
|
||||
|
||||
func parsePathPresets(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
|
||||
po, err := defaultProcessingOptions(headers)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
presets := strings.Split(parts[0], config.ArgumentsSeparator)
|
||||
urlParts := parts[1:]
|
||||
|
||||
if err = applyPresetOption(po, presets); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
url, extension, err := DecodeURL(urlParts)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if !po.Raw && len(extension) > 0 {
|
||||
if err = applyFormatOption(po, []string{extension}); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
return po, url, nil
|
||||
}
|
||||
|
||||
func ParsePath(path string, headers http.Header) (*ProcessingOptions, string, error) {
|
||||
// ParsePath parses the given request path and returns the processing options and image URL
|
||||
func (f *Factory) ParsePath(
|
||||
path string,
|
||||
headers http.Header,
|
||||
) (po *ProcessingOptions, imageURL string, err error) {
|
||||
if path == "" || path == "/" {
|
||||
return nil, "", newInvalidURLError("Invalid path: %s", path)
|
||||
return nil, "", newInvalidURLError("invalid path: %s", path)
|
||||
}
|
||||
|
||||
parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
|
||||
|
||||
var (
|
||||
imageURL string
|
||||
po *ProcessingOptions
|
||||
err error
|
||||
)
|
||||
|
||||
if config.OnlyPresets {
|
||||
po, imageURL, err = parsePathPresets(parts, headers)
|
||||
if f.config.OnlyPresets {
|
||||
po, imageURL, err = f.parsePathPresets(parts, headers)
|
||||
} else {
|
||||
po, imageURL, err = parsePathOptions(parts, headers)
|
||||
po, imageURL, err = f.parsePathOptions(parts, headers)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -1256,3 +1194,62 @@ func ParsePath(path string, headers http.Header) (*ProcessingOptions, string, er
|
||||
|
||||
return po, imageURL, nil
|
||||
}
|
||||
|
||||
// parsePathOptions parses processing options from the URL path
|
||||
func (f *Factory) parsePathOptions(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
|
||||
if _, ok := resizeTypes[parts[0]]; ok {
|
||||
return nil, "", newInvalidURLError("It looks like you're using the deprecated basic URL format")
|
||||
}
|
||||
|
||||
po, err := f.defaultProcessingOptions(headers)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
options, urlParts := f.parseURLOptions(parts)
|
||||
|
||||
if err = f.applyURLOptions(po, options, false); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
url, extension, err := f.DecodeURL(urlParts)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if !po.Raw && len(extension) > 0 {
|
||||
if err = applyFormatOption(po, []string{extension}); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
return po, url, nil
|
||||
}
|
||||
|
||||
// parsePathPresets parses presets from the URL path
|
||||
func (f *Factory) parsePathPresets(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
|
||||
po, err := f.defaultProcessingOptions(headers)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
presets := strings.Split(parts[0], f.config.ArgumentsSeparator)
|
||||
urlParts := parts[1:]
|
||||
|
||||
if err = f.applyPresetOption(po, presets); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
url, extension, err := f.DecodeURL(urlParts)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if !po.Raw && len(extension) > 0 {
|
||||
if err = applyFormatOption(po, []string{extension}); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
return po, url, nil
|
||||
}
|
||||
|
@@ -8,25 +8,65 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"time"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||
"github.com/imgproxy/imgproxy/v3/security"
|
||||
"github.com/imgproxy/imgproxy/v3/testutil"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ProcessingOptionsTestSuite struct{ suite.Suite }
|
||||
type ProcessingOptionsTestSuite struct {
|
||||
testutil.LazySuite
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) SetupTest() {
|
||||
config.Reset()
|
||||
// Reset presets
|
||||
presets = make(map[string]urlOptions)
|
||||
securityCfg testutil.LazyObj[*security.Config]
|
||||
security testutil.LazyObj[*security.Checker]
|
||||
|
||||
config testutil.LazyObj[*Config]
|
||||
factory testutil.LazyObj[*Factory]
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) SetupSuite() {
|
||||
s.config, _ = testutil.NewLazySuiteObj(
|
||||
s,
|
||||
func() (*Config, error) {
|
||||
c := NewDefaultConfig()
|
||||
return &c, nil
|
||||
},
|
||||
)
|
||||
|
||||
s.securityCfg, _ = testutil.NewLazySuiteObj(
|
||||
s,
|
||||
func() (*security.Config, error) {
|
||||
c := security.NewDefaultConfig()
|
||||
return &c, nil
|
||||
},
|
||||
)
|
||||
|
||||
s.security, _ = testutil.NewLazySuiteObj(
|
||||
s,
|
||||
func() (*security.Checker, error) {
|
||||
return security.New(s.securityCfg())
|
||||
},
|
||||
)
|
||||
|
||||
s.factory, _ = testutil.NewLazySuiteObj(
|
||||
s,
|
||||
func() (*Factory, error) {
|
||||
return NewFactory(s.config(), s.security())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) SetupSubTest() {
|
||||
s.ResetLazyObjects()
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParseBase64URL() {
|
||||
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
|
||||
path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
|
||||
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(originURL, imageURL)
|
||||
@@ -34,11 +74,11 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URL() {
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithFilename() {
|
||||
config.Base64URLIncludesFilename = true
|
||||
s.config().Base64URLIncludesFilename = true
|
||||
|
||||
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
|
||||
path := fmt.Sprintf("/size:100:100/%s.png/puppy.jpg", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
|
||||
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(originURL, imageURL)
|
||||
@@ -48,7 +88,7 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithFilename() {
|
||||
func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithoutExtension() {
|
||||
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
|
||||
path := fmt.Sprintf("/size:100:100/%s", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
|
||||
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(originURL, imageURL)
|
||||
@@ -56,26 +96,26 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithoutExtension() {
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithBase() {
|
||||
config.BaseURL = "http://images.dev/"
|
||||
s.config().BaseURL = "http://images.dev/"
|
||||
|
||||
originURL := "lorem/ipsum.jpg?param=value"
|
||||
path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
|
||||
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(fmt.Sprintf("%s%s", config.BaseURL, originURL), imageURL)
|
||||
s.Require().Equal(fmt.Sprintf("%s%s", s.config().BaseURL, originURL), imageURL)
|
||||
s.Require().Equal(imagetype.PNG, po.Format)
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithReplacement() {
|
||||
config.URLReplacements = []config.URLReplacement{
|
||||
s.config().URLReplacements = []config.URLReplacement{
|
||||
{Regexp: regexp.MustCompile("^test://([^/]*)/"), Replacement: "test2://images.dev/${1}/dolor/"},
|
||||
{Regexp: regexp.MustCompile("^test2://"), Replacement: "http://"},
|
||||
}
|
||||
|
||||
originURL := "test://lorem/ipsum.jpg?param=value"
|
||||
path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
|
||||
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal("http://images.dev/lorem/dolor/ipsum.jpg?param=value", imageURL)
|
||||
@@ -85,7 +125,7 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithReplacement() {
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePlainURL() {
|
||||
originURL := "http://images.dev/lorem/ipsum.jpg"
|
||||
path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
|
||||
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(originURL, imageURL)
|
||||
@@ -96,7 +136,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithoutExtension() {
|
||||
originURL := "http://images.dev/lorem/ipsum.jpg"
|
||||
path := fmt.Sprintf("/size:100:100/plain/%s", originURL)
|
||||
|
||||
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(originURL, imageURL)
|
||||
@@ -105,7 +145,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithoutExtension() {
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscaped() {
|
||||
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
|
||||
path := fmt.Sprintf("/size:100:100/plain/%s@png", url.PathEscape(originURL))
|
||||
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(originURL, imageURL)
|
||||
@@ -113,26 +153,26 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscaped() {
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithBase() {
|
||||
config.BaseURL = "http://images.dev/"
|
||||
s.config().BaseURL = "http://images.dev/"
|
||||
|
||||
originURL := "lorem/ipsum.jpg"
|
||||
path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
|
||||
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(fmt.Sprintf("%s%s", config.BaseURL, originURL), imageURL)
|
||||
s.Require().Equal(fmt.Sprintf("%s%s", s.config().BaseURL, originURL), imageURL)
|
||||
s.Require().Equal(imagetype.PNG, po.Format)
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithReplacement() {
|
||||
config.URLReplacements = []config.URLReplacement{
|
||||
s.config().URLReplacements = []config.URLReplacement{
|
||||
{Regexp: regexp.MustCompile("^test://([^/]*)/"), Replacement: "test2://images.dev/${1}/dolor/"},
|
||||
{Regexp: regexp.MustCompile("^test2://"), Replacement: "http://"},
|
||||
}
|
||||
|
||||
originURL := "test://lorem/ipsum.jpg"
|
||||
path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
|
||||
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal("http://images.dev/lorem/dolor/ipsum.jpg", imageURL)
|
||||
@@ -140,22 +180,22 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithReplacement() {
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscapedWithBase() {
|
||||
config.BaseURL = "http://images.dev/"
|
||||
s.config().BaseURL = "http://images.dev/"
|
||||
|
||||
originURL := "lorem/ipsum.jpg?param=value"
|
||||
path := fmt.Sprintf("/size:100:100/plain/%s@png", url.PathEscape(originURL))
|
||||
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(fmt.Sprintf("%s%s", config.BaseURL, originURL), imageURL)
|
||||
s.Require().Equal(fmt.Sprintf("%s%s", s.config().BaseURL, originURL), imageURL)
|
||||
s.Require().Equal(imagetype.PNG, po.Format)
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParseWithArgumentsSeparator() {
|
||||
config.ArgumentsSeparator = ","
|
||||
s.config().ArgumentsSeparator = ","
|
||||
|
||||
path := "/size,100,100,1/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -164,27 +204,9 @@ func (s *ProcessingOptionsTestSuite) TestParseWithArgumentsSeparator() {
|
||||
s.Require().True(po.Enlarge)
|
||||
}
|
||||
|
||||
// func (s *ProcessingOptionsTestSuite) TestParseURLAllowedSource() {
|
||||
// config.AllowedSources = []string{"local://", "http://images.dev/"}
|
||||
|
||||
// path := "/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
// _, _, err := ParsePath(path, make(http.Header))
|
||||
|
||||
// s.Require().NoError(err)
|
||||
// }
|
||||
|
||||
// func (s *ProcessingOptionsTestSuite) TestParseURLNotAllowedSource() {
|
||||
// config.AllowedSources = []string{"local://", "http://images.dev/"}
|
||||
|
||||
// path := "/plain/s3://images/lorem/ipsum.jpg"
|
||||
// _, _, err := ParsePath(path, make(http.Header))
|
||||
|
||||
// s.Require().Error(err)
|
||||
// }
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathFormat() {
|
||||
path := "/format:webp/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -193,7 +215,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathFormat() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathResize() {
|
||||
path := "/resize:fill:100:200:1/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -205,7 +227,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathResize() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathResizingType() {
|
||||
path := "/resizing_type:fill/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -214,7 +236,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathResizingType() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathSize() {
|
||||
path := "/size:100:200:1/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -225,7 +247,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathSize() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathWidth() {
|
||||
path := "/width:100/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -234,7 +256,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWidth() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathHeight() {
|
||||
path := "/height:100/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -243,7 +265,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathHeight() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathEnlarge() {
|
||||
path := "/enlarge:1/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -252,7 +274,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathEnlarge() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathExtend() {
|
||||
path := "/extend:1:so:10:20/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -264,21 +286,21 @@ func (s *ProcessingOptionsTestSuite) TestParsePathExtend() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathExtendSmartGravity() {
|
||||
path := "/extend:1:sm/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
_, _, err := ParsePath(path, make(http.Header))
|
||||
_, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().Error(err)
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathExtendReplicateGravity() {
|
||||
path := "/extend:1:re/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
_, _, err := ParsePath(path, make(http.Header))
|
||||
_, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().Error(err)
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathGravity() {
|
||||
path := "/gravity:soea/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -287,7 +309,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathGravity() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathGravityFocusPoint() {
|
||||
path := "/gravity:fp:0.5:0.75/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -298,14 +320,14 @@ func (s *ProcessingOptionsTestSuite) TestParsePathGravityFocusPoint() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathGravityReplicate() {
|
||||
path := "/gravity:re/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
_, _, err := ParsePath(path, make(http.Header))
|
||||
_, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().Error(err)
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathCrop() {
|
||||
path := "/crop:100:200/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -318,7 +340,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathCrop() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathCropGravity() {
|
||||
path := "/crop:100:200:nowe:10:20/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -331,14 +353,14 @@ func (s *ProcessingOptionsTestSuite) TestParsePathCropGravity() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathCropGravityReplicate() {
|
||||
path := "/crop:100:200:re/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
_, _, err := ParsePath(path, make(http.Header))
|
||||
_, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().Error(err)
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathQuality() {
|
||||
path := "/quality:55/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -347,7 +369,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathQuality() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathBackground() {
|
||||
path := "/background:128:129:130/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -359,7 +381,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBackground() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundHex() {
|
||||
path := "/background:ffddee/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -371,7 +393,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundHex() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundDisable() {
|
||||
path := "/background:fff/background:/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -380,7 +402,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundDisable() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathBlur() {
|
||||
path := "/blur:0.2/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -389,7 +411,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBlur() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathSharpen() {
|
||||
path := "/sharpen:0.2/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -397,7 +419,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathSharpen() {
|
||||
}
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathDpr() {
|
||||
path := "/dpr:2/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -405,7 +427,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathDpr() {
|
||||
}
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathWatermark() {
|
||||
path := "/watermark:0.5:soea:10:20:0.6/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -417,17 +439,13 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWatermark() {
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathPreset() {
|
||||
presets["test1"] = urlOptions{
|
||||
urlOption{Name: "resizing_type", Args: []string{"fill"}},
|
||||
}
|
||||
|
||||
presets["test2"] = urlOptions{
|
||||
urlOption{Name: "blur", Args: []string{"0.2"}},
|
||||
urlOption{Name: "quality", Args: []string{"50"}},
|
||||
s.config().Presets = []string{
|
||||
"test1=resizing_type:fill",
|
||||
"test2=blur:0.2/quality:50",
|
||||
}
|
||||
|
||||
path := "/preset:test1:test2/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -437,14 +455,12 @@ func (s *ProcessingOptionsTestSuite) TestParsePathPreset() {
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathPresetDefault() {
|
||||
presets["default"] = urlOptions{
|
||||
urlOption{Name: "resizing_type", Args: []string{"fill"}},
|
||||
urlOption{Name: "blur", Args: []string{"0.2"}},
|
||||
urlOption{Name: "quality", Args: []string{"50"}},
|
||||
s.config().Presets = []string{
|
||||
"default=resizing_type:fill/blur:0.2/quality:50",
|
||||
}
|
||||
|
||||
path := "/quality:70/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -454,18 +470,13 @@ func (s *ProcessingOptionsTestSuite) TestParsePathPresetDefault() {
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathPresetLoopDetection() {
|
||||
presets["test1"] = urlOptions{
|
||||
urlOption{Name: "resizing_type", Args: []string{"fill"}},
|
||||
urlOption{Name: "preset", Args: []string{"test2"}},
|
||||
}
|
||||
|
||||
presets["test2"] = urlOptions{
|
||||
urlOption{Name: "blur", Args: []string{"0.2"}},
|
||||
urlOption{Name: "preset", Args: []string{"test1"}},
|
||||
s.config().Presets = []string{
|
||||
"test1=resizing_type:fill/preset:test2",
|
||||
"test2=blur:0.2/preset:test1",
|
||||
}
|
||||
|
||||
path := "/preset:test1/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -474,7 +485,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathPresetLoopDetection() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathCachebuster() {
|
||||
path := "/cachebuster:123/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -483,7 +494,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathCachebuster() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathStripMetadata() {
|
||||
path := "/strip_metadata:true/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -491,11 +502,11 @@ func (s *ProcessingOptionsTestSuite) TestParsePathStripMetadata() {
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathWebpDetection() {
|
||||
config.AutoWebp = true
|
||||
s.config().AutoWebp = true
|
||||
|
||||
path := "/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
headers := http.Header{"Accept": []string{"image/webp"}}
|
||||
po, _, err := ParsePath(path, headers)
|
||||
po, _, err := s.factory().ParsePath(path, headers)
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -504,11 +515,11 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWebpDetection() {
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathWebpEnforce() {
|
||||
config.EnforceWebp = true
|
||||
s.config().EnforceWebp = true
|
||||
|
||||
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||
headers := http.Header{"Accept": []string{"image/webp"}}
|
||||
po, _, err := ParsePath(path, headers)
|
||||
po, _, err := s.factory().ParsePath(path, headers)
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -517,11 +528,11 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWebpEnforce() {
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeader() {
|
||||
config.EnableClientHints = true
|
||||
s.config().EnableClientHints = true
|
||||
|
||||
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||
headers := http.Header{"Width": []string{"100"}}
|
||||
po, _, err := ParsePath(path, headers)
|
||||
po, _, err := s.factory().ParsePath(path, headers)
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -531,7 +542,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeader() {
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderDisabled() {
|
||||
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||
headers := http.Header{"Width": []string{"100"}}
|
||||
po, _, err := ParsePath(path, headers)
|
||||
po, _, err := s.factory().ParsePath(path, headers)
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -539,11 +550,11 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderDisabled() {
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderRedefine() {
|
||||
config.EnableClientHints = true
|
||||
s.config().EnableClientHints = true
|
||||
|
||||
path := "/width:150/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||
headers := http.Header{"Width": []string{"100"}}
|
||||
po, _, err := ParsePath(path, headers)
|
||||
po, _, err := s.factory().ParsePath(path, headers)
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -551,11 +562,11 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderRedefine() {
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathDprHeader() {
|
||||
config.EnableClientHints = true
|
||||
s.config().EnableClientHints = true
|
||||
|
||||
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||
headers := http.Header{"Dpr": []string{"2"}}
|
||||
po, _, err := ParsePath(path, headers)
|
||||
po, _, err := s.factory().ParsePath(path, headers)
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -565,7 +576,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathDprHeader() {
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathDprHeaderDisabled() {
|
||||
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
|
||||
headers := http.Header{"Dpr": []string{"2"}}
|
||||
po, _, err := ParsePath(path, headers)
|
||||
po, _, err := s.factory().ParsePath(path, headers)
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -575,7 +586,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathDprHeaderDisabled() {
|
||||
func (s *ProcessingOptionsTestSuite) TestParseSkipProcessing() {
|
||||
path := "/skp:jpg:png/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -585,7 +596,7 @@ func (s *ProcessingOptionsTestSuite) TestParseSkipProcessing() {
|
||||
func (s *ProcessingOptionsTestSuite) TestParseSkipProcessingInvalid() {
|
||||
path := "/skp:jpg:png:bad_format/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
|
||||
_, _, err := ParsePath(path, make(http.Header))
|
||||
_, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().Error(err)
|
||||
s.Require().Equal("Invalid image format in skip processing: bad_format", err.Error())
|
||||
@@ -593,30 +604,28 @@ func (s *ProcessingOptionsTestSuite) TestParseSkipProcessingInvalid() {
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParseExpires() {
|
||||
path := "/exp:32503669200/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
_, _, err := ParsePath(path, make(http.Header))
|
||||
_, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParseExpiresExpired() {
|
||||
path := "/exp:1609448400/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
_, _, err := ParsePath(path, make(http.Header))
|
||||
_, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().Error(err, "Expired URL")
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParsePathOnlyPresets() {
|
||||
config.OnlyPresets = true
|
||||
presets["test1"] = urlOptions{
|
||||
urlOption{Name: "blur", Args: []string{"0.2"}},
|
||||
}
|
||||
presets["test2"] = urlOptions{
|
||||
urlOption{Name: "quality", Args: []string{"50"}},
|
||||
s.config().OnlyPresets = true
|
||||
s.config().Presets = []string{
|
||||
"test1=blur:0.2",
|
||||
"test2=quality:50",
|
||||
}
|
||||
|
||||
path := "/test1:test2/plain/http://images.dev/lorem/ipsum.jpg"
|
||||
|
||||
po, _, err := ParsePath(path, make(http.Header))
|
||||
po, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -625,18 +634,16 @@ func (s *ProcessingOptionsTestSuite) TestParsePathOnlyPresets() {
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParseBase64URLOnlyPresets() {
|
||||
config.OnlyPresets = true
|
||||
presets["test1"] = urlOptions{
|
||||
urlOption{Name: "blur", Args: []string{"0.2"}},
|
||||
}
|
||||
presets["test2"] = urlOptions{
|
||||
urlOption{Name: "quality", Args: []string{"50"}},
|
||||
s.config().OnlyPresets = true
|
||||
s.config().Presets = []string{
|
||||
"test1=blur:0.2",
|
||||
"test2=quality:50",
|
||||
}
|
||||
|
||||
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
|
||||
path := fmt.Sprintf("/test1:test2/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
|
||||
|
||||
po, imageURL, err := ParsePath(path, make(http.Header))
|
||||
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -646,12 +653,6 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLOnlyPresets() {
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestParseAllowedOptions() {
|
||||
config.AllowedProcessingOptions = []string{"w", "h", "pr"}
|
||||
|
||||
presets["test1"] = urlOptions{
|
||||
urlOption{Name: "blur", Args: []string{"0.2"}},
|
||||
}
|
||||
|
||||
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
|
||||
|
||||
testCases := []struct {
|
||||
@@ -666,8 +667,13 @@ func (s *ProcessingOptionsTestSuite) TestParseAllowedOptions() {
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(strings.ReplaceAll(tc.options, "/", "_"), func() {
|
||||
s.config().AllowedProcessingOptions = []string{"w", "h", "pr"}
|
||||
s.config().Presets = []string{
|
||||
"test1=blur:0.2",
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/%s/%s.png", tc.options, base64.RawURLEncoding.EncodeToString([]byte(originURL)))
|
||||
_, _, err := ParsePath(path, make(http.Header))
|
||||
_, _, err := s.factory().ParsePath(path, make(http.Header))
|
||||
|
||||
if len(tc.expectedError) > 0 {
|
||||
s.Require().Error(err)
|
||||
@@ -679,6 +685,23 @@ func (s *ProcessingOptionsTestSuite) TestParseAllowedOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProcessingOptionsTestSuite) TestProcessingOptionsClone() {
|
||||
now := time.Now()
|
||||
|
||||
// Create ProcessingOptions using factory
|
||||
original := s.factory().NewProcessingOptions()
|
||||
original.SkipProcessingFormats = []imagetype.Type{
|
||||
imagetype.PNG, imagetype.JPEG,
|
||||
}
|
||||
original.UsedPresets = []string{"preset1", "preset2"}
|
||||
original.Expires = &now
|
||||
|
||||
// Clone the original
|
||||
cloned := original.clone()
|
||||
|
||||
testutil.EqualButNotSame(s.T(), original, cloned)
|
||||
}
|
||||
|
||||
func TestProcessingOptions(t *testing.T) {
|
||||
suite.Run(t, new(ProcessingOptionsTestSuite))
|
||||
}
|
||||
|
@@ -5,28 +5,26 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
)
|
||||
|
||||
const urlTokenPlain = "plain"
|
||||
|
||||
func preprocessURL(u string) string {
|
||||
for _, repl := range config.URLReplacements {
|
||||
func (f *Factory) preprocessURL(u string) string {
|
||||
for _, repl := range f.config.URLReplacements {
|
||||
u = repl.Regexp.ReplaceAllString(u, repl.Replacement)
|
||||
}
|
||||
|
||||
if len(config.BaseURL) == 0 || strings.HasPrefix(u, config.BaseURL) {
|
||||
if len(f.config.BaseURL) == 0 || strings.HasPrefix(u, f.config.BaseURL) {
|
||||
return u
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s", config.BaseURL, u)
|
||||
return fmt.Sprintf("%s%s", f.config.BaseURL, u)
|
||||
}
|
||||
|
||||
func decodeBase64URL(parts []string) (string, string, error) {
|
||||
func (f *Factory) decodeBase64URL(parts []string) (string, string, error) {
|
||||
var format string
|
||||
|
||||
if len(parts) > 1 && config.Base64URLIncludesFilename {
|
||||
if len(parts) > 1 && f.config.Base64URLIncludesFilename {
|
||||
parts = parts[:len(parts)-1]
|
||||
}
|
||||
|
||||
@@ -50,10 +48,10 @@ func decodeBase64URL(parts []string) (string, string, error) {
|
||||
return "", "", newInvalidURLError("Invalid url encoding: %s", encoded)
|
||||
}
|
||||
|
||||
return preprocessURL(string(imageURL)), format, nil
|
||||
return f.preprocessURL(string(imageURL)), format, nil
|
||||
}
|
||||
|
||||
func decodePlainURL(parts []string) (string, string, error) {
|
||||
func (f *Factory) decodePlainURL(parts []string) (string, string, error) {
|
||||
var format string
|
||||
|
||||
encoded := strings.Join(parts, "/")
|
||||
@@ -76,17 +74,17 @@ func decodePlainURL(parts []string) (string, string, error) {
|
||||
return "", "", newInvalidURLError("Invalid url encoding: %s", encoded)
|
||||
}
|
||||
|
||||
return preprocessURL(unescaped), format, nil
|
||||
return f.preprocessURL(unescaped), format, nil
|
||||
}
|
||||
|
||||
func DecodeURL(parts []string) (string, string, error) {
|
||||
func (f *Factory) DecodeURL(parts []string) (string, string, error) {
|
||||
if len(parts) == 0 {
|
||||
return "", "", newInvalidURLError("Image URL is empty")
|
||||
}
|
||||
|
||||
if parts[0] == urlTokenPlain && len(parts) > 1 {
|
||||
return decodePlainURL(parts[1:])
|
||||
return f.decodePlainURL(parts[1:])
|
||||
}
|
||||
|
||||
return decodeBase64URL(parts)
|
||||
return f.decodeBase64URL(parts)
|
||||
}
|
||||
|
@@ -2,8 +2,6 @@ package options
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
)
|
||||
|
||||
type urlOption struct {
|
||||
@@ -13,12 +11,12 @@ type urlOption struct {
|
||||
|
||||
type urlOptions []urlOption
|
||||
|
||||
func parseURLOptions(opts []string) (urlOptions, []string) {
|
||||
func (f *Factory) parseURLOptions(opts []string) (urlOptions, []string) {
|
||||
parsed := make(urlOptions, 0, len(opts))
|
||||
urlStart := len(opts) + 1
|
||||
|
||||
for i, opt := range opts {
|
||||
args := strings.Split(opt, config.ArgumentsSeparator)
|
||||
args := strings.Split(opt, f.config.ArgumentsSeparator)
|
||||
|
||||
if len(args) == 1 {
|
||||
urlStart = i
|
||||
|
@@ -16,12 +16,14 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||
"github.com/imgproxy/imgproxy/v3/options"
|
||||
"github.com/imgproxy/imgproxy/v3/security"
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
)
|
||||
|
||||
type ProcessingTestSuite struct {
|
||||
suite.Suite
|
||||
idf *imagedata.Factory
|
||||
pof *options.Factory
|
||||
}
|
||||
|
||||
func (s *ProcessingTestSuite) SetupSuite() {
|
||||
@@ -41,6 +43,18 @@ func (s *ProcessingTestSuite) SetupSuite() {
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.idf = imagedata.NewFactory(f)
|
||||
|
||||
scfg, err := security.LoadConfigFromEnv(nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
security, err := security.New(scfg)
|
||||
s.Require().NoError(err)
|
||||
|
||||
cfg, err := options.LoadConfigFromEnv(nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.pof, err = options.NewFactory(cfg, security)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (s *ProcessingTestSuite) openFile(name string) imagedata.ImageData {
|
||||
@@ -63,7 +77,7 @@ func (s *ProcessingTestSuite) checkSize(r *Result, width, height int) {
|
||||
func (s *ProcessingTestSuite) TestResizeToFit() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := options.NewProcessingOptions()
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFit
|
||||
|
||||
testCases := []struct {
|
||||
@@ -101,7 +115,7 @@ func (s *ProcessingTestSuite) TestResizeToFit() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := options.NewProcessingOptions()
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFit
|
||||
po.Enlarge = true
|
||||
|
||||
@@ -140,7 +154,7 @@ func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFitExtend() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := options.NewProcessingOptions()
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFit
|
||||
po.Extend = options.ExtendOptions{
|
||||
Enabled: true,
|
||||
@@ -184,7 +198,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtend() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := options.NewProcessingOptions()
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFit
|
||||
po.ExtendAspectRatio = options.ExtendOptions{
|
||||
Enabled: true,
|
||||
@@ -228,7 +242,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFill() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := options.NewProcessingOptions()
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFill
|
||||
|
||||
testCases := []struct {
|
||||
@@ -266,7 +280,7 @@ func (s *ProcessingTestSuite) TestResizeToFill() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := options.NewProcessingOptions()
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFill
|
||||
po.Enlarge = true
|
||||
|
||||
@@ -305,7 +319,7 @@ func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFillExtend() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := options.NewProcessingOptions()
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFill
|
||||
po.Extend = options.ExtendOptions{
|
||||
Enabled: true,
|
||||
@@ -351,7 +365,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtend() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := options.NewProcessingOptions()
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFill
|
||||
po.ExtendAspectRatio = options.ExtendOptions{
|
||||
Enabled: true,
|
||||
@@ -397,7 +411,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFillDown() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := options.NewProcessingOptions()
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFillDown
|
||||
|
||||
testCases := []struct {
|
||||
@@ -435,7 +449,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDown() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := options.NewProcessingOptions()
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFillDown
|
||||
po.Enlarge = true
|
||||
|
||||
@@ -474,7 +488,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := options.NewProcessingOptions()
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFillDown
|
||||
po.Extend = options.ExtendOptions{
|
||||
Enabled: true,
|
||||
@@ -520,7 +534,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
|
||||
func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := options.NewProcessingOptions()
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFillDown
|
||||
po.ExtendAspectRatio = options.ExtendOptions{
|
||||
Enabled: true,
|
||||
@@ -564,7 +578,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
|
||||
func (s *ProcessingTestSuite) TestResultSizeLimit() {
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
||||
po := options.NewProcessingOptions()
|
||||
po := s.pof.NewProcessingOptions()
|
||||
|
||||
testCases := []struct {
|
||||
limit int
|
||||
@@ -992,7 +1006,7 @@ func (s *ProcessingTestSuite) TestResultSizeLimit() {
|
||||
}
|
||||
|
||||
func (s *ProcessingTestSuite) TestImageResolutionTooLarge() {
|
||||
po := options.NewProcessingOptions()
|
||||
po := s.pof.NewProcessingOptions()
|
||||
po.SecurityOptions.MaxSrcResolution = 1
|
||||
|
||||
imgdata := s.openFile("test2.jpg")
|
||||
|
@@ -22,20 +22,22 @@ 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, po *options.ProcessingOptions, imgWidth, imgHeight int, offsetScale float64, framesCount int) error {
|
||||
if err := wm.Load(wmData, 1, 1.0, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
po := options.NewProcessingOptions()
|
||||
po.ResizingType = options.ResizeFit
|
||||
po.Dpr = 1
|
||||
po.Enlarge = true
|
||||
po.Format = wmData.Format()
|
||||
opts := po.Watermark
|
||||
|
||||
wmPo := po.Default()
|
||||
wmPo.ResizingType = options.ResizeFit
|
||||
wmPo.Dpr = 1
|
||||
wmPo.Enlarge = true
|
||||
wmPo.Format = wmData.Format()
|
||||
|
||||
if opts.Scale > 0 {
|
||||
po.Width = max(imath.ScaleToEven(imgWidth, opts.Scale), 1)
|
||||
po.Height = max(imath.ScaleToEven(imgHeight, opts.Scale), 1)
|
||||
wmPo.Width = max(imath.ScaleToEven(imgWidth, opts.Scale), 1)
|
||||
wmPo.Height = max(imath.ScaleToEven(imgHeight, opts.Scale), 1)
|
||||
}
|
||||
|
||||
if opts.ShouldReplicate() {
|
||||
@@ -53,14 +55,14 @@ func prepareWatermark(wm *vips.Image, wmData imagedata.ImageData, opts *options.
|
||||
offY = imath.ScaleToEven(imgHeight, opts.Position.Y)
|
||||
}
|
||||
|
||||
po.Padding.Enabled = true
|
||||
po.Padding.Left = offX / 2
|
||||
po.Padding.Right = offX - po.Padding.Left
|
||||
po.Padding.Top = offY / 2
|
||||
po.Padding.Bottom = offY - po.Padding.Top
|
||||
wmPo.Padding.Enabled = true
|
||||
wmPo.Padding.Left = offX / 2
|
||||
wmPo.Padding.Right = offX - wmPo.Padding.Left
|
||||
wmPo.Padding.Top = offY / 2
|
||||
wmPo.Padding.Bottom = offY - wmPo.Padding.Top
|
||||
}
|
||||
|
||||
if err := watermarkPipeline.Run(context.Background(), wm, po, wmData, nil); err != nil {
|
||||
if err := watermarkPipeline.Run(context.Background(), wm, wmPo, wmData, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -110,7 +112,7 @@ func applyWatermark(
|
||||
height := img.Height()
|
||||
frameHeight := height / framesCount
|
||||
|
||||
if err := prepareWatermark(wm, wmData, &opts, width, frameHeight, offsetScale, framesCount); err != nil {
|
||||
if err := prepareWatermark(wm, wmData, po, width, frameHeight, offsetScale, framesCount); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
104
testutil/equal_but_not_same.go
Normal file
104
testutil/equal_but_not_same.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// EqualButNotSame asserts that expected and actual objects are not the same.
|
||||
// It recursively checks all fields to ensure that no pointers are shared.
|
||||
// If a pointer, slice or map are nil in either object, the test fails.
|
||||
func EqualButNotSame(t *testing.T, expected, actual any) {
|
||||
t.Helper()
|
||||
|
||||
expectedVal := reflect.ValueOf(expected)
|
||||
actualVal := reflect.ValueOf(actual)
|
||||
|
||||
deepEqual(t, expectedVal, actualVal, "")
|
||||
}
|
||||
|
||||
// deepEqual recursively verifies that all values are equal but pointers are different
|
||||
// except for the Expires field which is explicitly allowed to be shared
|
||||
func deepEqual(t *testing.T, left, right reflect.Value, fieldPath string) {
|
||||
require.True(t, left.IsValid() && right.IsValid(), "invalid value at %s", fieldPath)
|
||||
require.Equal(t, left.Type(), right.Type(), "types are not equal at %s", fieldPath)
|
||||
|
||||
switch left.Kind() {
|
||||
case reflect.Ptr:
|
||||
// Pointers should not be nil and must point to different objects
|
||||
require.False(t, left.IsNil(), "nil pointer at %s (left)", fieldPath)
|
||||
require.False(t, right.IsNil(), "nil pointer at %s (right)", fieldPath)
|
||||
require.NotSame(t, left.Interface(), right.Interface(), "shared pointer at %s", fieldPath)
|
||||
|
||||
deepEqual(t, left.Elem(), right.Elem(), fieldPath)
|
||||
|
||||
case reflect.Slice:
|
||||
// Slices should contain some elements and must not share the same underlying array
|
||||
require.Equal(t, left.Len(), right.Len(), "slice length mismatch at %s", fieldPath)
|
||||
require.NotEmpty(t, left.Len(), "slice must not be empty %s (left)", fieldPath)
|
||||
require.NotEmpty(t, right.Len(), "slice must not be empty %s (right)", fieldPath)
|
||||
require.NotEqual(t, left.Pointer(), right.Pointer(), "shared slices at %s", fieldPath)
|
||||
|
||||
// Recursively verify slice elements
|
||||
for i := 0; i < left.Len(); i++ {
|
||||
elemPath := buildPath(fieldPath, "[", anyToString(i), "]")
|
||||
deepEqual(t, left.Index(i), right.Index(i), elemPath)
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
// Maps should contain some elements and must not share the same underlying map
|
||||
require.Equal(t, left.Len(), right.Len(), "map length mismatch at %s", fieldPath)
|
||||
require.NotEmpty(t, left.Len(), "map must not be empty %s (left)", fieldPath)
|
||||
require.NotEmpty(t, right.Len(), "map must not be empty %s (right)", fieldPath)
|
||||
require.NotEqual(t, left.Pointer(), right.Pointer(), "shared maps at %s", fieldPath)
|
||||
|
||||
// Recursively verify map values
|
||||
for _, key := range left.MapKeys() {
|
||||
keyStr := anyToString(key.Interface())
|
||||
keyPath := buildPath(fieldPath, "[", keyStr, "]")
|
||||
originalMapVal := left.MapIndex(key)
|
||||
clonedMapVal := right.MapIndex(key)
|
||||
deepEqual(t, originalMapVal, clonedMapVal, keyPath)
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
require.Equal(t, left.Interface(), right.Interface(), "structs are not equal at %s", fieldPath)
|
||||
|
||||
// Fallback to recursive field-by-field comparison
|
||||
for i := 0; i < left.NumField(); i++ {
|
||||
field := left.Type().Field(i)
|
||||
if !field.IsExported() {
|
||||
continue // Skip unexported fields
|
||||
}
|
||||
|
||||
nestedPath := buildPath(fieldPath, ".", field.Name, "")
|
||||
originalFieldVal := left.Field(i)
|
||||
clonedFieldVal := right.Field(i)
|
||||
deepEqual(t, originalFieldVal, clonedFieldVal, nestedPath)
|
||||
}
|
||||
|
||||
default:
|
||||
// For primitive types, just verify equality
|
||||
require.Equal(t, left.Interface(), right.Interface(), "values not equal at %s", fieldPath)
|
||||
}
|
||||
}
|
||||
|
||||
// buildPath builds a field path for error messages
|
||||
func buildPath(basePath, separator, element, suffix string) string {
|
||||
if basePath == "" {
|
||||
return element + suffix
|
||||
}
|
||||
return basePath + separator + element + suffix
|
||||
}
|
||||
|
||||
// anyToString converts an any to a string for path building
|
||||
func anyToString(v any) string {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val
|
||||
default:
|
||||
return reflect.ValueOf(val).String()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user