mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-09-27 20:17:20 +02:00
Introduced instance
This commit is contained in:
96
.air.toml
96
.air.toml
@@ -3,61 +3,61 @@ testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ."
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = [
|
||||
"vips/vips.c",
|
||||
"vips/vips.h",
|
||||
"vips/source.c",
|
||||
"vips/source.h",
|
||||
"vips/icoload.c",
|
||||
"vips/icosave.c",
|
||||
"vips/ico.h",
|
||||
"vips/bmpload.c",
|
||||
"vips/bmpload.h",
|
||||
"vips/bmp.h"
|
||||
]
|
||||
kill_delay = "4s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = []
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = true
|
||||
stop_on_error = true
|
||||
args_bin = []
|
||||
bin = "./tmp/main"
|
||||
cmd = "make build -- -o ./tmp/main"
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = [
|
||||
"vips/vips.c",
|
||||
"vips/vips.h",
|
||||
"vips/source.c",
|
||||
"vips/source.h",
|
||||
"vips/icoload.c",
|
||||
"vips/icosave.c",
|
||||
"vips/ico.h",
|
||||
"vips/bmpload.c",
|
||||
"vips/bmpload.h",
|
||||
"vips/bmp.h",
|
||||
]
|
||||
kill_delay = "4s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = []
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = true
|
||||
stop_on_error = true
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
silent = false
|
||||
time = false
|
||||
main_only = false
|
||||
silent = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
clean_on_exit = false
|
||||
|
||||
[proxy]
|
||||
app_port = 0
|
||||
enabled = false
|
||||
proxy_port = 0
|
||||
app_port = 0
|
||||
enabled = false
|
||||
proxy_port = 0
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
||||
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Mark git workspace as safe
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
- name: Test
|
||||
run: go test -tags integration ./...
|
||||
run: go test ./...
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
66
Makefile
Normal file
66
Makefile
Normal file
@@ -0,0 +1,66 @@
|
||||
# imgproxy Makefile
|
||||
|
||||
BINARY := imgproxy
|
||||
|
||||
GOCMD := go
|
||||
GOBUILD := $(GOCMD) build
|
||||
GOCLEAN := $(GOCMD) clean
|
||||
GOTEST := $(GOCMD) test
|
||||
GOFMT := gofmt
|
||||
GOLINT := golangci-lint
|
||||
GOTESTSUM := gotestsum
|
||||
|
||||
SRCDIR := ./cli
|
||||
|
||||
# Default target
|
||||
.PHONY: all
|
||||
all: build
|
||||
|
||||
# Build the binary. If -o is not provided, it defaults to $(BINARY).
|
||||
#
|
||||
# Usage:
|
||||
# make build -- -o output_name
|
||||
.PHONY: build
|
||||
build:
|
||||
@args="$(filter-out $@,$(MAKECMDGOALS))"; \
|
||||
if echo "$$args" | grep -q "\-o"; then \
|
||||
$(GOBUILD) $$args $(SRCDIR); \
|
||||
else \
|
||||
$(GOBUILD) -o $(BINARY) $$args $(SRCDIR); \
|
||||
fi
|
||||
|
||||
# Clean
|
||||
.PHONY: clean
|
||||
clean:
|
||||
$(GOCLEAN)
|
||||
rm -f $(BINARY)
|
||||
|
||||
# Run tests
|
||||
#
|
||||
# Usage:
|
||||
# make test -- -run FooTest
|
||||
.PHONY: test
|
||||
test:
|
||||
@$(GOTEST) ./... $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
# Run gotestsum
|
||||
#
|
||||
# Usage:
|
||||
# make testsum -- -run FooTest
|
||||
testsum:
|
||||
@$(GOTESTSUM) -- $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
# Format code
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
$(GOFMT) -s -w .
|
||||
|
||||
# Lint code (requires golangci-lint installed)
|
||||
.PHONY: lint
|
||||
lint:
|
||||
$(GOLINT) run ./...
|
||||
|
||||
# Make any unknown target do nothing to avoid "up to date" messages
|
||||
.PHONY: FORCE
|
||||
%: FORCE
|
||||
@:
|
@@ -21,7 +21,12 @@ func (s *staticProvider) Get(_ context.Context, po *options.ProcessingOptions) (
|
||||
}
|
||||
|
||||
// NewStaticFromTriple creates a new ImageProvider from either a base64 string, file path, or URL
|
||||
func NewStaticProvider(ctx context.Context, c *StaticConfig, desc string, idf *imagedata.Factory) (Provider, error) {
|
||||
func NewStaticProvider(
|
||||
ctx context.Context,
|
||||
c *StaticConfig,
|
||||
desc string,
|
||||
idf *imagedata.Factory,
|
||||
) (Provider, error) {
|
||||
var (
|
||||
data imagedata.ImageData
|
||||
headers = make(http.Header)
|
||||
|
@@ -10,9 +10,11 @@ import (
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/config/configurators"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func healthcheck() int {
|
||||
// healthcheck performs a healthcheck on a running imgproxy instance
|
||||
func healthcheck(ctx context.Context, c *cli.Command) error {
|
||||
network := config.Network
|
||||
bind := config.Bind
|
||||
pathprefix := config.PathPrefix
|
||||
@@ -32,7 +34,7 @@ func healthcheck() int {
|
||||
res, err := httpc.Get(fmt.Sprintf("http://imgproxy%s/health", pathprefix))
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
return 1
|
||||
return cli.Exit(err, 1)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
@@ -40,8 +42,9 @@ func healthcheck() int {
|
||||
fmt.Fprintln(os.Stderr, string(msg))
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return 1
|
||||
err := fmt.Errorf("healthcheck failed: %s", msg)
|
||||
return cli.Exit(err, 1)
|
||||
}
|
||||
|
||||
return 0
|
||||
return nil
|
||||
}
|
86
cli/main.go
Normal file
86
cli/main.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3"
|
||||
"github.com/imgproxy/imgproxy/v3/version"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// ver prints the imgproxy version and runs the main application
|
||||
func ver(ctx context.Context, c *cli.Command) error {
|
||||
fmt.Println(version.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// run starts the imgproxy server
|
||||
func run(ctx context.Context, cmd *cli.Command) error {
|
||||
// NOTE: for now, these flags are loaded in config.go package
|
||||
|
||||
// keypath := cmd.String("keypath")
|
||||
// saltpath := cmd.String("saltpath")
|
||||
// presets := cmd.String("presets")
|
||||
|
||||
if err := imgproxy.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer imgproxy.Shutdown()
|
||||
|
||||
cfg, err := imgproxy.LoadConfigFromEnv(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
instance, err := imgproxy.New(ctx, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := instance.StartServer(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
cmd := &cli.Command{
|
||||
Name: "imgproxy",
|
||||
Usage: "Fast and secure standalone server for resizing and converting remote images",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "keypath",
|
||||
Usage: "path of the file with hex-encoded key",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "saltpath",
|
||||
Usage: "path of the file with hex-encoded salt",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "presets",
|
||||
Usage: "path of the file with presets",
|
||||
},
|
||||
},
|
||||
Action: run,
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "version",
|
||||
Usage: "print the version",
|
||||
Action: ver,
|
||||
},
|
||||
{
|
||||
Name: "health",
|
||||
Usage: "perform a healthcheck on a running imgproxy instance",
|
||||
Action: healthcheck,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := cmd.Run(context.Background(), os.Args); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
86
config.go
Normal file
86
config.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package imgproxy
|
||||
|
||||
import (
|
||||
"github.com/imgproxy/imgproxy/v3/auximageprovider"
|
||||
"github.com/imgproxy/imgproxy/v3/ensure"
|
||||
"github.com/imgproxy/imgproxy/v3/fetcher"
|
||||
processinghandler "github.com/imgproxy/imgproxy/v3/handlers/processing"
|
||||
"github.com/imgproxy/imgproxy/v3/handlers/stream"
|
||||
"github.com/imgproxy/imgproxy/v3/headerwriter"
|
||||
"github.com/imgproxy/imgproxy/v3/semaphores"
|
||||
"github.com/imgproxy/imgproxy/v3/server"
|
||||
"github.com/imgproxy/imgproxy/v3/transport"
|
||||
)
|
||||
|
||||
// Config represents an instance configuration
|
||||
type Config struct {
|
||||
HeaderWriter headerwriter.Config
|
||||
Semaphores semaphores.Config
|
||||
FallbackImage auximageprovider.StaticConfig
|
||||
WatermarkImage auximageprovider.StaticConfig
|
||||
Transport transport.Config
|
||||
Fetcher fetcher.Config
|
||||
ProcessingHandler processinghandler.Config
|
||||
StreamHandler stream.Config
|
||||
Server server.Config
|
||||
}
|
||||
|
||||
// NewDefaultConfig creates a new default configuration
|
||||
func NewDefaultConfig() Config {
|
||||
return Config{
|
||||
HeaderWriter: headerwriter.NewDefaultConfig(),
|
||||
Semaphores: semaphores.NewDefaultConfig(),
|
||||
FallbackImage: auximageprovider.NewDefaultStaticConfig(),
|
||||
WatermarkImage: auximageprovider.NewDefaultStaticConfig(),
|
||||
Transport: transport.NewDefaultConfig(),
|
||||
Fetcher: fetcher.NewDefaultConfig(),
|
||||
ProcessingHandler: processinghandler.NewDefaultConfig(),
|
||||
StreamHandler: stream.NewDefaultConfig(),
|
||||
Server: server.NewDefaultConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadConfigFromEnv loads configuration from environment variables
|
||||
func LoadConfigFromEnv(c *Config) (*Config, error) {
|
||||
c = ensure.Ensure(c, NewDefaultConfig)
|
||||
|
||||
var err error
|
||||
|
||||
if _, err = server.LoadConfigFromEnv(&c.Server); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err = auximageprovider.LoadFallbackStaticConfigFromEnv(&c.FallbackImage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err = auximageprovider.LoadWatermarkStaticConfigFromEnv(&c.WatermarkImage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err = headerwriter.LoadConfigFromEnv(&c.HeaderWriter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err = semaphores.LoadConfigFromEnv(&c.Semaphores); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err = transport.LoadConfigFromEnv(&c.Transport); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err = fetcher.LoadConfigFromEnv(&c.Fetcher); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err = processinghandler.LoadConfigFromEnv(&c.ProcessingHandler); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err = stream.LoadConfigFromEnv(&c.StreamHandler); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
@@ -17,6 +17,14 @@ import (
|
||||
"github.com/imgproxy/imgproxy/v3/version"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// We need to reset config to defaults once on app start.
|
||||
// Tests could perform config.Reset() to ensure a clean state where required.
|
||||
// NOTE: this is temporary workaround until we finally move
|
||||
// to Config objects everywhere
|
||||
Reset()
|
||||
}
|
||||
|
||||
type URLReplacement = configurators.URLReplacement
|
||||
|
||||
var (
|
||||
@@ -425,8 +433,6 @@ func Reset() {
|
||||
}
|
||||
|
||||
func Configure() error {
|
||||
Reset()
|
||||
|
||||
if port := os.Getenv("PORT"); len(port) > 0 {
|
||||
Bind = fmt.Sprintf(":%s", port)
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ FROM ghcr.io/imgproxy/imgproxy-base:${BASE_IMAGE_VERSION} AS build
|
||||
ENV CGO_ENABLED=1
|
||||
|
||||
COPY . .
|
||||
RUN bash -c 'go build -v -ldflags "-s -w" -o /opt/imgproxy/bin/imgproxy'
|
||||
RUN bash -c 'go build -v -ldflags "-s -w" -o /opt/imgproxy/bin/imgproxy ./cli'
|
||||
|
||||
# Remove unnecessary files
|
||||
RUN rm -rf /opt/imgproxy/lib/pkgconfig /opt/imgproxy/lib/cmake
|
||||
|
@@ -13,8 +13,10 @@ import (
|
||||
type Config struct {
|
||||
// UserAgent is the User-Agent header to use when fetching images.
|
||||
UserAgent string
|
||||
|
||||
// DownloadTimeout is the timeout for downloading an image, in seconds.
|
||||
DownloadTimeout time.Duration
|
||||
|
||||
// MaxRedirects is the maximum number of redirects to follow when fetching an image.
|
||||
MaxRedirects int
|
||||
}
|
||||
|
3
go.mod
3
go.mod
@@ -119,6 +119,7 @@ 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
|
||||
@@ -174,6 +175,7 @@ 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
|
||||
@@ -182,6 +184,7 @@ 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,6 +44,7 @@ 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=
|
||||
@@ -172,6 +173,8 @@ 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=
|
||||
@@ -394,6 +397,8 @@ 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=
|
||||
@@ -441,6 +446,10 @@ 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=
|
||||
github.com/vmihailenco/msgpack/v4 v4.3.13/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
|
||||
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
|
||||
|
@@ -26,7 +26,7 @@ type Handler struct {
|
||||
semaphores *semaphores.Semaphores
|
||||
fallbackImage auximageprovider.Provider
|
||||
watermarkImage auximageprovider.Provider
|
||||
imageData *imagedata.Factory
|
||||
idf *imagedata.Factory
|
||||
}
|
||||
|
||||
// New creates new handler object
|
||||
@@ -50,7 +50,7 @@ func New(
|
||||
semaphores: semaphores,
|
||||
fallbackImage: fi,
|
||||
watermarkImage: wi,
|
||||
imageData: idf,
|
||||
idf: idf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func (h *Handler) Execute(
|
||||
monitoringMeta: mm,
|
||||
semaphores: h.semaphores,
|
||||
hwr: h.hw.NewRequest(),
|
||||
idf: h.imageData,
|
||||
idf: h.idf,
|
||||
}
|
||||
|
||||
return req.execute(ctx)
|
||||
|
1
handlers/processing/handler_test.go
Normal file
1
handlers/processing/handler_test.go
Normal file
@@ -0,0 +1 @@
|
||||
package processing
|
@@ -154,7 +154,7 @@ func (r *request) getFallbackImage(
|
||||
// processImage calls actual image processing
|
||||
func (r *request) processImage(ctx context.Context, originData imagedata.ImageData) (*processing.Result, error) {
|
||||
defer monitoring.StartProcessingSegment(ctx, r.monitoringMeta.Filter(monitoring.MetaProcessingOptions))()
|
||||
return processing.ProcessImage(ctx, originData, r.po, r.handler.watermarkImage, r.handler.imageData)
|
||||
return processing.ProcessImage(ctx, originData, r.po, r.handler.watermarkImage, r.handler.idf)
|
||||
}
|
||||
|
||||
// writeDebugHeaders writes debug headers (X-Origin-*, X-Result-*) to the response
|
||||
|
170
imgproxy.go
Normal file
170
imgproxy.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package imgproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/auximageprovider"
|
||||
"github.com/imgproxy/imgproxy/v3/fetcher"
|
||||
"github.com/imgproxy/imgproxy/v3/handlers"
|
||||
processinghandler "github.com/imgproxy/imgproxy/v3/handlers/processing"
|
||||
"github.com/imgproxy/imgproxy/v3/handlers/stream"
|
||||
"github.com/imgproxy/imgproxy/v3/headerwriter"
|
||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||
"github.com/imgproxy/imgproxy/v3/memory"
|
||||
"github.com/imgproxy/imgproxy/v3/monitoring/prometheus"
|
||||
"github.com/imgproxy/imgproxy/v3/semaphores"
|
||||
"github.com/imgproxy/imgproxy/v3/server"
|
||||
"github.com/imgproxy/imgproxy/v3/transport"
|
||||
)
|
||||
|
||||
const (
|
||||
faviconPath = "/favicon.ico"
|
||||
healthPath = "/health"
|
||||
)
|
||||
|
||||
// Imgproxy holds all the components needed for imgproxy to function
|
||||
type Imgproxy struct {
|
||||
HeaderWriter *headerwriter.Writer
|
||||
Semaphores *semaphores.Semaphores
|
||||
FallbackImage auximageprovider.Provider
|
||||
WatermarkImage auximageprovider.Provider
|
||||
Fetcher *fetcher.Fetcher
|
||||
ProcessingHandler *processinghandler.Handler
|
||||
StreamHandler *stream.Handler
|
||||
ImageDataFactory *imagedata.Factory
|
||||
Config *Config
|
||||
}
|
||||
|
||||
// New creates a new imgproxy instance
|
||||
func New(ctx context.Context, config *Config) (*Imgproxy, error) {
|
||||
headerWriter, err := headerwriter.New(&config.HeaderWriter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ts, err := transport.New(&config.Transport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fetcher, err := fetcher.New(ts, &config.Fetcher)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idf := imagedata.NewFactory(fetcher)
|
||||
|
||||
fallbackImage, err := auximageprovider.NewStaticProvider(ctx, &config.FallbackImage, "fallback", idf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
watermarkImage, err := auximageprovider.NewStaticProvider(ctx, &config.WatermarkImage, "watermark", idf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
semaphores, err := semaphores.New(&config.Semaphores)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
streamHandler, err := stream.New(&config.StreamHandler, headerWriter, fetcher)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ph, err := processinghandler.New(
|
||||
streamHandler, headerWriter, semaphores, fallbackImage, watermarkImage, idf, &config.ProcessingHandler,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Imgproxy{
|
||||
HeaderWriter: headerWriter,
|
||||
Semaphores: semaphores,
|
||||
FallbackImage: fallbackImage,
|
||||
WatermarkImage: watermarkImage,
|
||||
Fetcher: fetcher,
|
||||
StreamHandler: streamHandler,
|
||||
ProcessingHandler: ph,
|
||||
ImageDataFactory: idf,
|
||||
Config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BuildRouter sets up the HTTP routes and middleware
|
||||
func (i *Imgproxy) BuildRouter() (*server.Router, error) {
|
||||
r, err := server.NewRouter(&i.Config.Server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.GET("/", handlers.LandingHandler)
|
||||
r.GET("", handlers.LandingHandler)
|
||||
|
||||
r.GET(faviconPath, r.NotFoundHandler).Silent()
|
||||
r.GET(healthPath, handlers.HealthHandler).Silent()
|
||||
if i.Config.Server.HealthCheckPath != "" {
|
||||
r.GET(i.Config.Server.HealthCheckPath, handlers.HealthHandler).Silent()
|
||||
}
|
||||
|
||||
r.GET(
|
||||
"/*", i.ProcessingHandler.Execute,
|
||||
r.WithSecret, r.WithCORS, r.WithPanic, r.WithReportError, r.WithMonitoring,
|
||||
)
|
||||
|
||||
r.HEAD("/*", r.OkHandler, r.WithCORS)
|
||||
r.OPTIONS("/*", r.OkHandler, r.WithCORS)
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Start runs the imgproxy server. This function blocks until the context is cancelled.
|
||||
func (i *Imgproxy) StartServer(ctx context.Context) error {
|
||||
go i.startMemoryTicker(ctx)
|
||||
|
||||
ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
if err := prometheus.StartServer(cancel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
router, err := i.BuildRouter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := server.Start(cancel, router)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Shutdown(context.Background())
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// startMemoryTicker starts a ticker that periodically frees memory and optionally logs memory stats
|
||||
func (i *Imgproxy) startMemoryTicker(ctx context.Context) {
|
||||
ticker := time.NewTicker(i.Config.Server.FreeMemoryInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
memory.Free()
|
||||
|
||||
if i.Config.Server.LogMemStats {
|
||||
memory.LogStats()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
74
init.go
Normal file
74
init.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// init_once.go contains global initialization/teardown functions that should be called exactly once
|
||||
// per process.
|
||||
package imgproxy
|
||||
|
||||
import (
|
||||
"github.com/DataDog/datadog-agent/pkg/trace/log"
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/config/loadenv"
|
||||
"github.com/imgproxy/imgproxy/v3/errorreport"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Init performs the global resources initialization. This should be done once per process.
|
||||
func Init() error {
|
||||
if err := loadenv.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := logger.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// NOTE: This is temporary workaround. We have to load env vars in config.go before
|
||||
// actually configuring ImgProxy instance because for now we use it as a source of truth.
|
||||
// Will be removed once we move env var loading to imgproxy.go
|
||||
if err := config.Configure(); err != nil {
|
||||
return err
|
||||
}
|
||||
// NOTE: End of temporary workaround.
|
||||
|
||||
gliblog.Init()
|
||||
|
||||
maxprocs.Set(maxprocs.Logger(log.Debugf))
|
||||
|
||||
if err := monitoring.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := vips.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errorreport.Init()
|
||||
|
||||
if err := processing.ValidatePreferredFormats(); err != nil {
|
||||
vips.Shutdown()
|
||||
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
|
||||
}
|
||||
|
||||
// Shutdown performs global cleanup
|
||||
func Shutdown() {
|
||||
monitoring.Stop()
|
||||
errorreport.Close()
|
||||
vips.Shutdown()
|
||||
}
|
@@ -1,12 +1,12 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -14,28 +14,50 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/corona10/goimagehash"
|
||||
"github.com/imgproxy/imgproxy/v3"
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||
"github.com/imgproxy/imgproxy/v3/testutil"
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
const (
|
||||
similarityThreshold = 5 // Distance between images to be considered similar
|
||||
)
|
||||
|
||||
type LoadTestSuite struct {
|
||||
suite.Suite
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
testData *testutil.TestDataProvider
|
||||
testImagesPath string
|
||||
}
|
||||
|
||||
// SetupSuite starts imgproxy instance server
|
||||
func (s *LoadTestSuite) SetupSuite() {
|
||||
s.testData = testutil.NewTestDataProvider(s.T())
|
||||
s.testImagesPath = s.testData.Path("test-images")
|
||||
s.ctx, s.cancel = context.WithCancel(s.T().Context())
|
||||
|
||||
s.startImgproxy(s.ctx)
|
||||
}
|
||||
|
||||
// TearDownSuite stops imgproxy instance server
|
||||
func (s *LoadTestSuite) TearDownSuite() {
|
||||
s.cancel()
|
||||
}
|
||||
|
||||
// testLoadFolder fetches images iterates over images in the specified folder,
|
||||
// runs imgproxy on each image, and compares the result with the reference image
|
||||
// which is expected to be in the `integration` folder with the same name
|
||||
// but with `.png` extension.
|
||||
func testLoadFolder(t *testing.T, cs, sourcePath, folder string) {
|
||||
t.Logf("Testing folder: %s", folder)
|
||||
|
||||
walkPath := path.Join(sourcePath, folder)
|
||||
func (s *LoadTestSuite) testLoadFolder(folder string) {
|
||||
walkPath := path.Join(s.testImagesPath, folder)
|
||||
|
||||
// Iterate over the files in the source folder
|
||||
err := filepath.Walk(walkPath, func(path string, info os.FileInfo, err error) error {
|
||||
require.NoError(t, err)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Skip directories
|
||||
if info.IsDir() {
|
||||
@@ -49,39 +71,79 @@ func testLoadFolder(t *testing.T, cs, sourcePath, folder string) {
|
||||
referencePath := strings.TrimSuffix(basePath, filepath.Ext(basePath)) + ".png"
|
||||
|
||||
// Construct the full path to the reference image (integration/ folder)
|
||||
referencePath = filepath.Join(sourcePath, "integration", folder, referencePath)
|
||||
referencePath = filepath.Join(s.testImagesPath, "integration", folder, referencePath)
|
||||
|
||||
// Construct the source URL for imgproxy (no processing)
|
||||
sourceUrl := fmt.Sprintf("insecure/plain/local:///%s/%s@png", folder, basePath)
|
||||
|
||||
imgproxyImageBytes := fetchImage(t, cs, sourceUrl)
|
||||
imgproxyImageBytes := s.fetchImage(sourceUrl)
|
||||
imgproxyImage, err := png.Decode(bytes.NewReader(imgproxyImageBytes))
|
||||
require.NoError(t, err, "Failed to decode PNG image from imgproxy for %s", basePath)
|
||||
s.Require().NoError(err, "Failed to decode PNG image from imgproxy for %s", basePath)
|
||||
|
||||
referenceFile, err := os.Open(referencePath)
|
||||
require.NoError(t, err)
|
||||
s.Require().NoError(err)
|
||||
defer referenceFile.Close()
|
||||
|
||||
referenceImage, err := png.Decode(referenceFile)
|
||||
require.NoError(t, err, "Failed to decode PNG reference image for %s", referencePath)
|
||||
s.Require().NoError(err, "Failed to decode PNG reference image for %s", referencePath)
|
||||
|
||||
hash1, err := goimagehash.DifferenceHash(imgproxyImage)
|
||||
require.NoError(t, err)
|
||||
s.Require().NoError(err)
|
||||
|
||||
hash2, err := goimagehash.DifferenceHash(referenceImage)
|
||||
require.NoError(t, err)
|
||||
s.Require().NoError(err)
|
||||
|
||||
distance, err := hash1.Distance(hash2)
|
||||
require.NoError(t, err)
|
||||
s.Require().NoError(err)
|
||||
|
||||
assert.LessOrEqual(t, distance, similarityThreshold,
|
||||
s.Require().LessOrEqual(distance, similarityThreshold,
|
||||
"Image %s differs from reference image %s by %d, which is greater than the allowed threshold of %d",
|
||||
basePath, referencePath, distance, similarityThreshold)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
// fetchImage fetches an image from the imgproxy server
|
||||
func (s *LoadTestSuite) fetchImage(path string) []byte {
|
||||
url := fmt.Sprintf("http://%s:%d/%s", bindHost, bindPort, path)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
s.Require().NoError(err, "Failed to fetch image from %s", url)
|
||||
defer resp.Body.Close()
|
||||
|
||||
s.Require().Equal(http.StatusOK, resp.StatusCode, "Expected status code 200 OK, got %d, url: %s", resp.StatusCode, url)
|
||||
|
||||
bytes, err := io.ReadAll(resp.Body)
|
||||
s.Require().NoError(err, "Failed to read response body from %s", url)
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
func (s *LoadTestSuite) startImgproxy(ctx context.Context) *imgproxy.Imgproxy {
|
||||
c, err := imgproxy.LoadConfigFromEnv(nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
c.Server.Bind = ":" + fmt.Sprintf("%d", bindPort)
|
||||
c.Transport.Local.Root = s.testImagesPath
|
||||
c.Server.LogMemStats = true
|
||||
|
||||
config.MaxAnimationFrames = 999
|
||||
config.DevelopmentErrorsMode = true
|
||||
|
||||
i, err := imgproxy.New(ctx, c)
|
||||
s.Require().NoError(err)
|
||||
|
||||
go func() {
|
||||
err = i.StartServer(ctx)
|
||||
if err != nil {
|
||||
s.T().Errorf("Imgproxy server exited with error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// TestLoadSaveToPng ensures that our load pipeline works,
|
||||
@@ -89,53 +151,34 @@ func testLoadFolder(t *testing.T, cs, sourcePath, folder string) {
|
||||
// in the folder, it does the passthrough request through imgproxy:
|
||||
// no processing, just convert format of the source file to png.
|
||||
// Then, it compares the result with the reference image.
|
||||
func TestLoadSaveToPng(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// TODO: Will be moved to test suite (like in processing_test.go)
|
||||
// Since we use SupportsLoad, we need to initialize vips
|
||||
defer vips.Shutdown() // either way it needs to be deinitialized
|
||||
err := vips.Init()
|
||||
require.NoError(t, err, "Failed to initialize vips")
|
||||
|
||||
path, err := testImagesPath(t)
|
||||
require.NoError(t, err)
|
||||
|
||||
cs := startImgproxy(t, ctx, path)
|
||||
|
||||
if vips.SupportsLoad(imagetype.GIF) {
|
||||
testLoadFolder(t, cs, path, "gif")
|
||||
func (s *LoadTestSuite) TestLoadSaveToPng() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
imageType imagetype.Type
|
||||
folderName string
|
||||
}{
|
||||
{"GIF", imagetype.GIF, "gif"},
|
||||
{"JPEG", imagetype.JPEG, "jpg"},
|
||||
{"HEIC", imagetype.HEIC, "heif"},
|
||||
{"JXL", imagetype.JXL, "jxl"},
|
||||
{"SVG", imagetype.SVG, "svg"},
|
||||
{"TIFF", imagetype.TIFF, "tiff"},
|
||||
{"WEBP", imagetype.WEBP, "webp"},
|
||||
{"BMP", imagetype.BMP, "bmp"},
|
||||
{"ICO", imagetype.ICO, "ico"},
|
||||
}
|
||||
|
||||
if vips.SupportsLoad(imagetype.JPEG) {
|
||||
testLoadFolder(t, cs, path, "jpg")
|
||||
}
|
||||
|
||||
if vips.SupportsLoad(imagetype.HEIC) {
|
||||
testLoadFolder(t, cs, path, "heif")
|
||||
}
|
||||
|
||||
if vips.SupportsLoad(imagetype.JXL) {
|
||||
testLoadFolder(t, cs, path, "jxl")
|
||||
}
|
||||
|
||||
if vips.SupportsLoad(imagetype.SVG) {
|
||||
testLoadFolder(t, cs, path, "svg")
|
||||
}
|
||||
|
||||
if vips.SupportsLoad(imagetype.TIFF) {
|
||||
testLoadFolder(t, cs, path, "tiff")
|
||||
}
|
||||
|
||||
if vips.SupportsLoad(imagetype.WEBP) {
|
||||
testLoadFolder(t, cs, path, "webp")
|
||||
}
|
||||
|
||||
if vips.SupportsLoad(imagetype.BMP) {
|
||||
testLoadFolder(t, cs, path, "bmp")
|
||||
}
|
||||
|
||||
if vips.SupportsLoad(imagetype.ICO) {
|
||||
testLoadFolder(t, cs, path, "ico")
|
||||
for _, tc := range testCases {
|
||||
s.T().Run(tc.name, func(t *testing.T) {
|
||||
if vips.SupportsLoad(tc.imageType) {
|
||||
s.testLoadFolder(tc.folderName)
|
||||
} else {
|
||||
t.Skipf("%s format not supported by VIPS", tc.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegration(t *testing.T) {
|
||||
suite.Run(t, new(LoadTestSuite))
|
||||
}
|
||||
|
20
integration/main_test.go
Normal file
20
integration/main_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
bindPort = 9090 // Port to bind imgproxy to
|
||||
bindHost = "localhost"
|
||||
)
|
||||
|
||||
// TestMain performs global setup/teardown for the integration tests.
|
||||
func TestMain(m *testing.M) {
|
||||
imgproxy.Init()
|
||||
os.Exit(m.Run())
|
||||
imgproxy.Shutdown()
|
||||
}
|
@@ -1,9 +1,4 @@
|
||||
package main
|
||||
|
||||
// NOTE: this test is the integration test for the processing handler. We can't extract and
|
||||
// move it to handlers package yet because it depends on the global routes, methods and
|
||||
// initialization functions. Once those would we wrapped into structures, we'll be able to move this test
|
||||
// to where it belongs.
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -11,70 +6,84 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3"
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/config/configurators"
|
||||
"github.com/imgproxy/imgproxy/v3/fetcher"
|
||||
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||
"github.com/imgproxy/imgproxy/v3/server"
|
||||
"github.com/imgproxy/imgproxy/v3/svg"
|
||||
"github.com/imgproxy/imgproxy/v3/testutil"
|
||||
"github.com/imgproxy/imgproxy/v3/transport"
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// ProcessingHandlerTestSuite is a test suite for testing image processing handler
|
||||
type ProcessingHandlerTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
router *server.Router
|
||||
testData *testutil.TestDataProvider
|
||||
config testutil.LazyObj[*imgproxy.Config]
|
||||
router testutil.LazyObj[*server.Router]
|
||||
imgproxy testutil.LazyObj[*imgproxy.Imgproxy]
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) SetupSuite() {
|
||||
config.Reset()
|
||||
|
||||
wd, err := os.Getwd()
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.T().Setenv("IMGPROXY_LOCAL_FILESYSTEM_ROOT", filepath.Join(wd, "/testdata"))
|
||||
s.T().Setenv("IMGPROXY_CLIENT_KEEP_ALIVE_TIMEOUT", "0")
|
||||
|
||||
err = initialize()
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Silence all the logs
|
||||
logrus.SetOutput(io.Discard)
|
||||
|
||||
cfg := server.NewDefaultConfig()
|
||||
r, err := server.NewRouter(&cfg)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router = buildRouter(r)
|
||||
// Initialize test data provider (local test files)
|
||||
s.testData = testutil.NewTestDataProvider(s.T())
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TeardownSuite() {
|
||||
shutdown()
|
||||
logrus.SetOutput(os.Stdout)
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) SetupTest() {
|
||||
wd, err := os.Getwd()
|
||||
s.Require().NoError(err)
|
||||
// setupObjs initializes lazy objects
|
||||
func (s *ProcessingHandlerTestSuite) setupObjs() {
|
||||
s.config = testutil.NewLazyObj(s.T(), func() (*imgproxy.Config, error) {
|
||||
c, err := imgproxy.LoadConfigFromEnv(nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
config.Reset()
|
||||
config.AllowLoopbackSourceAddresses = true
|
||||
config.LocalFileSystemRoot = filepath.Join(wd, "/testdata")
|
||||
config.ClientKeepAliveTimeout = 0
|
||||
c.Transport.Local.Root = s.testData.Root()
|
||||
c.Transport.HTTP.ClientKeepAliveTimeout = 0
|
||||
|
||||
return c, nil
|
||||
})
|
||||
|
||||
s.imgproxy = testutil.NewLazyObj(s.T(), func() (*imgproxy.Imgproxy, error) {
|
||||
return imgproxy.New(s.T().Context(), s.config())
|
||||
})
|
||||
|
||||
s.router = testutil.NewLazyObj(s.T(), func() (*server.Router, error) {
|
||||
return s.imgproxy().BuildRouter()
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) send(path string, header ...http.Header) *httptest.ResponseRecorder {
|
||||
func (s *ProcessingHandlerTestSuite) SetupTest() {
|
||||
config.Reset() // We reset config only at the start of each test
|
||||
|
||||
// NOTE: This must be moved to security config
|
||||
config.AllowLoopbackSourceAddresses = true
|
||||
// NOTE: end note
|
||||
|
||||
s.setupObjs()
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) SetupSubTest() {
|
||||
// We use t.Run() a lot, so we need to reset lazy objects at the beginning of each subtest
|
||||
s.setupObjs()
|
||||
}
|
||||
|
||||
// GET performs a GET request to the given path and returns the response recorder
|
||||
func (s *ProcessingHandlerTestSuite) GET(path string, header ...http.Header) *http.Response {
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
@@ -82,84 +91,43 @@ func (s *ProcessingHandlerTestSuite) send(path string, header ...http.Header) *h
|
||||
req.Header = header[0]
|
||||
}
|
||||
|
||||
s.router.ServeHTTP(rw, req)
|
||||
s.router().ServeHTTP(rw, req)
|
||||
|
||||
return rw
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) readTestFile(name string) []byte {
|
||||
wd, err := os.Getwd()
|
||||
s.Require().NoError(err)
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(wd, "testdata", name))
|
||||
s.Require().NoError(err)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) readTestImageData(name string) imagedata.ImageData {
|
||||
wd, err := os.Getwd()
|
||||
s.Require().NoError(err)
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(wd, "testdata", name))
|
||||
s.Require().NoError(err)
|
||||
|
||||
// NOTE: Temporary workaround. We create imagedata.Factory here
|
||||
// because currently configuration is changed via env vars
|
||||
// or config. We need to pick up those config changes.
|
||||
// This will be addressed in the next PR
|
||||
trc, err := transport.LoadConfigFromEnv(nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
tr, err := transport.New(trc)
|
||||
s.Require().NoError(err)
|
||||
|
||||
fc, err := fetcher.LoadConfigFromEnv(nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
f, err := fetcher.New(tr, fc)
|
||||
s.Require().NoError(err)
|
||||
|
||||
idf := imagedata.NewFactory(f)
|
||||
// end of workaround
|
||||
|
||||
imgdata, err := idf.NewFromBytes(data)
|
||||
s.Require().NoError(err)
|
||||
|
||||
return imgdata
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestRequest() {
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||
res := rw.Result()
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
s.Require().Equal("image/png", res.Header.Get("Content-Type"))
|
||||
|
||||
format, err := imagetype.Detect(res.Body)
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(imagetype.PNG, format)
|
||||
return rw.Result()
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestSignatureValidationFailure() {
|
||||
config.Keys = [][]byte{[]byte("test-key")}
|
||||
config.Salts = [][]byte{[]byte("test-salt")}
|
||||
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||
res := rw.Result()
|
||||
tt := []struct {
|
||||
name string
|
||||
url string
|
||||
statusCode int
|
||||
}{
|
||||
{
|
||||
name: "NoSignature",
|
||||
url: "/unsafe/rs:fill:4:4/plain/local:///test1.png",
|
||||
statusCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "BadSignature",
|
||||
url: "/bad-signature/rs:fill:4:4/plain/local:///test1.png",
|
||||
statusCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "ValidSignature",
|
||||
url: "/My9d3xq_PYpVHsPrCyww0Kh1w5KZeZhIlWhsa4az1TI/rs:fill:4:4/plain/local:///test1.png",
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
s.Require().Equal(403, res.StatusCode)
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestSignatureValidationSuccess() {
|
||||
config.Keys = [][]byte{[]byte("test-key")}
|
||||
config.Salts = [][]byte{[]byte("test-salt")}
|
||||
|
||||
rw := s.send("/My9d3xq_PYpVHsPrCyww0Kh1w5KZeZhIlWhsa4az1TI/rs:fill:4:4/plain/local:///test1.png")
|
||||
res := rw.Result()
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
for _, tc := range tt {
|
||||
s.Run(tc.name, func() {
|
||||
res := s.GET(tc.url)
|
||||
s.Require().Equal(tc.statusCode, res.StatusCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestSourceValidation() {
|
||||
@@ -176,19 +144,16 @@ func (s *ProcessingHandlerTestSuite) TestSourceValidation() {
|
||||
name: "match http URL without wildcard",
|
||||
allowedSources: []string{"local://", "http://images.dev/"},
|
||||
requestPath: "/unsafe/plain/http://images.dev/lorem/ipsum.jpg",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "match http URL with wildcard in hostname single level",
|
||||
allowedSources: []string{"local://", "http://*.mycdn.dev/"},
|
||||
requestPath: "/unsafe/plain/http://a-1.mycdn.dev/lorem/ipsum.jpg",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "match http URL with wildcard in hostname multiple levels",
|
||||
allowedSources: []string{"local://", "http://*.mycdn.dev/"},
|
||||
requestPath: "/unsafe/plain/http://a-1.b-2.mycdn.dev/lorem/ipsum.jpg",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "no match s3 URL with allowed local and http URLs",
|
||||
@@ -206,26 +171,24 @@ func (s *ProcessingHandlerTestSuite) TestSourceValidation() {
|
||||
|
||||
for _, tc := range tt {
|
||||
s.Run(tc.name, func() {
|
||||
exps := make([]*regexp.Regexp, len(tc.allowedSources))
|
||||
config.AllowedSources = make([]*regexp.Regexp, len(tc.allowedSources))
|
||||
for i, pattern := range tc.allowedSources {
|
||||
exps[i] = configurators.RegexpFromPattern(pattern)
|
||||
config.AllowedSources[i] = configurators.RegexpFromPattern(pattern)
|
||||
}
|
||||
config.AllowedSources = exps
|
||||
|
||||
rw := s.send(tc.requestPath)
|
||||
res := rw.Result()
|
||||
res := s.GET(tc.requestPath)
|
||||
|
||||
if tc.expectedError {
|
||||
s.Require().Equal(404, res.StatusCode)
|
||||
s.Require().Equal(http.StatusNotFound, res.StatusCode)
|
||||
} else {
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestSourceNetworkValidation() {
|
||||
data := s.readTestFile("test1.png")
|
||||
data := s.testData.Read("test1.png")
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(200)
|
||||
@@ -233,180 +196,158 @@ func (s *ProcessingHandlerTestSuite) TestSourceNetworkValidation() {
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
var rw *httptest.ResponseRecorder
|
||||
url := fmt.Sprintf("/unsafe/rs:fill:4:4/plain/%s/test1.png", server.URL)
|
||||
|
||||
u := fmt.Sprintf("/unsafe/rs:fill:4:4/plain/%s/test1.png", server.URL)
|
||||
// We wrap this in a subtest to reset s.router()
|
||||
s.Run("AllowLoopbackSourceAddressesTrue", func() {
|
||||
config.AllowLoopbackSourceAddresses = true
|
||||
res := s.GET(url)
|
||||
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||
})
|
||||
|
||||
rw = s.send(u)
|
||||
s.Require().Equal(200, rw.Result().StatusCode)
|
||||
|
||||
config.AllowLoopbackSourceAddresses = false
|
||||
rw = s.send(u)
|
||||
s.Require().Equal(404, rw.Result().StatusCode)
|
||||
s.Run("AllowLoopbackSourceAddressesFalse", func() {
|
||||
config.AllowLoopbackSourceAddresses = false
|
||||
res := s.GET(url)
|
||||
s.Require().Equal(http.StatusNotFound, res.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestSourceFormatNotSupported() {
|
||||
vips.DisableLoadSupport(imagetype.PNG)
|
||||
defer vips.ResetLoadSupport()
|
||||
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||
res := rw.Result()
|
||||
|
||||
s.Require().Equal(422, res.StatusCode)
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||
s.Require().Equal(http.StatusUnprocessableEntity, res.StatusCode)
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestResultingFormatNotSupported() {
|
||||
vips.DisableSaveSupport(imagetype.PNG)
|
||||
defer vips.ResetSaveSupport()
|
||||
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
|
||||
res := rw.Result()
|
||||
|
||||
s.Require().Equal(422, res.StatusCode)
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
|
||||
s.Require().Equal(http.StatusUnprocessableEntity, res.StatusCode)
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingConfig() {
|
||||
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
||||
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
|
||||
expected := s.readTestImageData("test1.png")
|
||||
|
||||
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
||||
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||
s.Require().True(s.testData.FileEqualsToReader("test1.png", res.Body))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingPO() {
|
||||
rw := s.send("/unsafe/rs:fill:4:4/skp:png/plain/local:///test1.png")
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/rs:fill:4:4/skp:png/plain/local:///test1.png")
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
|
||||
expected := s.readTestImageData("test1.png")
|
||||
|
||||
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
||||
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||
s.Require().True(s.testData.FileEqualsToReader("test1.png", res.Body))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
|
||||
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
||||
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
|
||||
expected := s.readTestImageData("test1.png")
|
||||
|
||||
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
||||
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||
s.Require().True(s.testData.FileEqualsToReader("test1.png", res.Body))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingDifferentFormat() {
|
||||
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
||||
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@jpg")
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@jpg")
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
|
||||
expected := s.readTestImageData("test1.png")
|
||||
|
||||
s.Require().False(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
||||
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||
s.Require().False(s.testData.FileEqualsToReader("test1.png", res.Body))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingSVG() {
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.svg")
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.svg")
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||
|
||||
expected, err := svg.Sanitize(s.readTestImageData("test1.svg"))
|
||||
data, err := s.imgproxy().ImageDataFactory.NewFromBytes(s.testData.Read("test1.svg"))
|
||||
s.Require().NoError(err)
|
||||
|
||||
expected, err := svg.Sanitize(data)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestNotSkipProcessingSVGToJPG() {
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.svg@jpg")
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.svg@jpg")
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
|
||||
expected := s.readTestImageData("test1.svg")
|
||||
|
||||
s.Require().False(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
||||
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||
s.Require().False(s.testData.FileEqualsToReader("test1.svg", res.Body))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestErrorSavingToSVG() {
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@svg")
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@svg")
|
||||
|
||||
s.Require().Equal(422, res.StatusCode)
|
||||
s.Require().Equal(http.StatusUnprocessableEntity, res.StatusCode)
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughCacheControl() {
|
||||
config.CacheControlPassthrough = true
|
||||
s.config().HeaderWriter.CacheControlPassthrough = true
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Cache-Control", "max-age=1234, public")
|
||||
rw.Header().Set("Expires", time.Now().Add(time.Hour).UTC().Format(http.TimeFormat))
|
||||
rw.Header().Set(httpheaders.CacheControl, "max-age=1234, public")
|
||||
rw.Header().Set(httpheaders.Expires, time.Now().Add(time.Hour).UTC().Format(http.TimeFormat))
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(s.readTestFile("test1.png"))
|
||||
rw.Write(s.testData.Read("test1.png"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||
|
||||
s.Require().Equal("max-age=1234, public", res.Header.Get("Cache-Control"))
|
||||
s.Require().Empty(res.Header.Get("Expires"))
|
||||
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||
s.Require().Equal("max-age=1234, public", res.Header.Get(httpheaders.CacheControl))
|
||||
s.Require().Empty(res.Header.Get(httpheaders.Expires))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughExpires() {
|
||||
config.CacheControlPassthrough = true
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Expires", time.Now().Add(1239*time.Second).UTC().Format(http.TimeFormat))
|
||||
rw.Header().Set(httpheaders.Expires, time.Now().Add(1239*time.Second).UTC().Format(http.TimeFormat))
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(s.readTestFile("test1.png"))
|
||||
rw.Write(s.testData.Read("test1.png"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||
|
||||
// Use regex to allow some delay
|
||||
s.Require().Regexp("max-age=123[0-9], public", res.Header.Get("Cache-Control"))
|
||||
s.Require().Empty(res.Header.Get("Expires"))
|
||||
s.Require().Regexp("max-age=123[0-9], public", res.Header.Get(httpheaders.CacheControl))
|
||||
s.Require().Empty(res.Header.Get(httpheaders.Expires))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughDisabled() {
|
||||
config.CacheControlPassthrough = false
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Cache-Control", "max-age=1234, public")
|
||||
rw.Header().Set("Expires", time.Now().Add(time.Hour).UTC().Format(http.TimeFormat))
|
||||
rw.Header().Set(httpheaders.CacheControl, "max-age=1234, public")
|
||||
rw.Header().Set(httpheaders.Expires, time.Now().Add(time.Hour).UTC().Format(http.TimeFormat))
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(s.readTestFile("test1.png"))
|
||||
rw.Write(s.testData.Read("test1.png"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||
|
||||
s.Require().NotEqual("max-age=1234, public", res.Header.Get("Cache-Control"))
|
||||
s.Require().Empty(res.Header.Get("Expires"))
|
||||
s.Require().NotEqual("max-age=1234, public", res.Header.Get(httpheaders.CacheControl))
|
||||
s.Require().Empty(res.Header.Get(httpheaders.Expires))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestETagDisabled() {
|
||||
config.ETagEnabled = false
|
||||
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
s.Require().Empty(res.Header.Get("ETag"))
|
||||
s.Require().Empty(res.Header.Get(httpheaders.Etag))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
|
||||
@@ -425,8 +366,7 @@ func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
|
||||
header := make(http.Header)
|
||||
header.Set(httpheaders.IfNoneMatch, etag)
|
||||
|
||||
rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||
res := rw.Result()
|
||||
res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||
|
||||
s.Require().Equal(304, res.StatusCode)
|
||||
s.Require().Equal(etag, res.Header.Get(httpheaders.Etag))
|
||||
@@ -435,39 +375,37 @@ func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
|
||||
func (s *ProcessingHandlerTestSuite) TestLastModifiedEnabled() {
|
||||
config.LastModifiedEnabled = true
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT")
|
||||
rw.Header().Set(httpheaders.LastModified, "Wed, 21 Oct 2015 07:28:00 GMT")
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(s.readTestFile("test1.png"))
|
||||
rw.Write(s.testData.Read("test1.png"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||
|
||||
s.Require().Equal("Wed, 21 Oct 2015 07:28:00 GMT", res.Header.Get("Last-Modified"))
|
||||
s.Require().Equal("Wed, 21 Oct 2015 07:28:00 GMT", res.Header.Get(httpheaders.LastModified))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestLastModifiedDisabled() {
|
||||
config.LastModifiedEnabled = false
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT")
|
||||
rw.Header().Set(httpheaders.LastModified, "Wed, 21 Oct 2015 07:28:00 GMT")
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(s.readTestFile("test1.png"))
|
||||
rw.Write(s.testData.Read("test1.png"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||
|
||||
s.Require().Empty(res.Header.Get("Last-Modified"))
|
||||
s.Require().Empty(res.Header.Get(httpheaders.LastModified))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqExactMatchLastModifiedDisabled() {
|
||||
config.LastModifiedEnabled = false
|
||||
data := s.readTestFile("test1.png")
|
||||
data := s.testData.Read("test1.png")
|
||||
lastModified := "Wed, 21 Oct 2015 07:28:00 GMT"
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
modifiedSince := r.Header.Get("If-Modified-Since")
|
||||
modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
|
||||
s.Empty(modifiedSince)
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(data)
|
||||
@@ -475,9 +413,8 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqExactMatchLastModifiedD
|
||||
defer ts.Close()
|
||||
|
||||
header := make(http.Header)
|
||||
header.Set("If-Modified-Since", lastModified)
|
||||
rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||
res := rw.Result()
|
||||
header.Set(httpheaders.IfModifiedSince, lastModified)
|
||||
res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
}
|
||||
@@ -486,25 +423,24 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqExactMatchLastModifiedE
|
||||
config.LastModifiedEnabled = true
|
||||
lastModified := "Wed, 21 Oct 2015 07:28:00 GMT"
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
modifiedSince := r.Header.Get("If-Modified-Since")
|
||||
modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
|
||||
s.Equal(lastModified, modifiedSince)
|
||||
rw.WriteHeader(304)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
header := make(http.Header)
|
||||
header.Set("If-Modified-Since", lastModified)
|
||||
rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||
res := rw.Result()
|
||||
header.Set(httpheaders.IfModifiedSince, lastModified)
|
||||
res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||
|
||||
s.Require().Equal(304, res.StatusCode)
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareMoreRecentLastModifiedDisabled() {
|
||||
data := s.readTestFile("test1.png")
|
||||
data := s.testData.Read("test1.png")
|
||||
config.LastModifiedEnabled = false
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
modifiedSince := r.Header.Get("If-Modified-Since")
|
||||
modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
|
||||
s.Empty(modifiedSince)
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(data)
|
||||
@@ -514,10 +450,9 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareMoreRecentLastMo
|
||||
recentTimestamp := "Thu, 25 Feb 2021 01:45:00 GMT"
|
||||
|
||||
header := make(http.Header)
|
||||
header.Set("If-Modified-Since", recentTimestamp)
|
||||
rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||
res := rw.Result()
|
||||
header.Set(httpheaders.IfModifiedSince, recentTimestamp)
|
||||
|
||||
res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
}
|
||||
|
||||
@@ -525,7 +460,7 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareMoreRecentLastMo
|
||||
config.LastModifiedEnabled = true
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
fileLastModified, _ := time.Parse(http.TimeFormat, "Wed, 21 Oct 2015 07:28:00 GMT")
|
||||
modifiedSince := r.Header.Get("If-Modified-Since")
|
||||
modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
|
||||
parsedModifiedSince, err := time.Parse(http.TimeFormat, modifiedSince)
|
||||
s.NoError(err)
|
||||
s.True(fileLastModified.Before(parsedModifiedSince))
|
||||
@@ -536,18 +471,17 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareMoreRecentLastMo
|
||||
recentTimestamp := "Thu, 25 Feb 2021 01:45:00 GMT"
|
||||
|
||||
header := make(http.Header)
|
||||
header.Set("If-Modified-Since", recentTimestamp)
|
||||
rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||
res := rw.Result()
|
||||
header.Set(httpheaders.IfModifiedSince, recentTimestamp)
|
||||
res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||
|
||||
s.Require().Equal(304, res.StatusCode)
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifiedDisabled() {
|
||||
config.LastModifiedEnabled = false
|
||||
data := s.readTestFile("test1.png")
|
||||
s.config().ProcessingHandler.LastModifiedEnabled = false
|
||||
data := s.testData.Read("test1.png")
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
modifiedSince := r.Header.Get("If-Modified-Since")
|
||||
modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
|
||||
s.Empty(modifiedSince)
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(data)
|
||||
@@ -557,19 +491,18 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifi
|
||||
oldTimestamp := "Tue, 01 Oct 2013 17:31:00 GMT"
|
||||
|
||||
header := make(http.Header)
|
||||
header.Set("If-Modified-Since", oldTimestamp)
|
||||
rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||
res := rw.Result()
|
||||
header.Set(httpheaders.IfModifiedSince, oldTimestamp)
|
||||
res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifiedEnabled() {
|
||||
config.LastModifiedEnabled = true
|
||||
data := s.readTestFile("test1.png")
|
||||
data := s.testData.Read("test1.png")
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
fileLastModified, _ := time.Parse(http.TimeFormat, "Wed, 21 Oct 2015 07:28:00 GMT")
|
||||
modifiedSince := r.Header.Get("If-Modified-Since")
|
||||
modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
|
||||
parsedModifiedSince, err := time.Parse(http.TimeFormat, modifiedSince)
|
||||
s.NoError(err)
|
||||
s.True(fileLastModified.After(parsedModifiedSince))
|
||||
@@ -581,9 +514,8 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifi
|
||||
oldTimestamp := "Tue, 01 Oct 2013 17:31:00 GMT"
|
||||
|
||||
header := make(http.Header)
|
||||
header.Set("If-Modified-Since", oldTimestamp)
|
||||
rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||
res := rw.Result()
|
||||
header.Set(httpheaders.IfModifiedSince, oldTimestamp)
|
||||
res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
}
|
||||
@@ -591,43 +523,40 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifi
|
||||
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvg() {
|
||||
config.AlwaysRasterizeSvg = true
|
||||
|
||||
rw := s.send("/unsafe/rs:fill:40:40/plain/local:///test1.svg")
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/rs:fill:40:40/plain/local:///test1.svg")
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
s.Require().Equal("image/png", res.Header.Get("Content-Type"))
|
||||
s.Require().Equal("image/png", res.Header.Get(httpheaders.ContentType))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithEnforceAvif() {
|
||||
config.AlwaysRasterizeSvg = true
|
||||
config.EnforceWebp = true
|
||||
|
||||
rw := s.send("/unsafe/plain/local:///test1.svg", http.Header{"Accept": []string{"image/webp"}})
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/plain/local:///test1.svg", http.Header{"Accept": []string{"image/webp"}})
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
s.Require().Equal("image/webp", res.Header.Get("Content-Type"))
|
||||
s.Require().Equal("image/webp", res.Header.Get(httpheaders.ContentType))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgDisabled() {
|
||||
config.AlwaysRasterizeSvg = false
|
||||
config.EnforceWebp = true
|
||||
|
||||
rw := s.send("/unsafe/plain/local:///test1.svg")
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/plain/local:///test1.svg")
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
s.Require().Equal("image/svg+xml", res.Header.Get("Content-Type"))
|
||||
s.Require().Equal("image/svg+xml", res.Header.Get(httpheaders.ContentType))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithFormat() {
|
||||
config.AlwaysRasterizeSvg = true
|
||||
config.SkipProcessingFormats = []imagetype.Type{imagetype.SVG}
|
||||
rw := s.send("/unsafe/plain/local:///test1.svg@svg")
|
||||
res := rw.Result()
|
||||
|
||||
res := s.GET("/unsafe/plain/local:///test1.svg@svg")
|
||||
|
||||
s.Require().Equal(200, res.StatusCode)
|
||||
s.Require().Equal("image/svg+xml", res.Header.Get("Content-Type"))
|
||||
s.Require().Equal("image/svg+xml", res.Header.Get(httpheaders.ContentType))
|
||||
}
|
||||
|
||||
func (s *ProcessingHandlerTestSuite) TestMaxSrcFileSizeGlobal() {
|
||||
@@ -635,12 +564,11 @@ func (s *ProcessingHandlerTestSuite) TestMaxSrcFileSizeGlobal() {
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(s.readTestFile("test1.png"))
|
||||
rw.Write(s.testData.Read("test1.png"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||
res := rw.Result()
|
||||
res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||
|
||||
s.Require().Equal(422, res.StatusCode)
|
||||
}
|
@@ -1,137 +0,0 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
// Integration test helpers for imgproxy.
|
||||
// We use regular `go build` instead of Docker to make sure
|
||||
// tests run in the same environment as other tests,
|
||||
// including in CI, where everything runs in a custom Docker image
|
||||
// against the different libvips versions.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
buildContext = ".." // Source code folder
|
||||
binPath = "/tmp/imgproxy-test" // Path to the built imgproxy binary
|
||||
bindPort = 9090 // Port to bind imgproxy to
|
||||
bindHost = "127.0.0.1" // Host to bind imgproxy to
|
||||
)
|
||||
|
||||
var (
|
||||
buildCmd = []string{"build", "-v", "-ldflags=-s -w", "-o", binPath} // imgproxy build command
|
||||
)
|
||||
|
||||
// waitForPort tries to connect to host:port until successful or timeout
|
||||
func waitForPort(host string, port int, timeout time.Duration) error {
|
||||
var address string
|
||||
if net.ParseIP(host) != nil && net.ParseIP(host).To4() == nil {
|
||||
// IPv6 address, wrap in brackets
|
||||
address = fmt.Sprintf("[%s]:%d", host, port)
|
||||
} else {
|
||||
address = fmt.Sprintf("%s:%d", host, port)
|
||||
}
|
||||
deadline := time.Now().Add(timeout)
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
conn, err := net.DialTimeout("tcp", address, 500*time.Millisecond)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
return nil // port is open
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
return fmt.Errorf("timeout waiting for port %s", address)
|
||||
}
|
||||
|
||||
func startImgproxy(t *testing.T, ctx context.Context, testImagesPath string) string {
|
||||
// Build the imgproxy binary
|
||||
buildCmd := exec.Command("go", buildCmd...)
|
||||
buildCmd.Dir = buildContext
|
||||
buildCmd.Env = os.Environ()
|
||||
buildOut, err := buildCmd.CombinedOutput()
|
||||
require.NoError(t, err, "failed to build imgproxy: %v\n%s", err, string(buildOut))
|
||||
|
||||
// Start imgproxy in the background
|
||||
cmd := exec.CommandContext(ctx, binPath)
|
||||
|
||||
// Set environment variables for imgproxy
|
||||
cmd.Env = append(os.Environ(), "IMGPROXY_BIND=:"+fmt.Sprintf("%d", bindPort))
|
||||
cmd.Env = append(cmd.Env, "IMGPROXY_LOCAL_FILESYSTEM_ROOT="+testImagesPath)
|
||||
cmd.Env = append(cmd.Env, "IMGPROXY_MAX_ANIMATION_FRAMES=999")
|
||||
cmd.Env = append(cmd.Env, "IMGPROXY_VIPS_LEAK_CHECK=true")
|
||||
cmd.Env = append(cmd.Env, "IMGPROXY_LOG_MEM_STATS=true")
|
||||
cmd.Env = append(cmd.Env, "IMGPROXY_DEVELOPMENT_ERRORS_MODE=true")
|
||||
|
||||
// That one is for the build logs
|
||||
stdout, _ := os.CreateTemp("", "imgproxy-stdout-*")
|
||||
stderr, _ := os.CreateTemp("", "imgproxy-stderr-*")
|
||||
cmd.Stdout = stdout
|
||||
cmd.Stderr = stderr
|
||||
|
||||
err = cmd.Start()
|
||||
require.NoError(t, err, "failed to start imgproxy: %v", err)
|
||||
|
||||
// Wait for port 8090 to be available
|
||||
err = waitForPort(bindHost, bindPort, 5*time.Second)
|
||||
if err != nil {
|
||||
cmd.Process.Kill()
|
||||
require.NoError(t, err, "imgproxy did not start in time")
|
||||
}
|
||||
|
||||
// Return a dummy container (nil) and connection string
|
||||
t.Cleanup(func() {
|
||||
cmd.Process.Kill()
|
||||
stdout.Close()
|
||||
stderr.Close()
|
||||
os.Remove(stdout.Name())
|
||||
os.Remove(stderr.Name())
|
||||
os.Remove(binPath)
|
||||
})
|
||||
|
||||
return fmt.Sprintf("%s:%d", bindHost, bindPort)
|
||||
}
|
||||
|
||||
// fetchImage fetches an image from the imgproxy server
|
||||
func fetchImage(t *testing.T, cs string, path string) []byte {
|
||||
url := fmt.Sprintf("http://%s/%s", cs, path)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
require.NoError(t, err, "Failed to fetch image from %s", url)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, "Expected status code 200 OK, got %d, url: %s", resp.StatusCode, url)
|
||||
|
||||
bytes, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err, "Failed to read response body from %s", url)
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
// testImagesPath returns the absolute path to the test images directory
|
||||
func testImagesPath(t *testing.T) (string, error) {
|
||||
// Get current working directory
|
||||
dir, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Convert to absolute path (if it's not already)
|
||||
absPath, err := filepath.Abs(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
return path.Join(absPath, "../testdata/test-images"), nil
|
||||
}
|
276
main.go
276
main.go
@@ -1,276 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"go.uber.org/automaxprocs/maxprocs"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/auximageprovider"
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/config/loadenv"
|
||||
"github.com/imgproxy/imgproxy/v3/errorreport"
|
||||
"github.com/imgproxy/imgproxy/v3/fetcher"
|
||||
"github.com/imgproxy/imgproxy/v3/gliblog"
|
||||
"github.com/imgproxy/imgproxy/v3/handlers"
|
||||
processingHandler "github.com/imgproxy/imgproxy/v3/handlers/processing"
|
||||
"github.com/imgproxy/imgproxy/v3/handlers/stream"
|
||||
"github.com/imgproxy/imgproxy/v3/headerwriter"
|
||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||
"github.com/imgproxy/imgproxy/v3/logger"
|
||||
"github.com/imgproxy/imgproxy/v3/memory"
|
||||
"github.com/imgproxy/imgproxy/v3/monitoring"
|
||||
"github.com/imgproxy/imgproxy/v3/monitoring/prometheus"
|
||||
"github.com/imgproxy/imgproxy/v3/options"
|
||||
"github.com/imgproxy/imgproxy/v3/processing"
|
||||
"github.com/imgproxy/imgproxy/v3/semaphores"
|
||||
"github.com/imgproxy/imgproxy/v3/server"
|
||||
"github.com/imgproxy/imgproxy/v3/transport"
|
||||
"github.com/imgproxy/imgproxy/v3/version"
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
)
|
||||
|
||||
const (
|
||||
faviconPath = "/favicon.ico"
|
||||
healthPath = "/health"
|
||||
categoryConfig = "(tmp)config" // NOTE: temporary category for reporting configration errors
|
||||
)
|
||||
|
||||
func callHandleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) error {
|
||||
// NOTE: This is temporary, will be moved level up at once
|
||||
hwc, err := headerwriter.LoadConfigFromEnv(nil)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
hw, err := headerwriter.New(hwc)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
sc, err := stream.LoadConfigFromEnv(nil)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
tcfg, err := transport.LoadConfigFromEnv(nil)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
tr, err := transport.New(tcfg)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
fc, err := fetcher.LoadConfigFromEnv(nil)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
fetcher, err := fetcher.New(tr, fc)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
idf := imagedata.NewFactory(fetcher)
|
||||
|
||||
stream, err := stream.New(sc, hw, fetcher)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
phc, err := processingHandler.LoadConfigFromEnv(nil)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
semc, err := semaphores.LoadConfigFromEnv(nil)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
semaphores, err := semaphores.New(semc)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
fic, err := auximageprovider.LoadFallbackStaticConfigFromEnv(nil)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
fi, err := auximageprovider.NewStaticProvider(
|
||||
r.Context(),
|
||||
fic,
|
||||
"fallback image",
|
||||
idf,
|
||||
)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
wic, err := auximageprovider.LoadWatermarkStaticConfigFromEnv(nil)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
wi, err := auximageprovider.NewStaticProvider(
|
||||
r.Context(),
|
||||
wic,
|
||||
"watermark image",
|
||||
idf,
|
||||
)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
h, err := processingHandler.New(stream, hw, semaphores, fi, wi, idf, phc)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||
}
|
||||
|
||||
return h.Execute(reqID, rw, r)
|
||||
}
|
||||
|
||||
func buildRouter(r *server.Router) *server.Router {
|
||||
r.GET("/", handlers.LandingHandler)
|
||||
r.GET("", handlers.LandingHandler)
|
||||
|
||||
r.GET(faviconPath, r.NotFoundHandler).Silent()
|
||||
r.GET(healthPath, handlers.HealthHandler).Silent()
|
||||
if config.HealthCheckPath != "" {
|
||||
r.GET(config.HealthCheckPath, handlers.HealthHandler).Silent()
|
||||
}
|
||||
|
||||
r.GET(
|
||||
"/*", callHandleProcessing,
|
||||
r.WithSecret, r.WithCORS, r.WithPanic, r.WithReportError, r.WithMonitoring,
|
||||
)
|
||||
|
||||
r.HEAD("/*", r.OkHandler, r.WithCORS)
|
||||
r.OPTIONS("/*", r.OkHandler, r.WithCORS)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func initialize() error {
|
||||
if err := loadenv.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := logger.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gliblog.Init()
|
||||
|
||||
maxprocs.Set(maxprocs.Logger(log.Debugf))
|
||||
|
||||
if err := config.Configure(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := monitoring.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errorreport.Init()
|
||||
|
||||
if err := vips.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := processing.ValidatePreferredFormats(); err != nil {
|
||||
vips.Shutdown()
|
||||
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
|
||||
}
|
||||
|
||||
func shutdown() {
|
||||
monitoring.Stop()
|
||||
errorreport.Close()
|
||||
vips.Shutdown()
|
||||
}
|
||||
|
||||
func run(ctx context.Context) error {
|
||||
if err := initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer shutdown()
|
||||
|
||||
go func() {
|
||||
var logMemStats = len(os.Getenv("IMGPROXY_LOG_MEM_STATS")) > 0
|
||||
|
||||
for range time.Tick(time.Duration(config.FreeMemoryInterval) * time.Second) {
|
||||
memory.Free()
|
||||
|
||||
if logMemStats {
|
||||
memory.LogStats()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
if err := prometheus.StartServer(cancel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := server.LoadConfigFromEnv(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r, err := server.NewRouter(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := server.Start(cancel, buildRouter(r))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Shutdown(context.Background())
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
switch flag.Arg(0) {
|
||||
case "health":
|
||||
os.Exit(healthcheck())
|
||||
case "version":
|
||||
fmt.Println(version.Version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if err := run(context.Background()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
2
pprof.go
2
pprof.go
@@ -1,7 +1,7 @@
|
||||
//go:build pprof
|
||||
// +build pprof
|
||||
|
||||
package main
|
||||
package imgproxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
@@ -25,6 +26,10 @@ type Config struct {
|
||||
DevelopmentErrorsMode bool // Enable development mode for detailed error messages
|
||||
SocketReusePort bool // Enable SO_REUSEPORT socket option
|
||||
HealthCheckPath string // Health check path from config
|
||||
|
||||
// TODO: We are not sure where to put it yet
|
||||
FreeMemoryInterval time.Duration // Interval for freeing memory
|
||||
LogMemStats bool // Log memory stats
|
||||
}
|
||||
|
||||
// NewDefaultConfig returns default config values
|
||||
@@ -43,6 +48,8 @@ func NewDefaultConfig() Config {
|
||||
DevelopmentErrorsMode: false,
|
||||
SocketReusePort: false,
|
||||
HealthCheckPath: "",
|
||||
FreeMemoryInterval: 10 * time.Second,
|
||||
LogMemStats: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +69,8 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
|
||||
c.DevelopmentErrorsMode = config.DevelopmentErrorsMode
|
||||
c.SocketReusePort = config.SoReuseport
|
||||
c.HealthCheckPath = config.HealthCheckPath
|
||||
c.FreeMemoryInterval = time.Duration(config.FreeMemoryInterval) * time.Second
|
||||
c.LogMemStats = len(os.Getenv("IMGPROXY_LOG_MEM_STATS")) > 0
|
||||
|
||||
return c, nil
|
||||
}
|
||||
@@ -92,5 +101,9 @@ func (c *Config) Validate() error {
|
||||
return fmt.Errorf("graceful timeout should be greater than or equal to 0, now - %d", c.GracefulTimeout)
|
||||
}
|
||||
|
||||
if c.FreeMemoryInterval <= 0 {
|
||||
return errors.New("free memory interval should be greater than zero")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -127,14 +127,16 @@ func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Set(httpheaders.XRequestID, reqID)
|
||||
|
||||
for _, rr := range r.routes {
|
||||
if rr.isMatch(req) {
|
||||
if !rr.silent {
|
||||
LogRequest(reqID, req)
|
||||
}
|
||||
|
||||
rr.handler(reqID, rw, req)
|
||||
return
|
||||
if !rr.isMatch(req) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !rr.silent {
|
||||
LogRequest(reqID, req)
|
||||
}
|
||||
|
||||
rr.handler(reqID, rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Means that we have not found matching route
|
||||
|
32
testutil/lazy_obj.go
Normal file
32
testutil/lazy_obj.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// LazyObj is a function that returns an object of type T.
|
||||
type LazyObj[T any] func() T
|
||||
|
||||
// LazyObjInit is a function that initializes and returns an object of type T and an error if any.
|
||||
type LazyObjInit[T any] func() (T, error)
|
||||
|
||||
// NewLazyObj creates a new LazyObj that initializes the object on the first call.
|
||||
func NewLazyObj[T any](t require.TestingT, init LazyObjInit[T]) LazyObj[T] {
|
||||
if h, ok := t.(interface{ Helper() }); ok {
|
||||
h.Helper()
|
||||
}
|
||||
|
||||
var obj *T
|
||||
|
||||
return func() T {
|
||||
if obj != nil {
|
||||
return *obj
|
||||
}
|
||||
|
||||
o, err := init()
|
||||
require.NoError(t, err)
|
||||
|
||||
obj = &o
|
||||
return o
|
||||
}
|
||||
}
|
99
testutil/test_data_provider.go
Normal file
99
testutil/test_data_provider.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
// TestDataFolderName is the name of the testdata directory
|
||||
TestDataFolderName = "testdata"
|
||||
)
|
||||
|
||||
// TestDataProvider provides access to test data images
|
||||
type TestDataProvider struct {
|
||||
path string
|
||||
t require.TestingT
|
||||
}
|
||||
|
||||
// New creates a new TestDataProvider
|
||||
func NewTestDataProvider(t require.TestingT) *TestDataProvider {
|
||||
if h, ok := t.(interface{ Helper() }); ok {
|
||||
h.Helper()
|
||||
}
|
||||
|
||||
path, err := findProjectRoot()
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return &TestDataProvider{
|
||||
path: filepath.Join(path, TestDataFolderName),
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
// findProjectRoot finds the absolute path to the project root by looking for go.mod
|
||||
func findProjectRoot() (string, error) {
|
||||
// Start from current working directory
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Walk up the directory tree looking for go.mod
|
||||
dir := wd
|
||||
for {
|
||||
goModPath := filepath.Join(dir, "go.mod")
|
||||
if _, err := os.Stat(goModPath); err == nil {
|
||||
// Found go.mod, this is our project root
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
// Reached filesystem root without finding go.mod
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
return "", os.ErrNotExist
|
||||
|
||||
}
|
||||
|
||||
// Root returns the absolute path to the testdata directory
|
||||
func (p *TestDataProvider) Root() string {
|
||||
return p.path
|
||||
}
|
||||
|
||||
// Path returns the absolute path to a file in the testdata directory
|
||||
func (p *TestDataProvider) Path(parts ...string) string {
|
||||
allParts := append([]string{p.path}, parts...)
|
||||
return filepath.Join(allParts...)
|
||||
}
|
||||
|
||||
// Read reads a test data file and returns it as bytes
|
||||
func (p *TestDataProvider) Read(name string) []byte {
|
||||
if h, ok := p.t.(interface{ Helper() }); ok {
|
||||
h.Helper()
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(p.Path(name))
|
||||
require.NoError(p.t, err)
|
||||
return data
|
||||
}
|
||||
|
||||
// Data reads a test data file and returns it as imagedata.ImageData
|
||||
func (p *TestDataProvider) Reader(name string) *bytes.Reader {
|
||||
return bytes.NewReader(p.Read(name))
|
||||
}
|
||||
|
||||
// FileEqualsToReader compares the contents of a test data file with the contents of the given reader
|
||||
func (p *TestDataProvider) FileEqualsToReader(name string, reader io.Reader) bool {
|
||||
expected := p.Reader(name)
|
||||
return ReadersEqual(p.t, expected, reader)
|
||||
}
|
Reference in New Issue
Block a user