mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-10-04 18:33:36 +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"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
args_bin = []
|
args_bin = []
|
||||||
bin = "./tmp/main"
|
bin = "./tmp/main"
|
||||||
cmd = "go build -o ./tmp/main ."
|
cmd = "make build -- -o ./tmp/main"
|
||||||
delay = 1000
|
delay = 1000
|
||||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||||
exclude_file = []
|
exclude_file = []
|
||||||
exclude_regex = ["_test.go"]
|
exclude_regex = ["_test.go"]
|
||||||
exclude_unchanged = false
|
exclude_unchanged = false
|
||||||
follow_symlink = false
|
follow_symlink = false
|
||||||
full_bin = ""
|
full_bin = ""
|
||||||
include_dir = []
|
include_dir = []
|
||||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
include_file = [
|
include_file = [
|
||||||
"vips/vips.c",
|
"vips/vips.c",
|
||||||
"vips/vips.h",
|
"vips/vips.h",
|
||||||
"vips/source.c",
|
"vips/source.c",
|
||||||
"vips/source.h",
|
"vips/source.h",
|
||||||
"vips/icoload.c",
|
"vips/icoload.c",
|
||||||
"vips/icosave.c",
|
"vips/icosave.c",
|
||||||
"vips/ico.h",
|
"vips/ico.h",
|
||||||
"vips/bmpload.c",
|
"vips/bmpload.c",
|
||||||
"vips/bmpload.h",
|
"vips/bmpload.h",
|
||||||
"vips/bmp.h"
|
"vips/bmp.h",
|
||||||
]
|
]
|
||||||
kill_delay = "4s"
|
kill_delay = "4s"
|
||||||
log = "build-errors.log"
|
log = "build-errors.log"
|
||||||
poll = false
|
poll = false
|
||||||
poll_interval = 0
|
poll_interval = 0
|
||||||
post_cmd = []
|
post_cmd = []
|
||||||
pre_cmd = []
|
pre_cmd = []
|
||||||
rerun = false
|
rerun = false
|
||||||
rerun_delay = 500
|
rerun_delay = 500
|
||||||
send_interrupt = true
|
send_interrupt = true
|
||||||
stop_on_error = true
|
stop_on_error = true
|
||||||
|
|
||||||
[color]
|
[color]
|
||||||
app = ""
|
app = ""
|
||||||
build = "yellow"
|
build = "yellow"
|
||||||
main = "magenta"
|
main = "magenta"
|
||||||
runner = "green"
|
runner = "green"
|
||||||
watcher = "cyan"
|
watcher = "cyan"
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
main_only = false
|
main_only = false
|
||||||
silent = false
|
silent = false
|
||||||
time = false
|
time = false
|
||||||
|
|
||||||
[misc]
|
[misc]
|
||||||
clean_on_exit = false
|
clean_on_exit = false
|
||||||
|
|
||||||
[proxy]
|
[proxy]
|
||||||
app_port = 0
|
app_port = 0
|
||||||
enabled = false
|
enabled = false
|
||||||
proxy_port = 0
|
proxy_port = 0
|
||||||
|
|
||||||
[screen]
|
[screen]
|
||||||
clear_on_rebuild = false
|
clear_on_rebuild = false
|
||||||
keep_scroll = true
|
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
|
- name: Mark git workspace as safe
|
||||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test -tags integration ./...
|
run: go test ./...
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
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
|
// 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 (
|
var (
|
||||||
data imagedata.ImageData
|
data imagedata.ImageData
|
||||||
headers = make(http.Header)
|
headers = make(http.Header)
|
||||||
|
@@ -10,9 +10,11 @@ import (
|
|||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/config"
|
"github.com/imgproxy/imgproxy/v3/config"
|
||||||
"github.com/imgproxy/imgproxy/v3/config/configurators"
|
"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
|
network := config.Network
|
||||||
bind := config.Bind
|
bind := config.Bind
|
||||||
pathprefix := config.PathPrefix
|
pathprefix := config.PathPrefix
|
||||||
@@ -32,7 +34,7 @@ func healthcheck() int {
|
|||||||
res, err := httpc.Get(fmt.Sprintf("http://imgproxy%s/health", pathprefix))
|
res, err := httpc.Get(fmt.Sprintf("http://imgproxy%s/health", pathprefix))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err.Error())
|
fmt.Fprintln(os.Stderr, err.Error())
|
||||||
return 1
|
return cli.Exit(err, 1)
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
@@ -40,8 +42,9 @@ func healthcheck() int {
|
|||||||
fmt.Fprintln(os.Stderr, string(msg))
|
fmt.Fprintln(os.Stderr, string(msg))
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
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"
|
"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
|
type URLReplacement = configurators.URLReplacement
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -423,8 +431,6 @@ func Reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Configure() error {
|
func Configure() error {
|
||||||
Reset()
|
|
||||||
|
|
||||||
if port := os.Getenv("PORT"); len(port) > 0 {
|
if port := os.Getenv("PORT"); len(port) > 0 {
|
||||||
Bind = fmt.Sprintf(":%s", port)
|
Bind = fmt.Sprintf(":%s", port)
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@ FROM ghcr.io/imgproxy/imgproxy-base:${BASE_IMAGE_VERSION} AS build
|
|||||||
ENV CGO_ENABLED=1
|
ENV CGO_ENABLED=1
|
||||||
|
|
||||||
COPY . .
|
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
|
# Remove unnecessary files
|
||||||
RUN rm -rf /opt/imgproxy/lib/pkgconfig /opt/imgproxy/lib/cmake
|
RUN rm -rf /opt/imgproxy/lib/pkgconfig /opt/imgproxy/lib/cmake
|
||||||
@@ -24,14 +24,14 @@ LABEL maintainer="Sergey Alexandrovich <darthsim@gmail.com>"
|
|||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get upgrade -y \
|
&& apt-get upgrade -y \
|
||||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
bash \
|
bash \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
libstdc++6 \
|
libstdc++6 \
|
||||||
fontconfig-config \
|
fontconfig-config \
|
||||||
fonts-dejavu-core \
|
fonts-dejavu-core \
|
||||||
media-types \
|
media-types \
|
||||||
libjemalloc2 \
|
libjemalloc2 \
|
||||||
libtcmalloc-minimal4 \
|
libtcmalloc-minimal4 \
|
||||||
&& ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so \
|
&& ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so \
|
||||||
&& ln -s /usr/lib/$(uname -m)-linux-gnu/libtcmalloc_minimal.so.4 /usr/local/lib/libtcmalloc_minimal.so \
|
&& ln -s /usr/lib/$(uname -m)-linux-gnu/libtcmalloc_minimal.so.4 /usr/local/lib/libtcmalloc_minimal.so \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
@@ -13,8 +13,10 @@ import (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
// UserAgent is the User-Agent header to use when fetching images.
|
// UserAgent is the User-Agent header to use when fetching images.
|
||||||
UserAgent string
|
UserAgent string
|
||||||
|
|
||||||
// DownloadTimeout is the timeout for downloading an image, in seconds.
|
// DownloadTimeout is the timeout for downloading an image, in seconds.
|
||||||
DownloadTimeout time.Duration
|
DownloadTimeout time.Duration
|
||||||
|
|
||||||
// MaxRedirects is the maximum number of redirects to follow when fetching an image.
|
// MaxRedirects is the maximum number of redirects to follow when fetching an image.
|
||||||
MaxRedirects int
|
MaxRedirects int
|
||||||
}
|
}
|
||||||
|
3
go.mod
3
go.mod
@@ -119,6 +119,7 @@ require (
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect
|
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect
|
||||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 // 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/common v0.65.0 // indirect
|
||||||
github.com/prometheus/procfs v0.17.0 // indirect
|
github.com/prometheus/procfs v0.17.0 // indirect
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // 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/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
|
||||||
github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect
|
github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect
|
||||||
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 // 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/tinylib/msgp v1.3.0 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||||
github.com/tklauser/numcpus v0.10.0 // 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/x448/float16 v0.8.4 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
github.com/zeebo/errs v1.4.0 // 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 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
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 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 h1:NMWdswlRx2M9uPY4Ux8p/Q/rDs7A97OG89fECiQ/Tz0=
|
||||||
github.com/DarthSim/godotenv v1.3.1/go.mod h1:B3ySe1HYTUFFR6+TPyHyxPWjUdh48il0Blebg9p1cCc=
|
github.com/DarthSim/godotenv v1.3.1/go.mod h1:B3ySe1HYTUFFR6+TPyHyxPWjUdh48il0Blebg9p1cCc=
|
||||||
github.com/DataDog/appsec-internal-go v1.13.0 h1:aO6DmHYsAU8BNFuvYJByhMKGgcQT3WAbj9J/sgAJxtA=
|
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/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 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI=
|
||||||
github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI=
|
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.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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
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/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 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
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 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
|
||||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
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=
|
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/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 h1:zY8bolSga5kOjBAaHS6hrdxLgEoYuT875xTy0QDwZWs=
|
||||||
github.com/trimmer-io/go-xmp v1.0.0/go.mod h1:Aaptr9sp1lLv7UnCAdQ+gSHZyY2miYaKmcNVj7HRBwA=
|
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 h1:A2wsiTbvp63ilDaWmsk2wjx6xZdxQOvpiNlKBGKKXKI=
|
||||||
github.com/vmihailenco/msgpack/v4 v4.3.13/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
|
github.com/vmihailenco/msgpack/v4 v4.3.13/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
|
||||||
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
|
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
|
||||||
|
@@ -26,7 +26,7 @@ type Handler struct {
|
|||||||
semaphores *semaphores.Semaphores
|
semaphores *semaphores.Semaphores
|
||||||
fallbackImage auximageprovider.Provider
|
fallbackImage auximageprovider.Provider
|
||||||
watermarkImage auximageprovider.Provider
|
watermarkImage auximageprovider.Provider
|
||||||
imageData *imagedata.Factory
|
idf *imagedata.Factory
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates new handler object
|
// New creates new handler object
|
||||||
@@ -50,7 +50,7 @@ func New(
|
|||||||
semaphores: semaphores,
|
semaphores: semaphores,
|
||||||
fallbackImage: fi,
|
fallbackImage: fi,
|
||||||
watermarkImage: wi,
|
watermarkImage: wi,
|
||||||
imageData: idf,
|
idf: idf,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ func (h *Handler) Execute(
|
|||||||
monitoringMeta: mm,
|
monitoringMeta: mm,
|
||||||
semaphores: h.semaphores,
|
semaphores: h.semaphores,
|
||||||
hwr: h.hw.NewRequest(),
|
hwr: h.hw.NewRequest(),
|
||||||
idf: h.imageData,
|
idf: h.idf,
|
||||||
}
|
}
|
||||||
|
|
||||||
return req.execute(ctx)
|
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
|
// processImage calls actual image processing
|
||||||
func (r *request) processImage(ctx context.Context, originData imagedata.ImageData) (*processing.Result, error) {
|
func (r *request) processImage(ctx context.Context, originData imagedata.ImageData) (*processing.Result, error) {
|
||||||
defer monitoring.StartProcessingSegment(ctx, r.monitoringMeta.Filter(monitoring.MetaProcessingOptions))()
|
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
|
// writeDebugHeaders writes debug headers (X-Origin-*, X-Result-*) to the response
|
||||||
|
189
imgproxy.go
Normal file
189
imgproxy.go
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
package imgproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v3/auximageprovider"
|
||||||
|
cfg "github.com/imgproxy/imgproxy/v3/config"
|
||||||
|
"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/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/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := processing.ValidatePreferredFormats(); err != nil {
|
||||||
|
vips.Shutdown()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := options.ParsePresets(cfg.Presets); err != nil {
|
||||||
|
vips.Shutdown()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := options.ValidatePresets(); err != nil {
|
||||||
|
vips.Shutdown()
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
79
init.go
Normal file
79
init.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// init_once.go contains global initialization/teardown functions that should be called exactly once
|
||||||
|
// per process.
|
||||||
|
package imgproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"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/vips"
|
||||||
|
"go.uber.org/automaxprocs/maxprocs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
done atomic.Bool // done indicates that initialization has been performed
|
||||||
|
once sync.Once // once is used to ensure initialization is performed only once
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init performs the global resources initialization. This should be done once per process.
|
||||||
|
func Init() error {
|
||||||
|
var err error
|
||||||
|
once.Do(func() {
|
||||||
|
err = initialize()
|
||||||
|
done.Store(true)
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown performs global cleanup
|
||||||
|
func Shutdown() {
|
||||||
|
if !done.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vips.Shutdown()
|
||||||
|
monitoring.Stop()
|
||||||
|
errorreport.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize contains the actual initialization logic
|
||||||
|
func initialize() error {
|
||||||
|
if err := logger.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
// 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 := loadenv.Load(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.Configure(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// NOTE: End of temporary workaround.
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@@ -1,12 +1,12 @@
|
|||||||
//go:build integration
|
|
||||||
// +build integration
|
|
||||||
|
|
||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/png"
|
"image/png"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -14,28 +14,52 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/corona10/goimagehash"
|
"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/imagetype"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/testutil"
|
||||||
"github.com/imgproxy/imgproxy/v3/vips"
|
"github.com/imgproxy/imgproxy/v3/vips"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
similarityThreshold = 5 // Distance between images to be considered similar
|
similarityThreshold = 5 // Distance between images to be considered similar
|
||||||
|
bindPort = 9090 // Port to bind imgproxy to
|
||||||
|
bindHost = "localhost"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type IntegrationSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
testData *testutil.TestDataProvider
|
||||||
|
testImagesPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite starts imgproxy instance server
|
||||||
|
func (s *IntegrationSuite) 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 *IntegrationSuite) TearDownSuite() {
|
||||||
|
s.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
// testLoadFolder fetches images iterates over images in the specified folder,
|
// testLoadFolder fetches images iterates over images in the specified folder,
|
||||||
// runs imgproxy on each image, and compares the result with the reference image
|
// 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
|
// which is expected to be in the `integration` folder with the same name
|
||||||
// but with `.png` extension.
|
// but with `.png` extension.
|
||||||
func testLoadFolder(t *testing.T, cs, sourcePath, folder string) {
|
func (s *IntegrationSuite) testLoadFolder(folder string) {
|
||||||
t.Logf("Testing folder: %s", folder)
|
walkPath := path.Join(s.testImagesPath, folder)
|
||||||
|
|
||||||
walkPath := path.Join(sourcePath, folder)
|
|
||||||
|
|
||||||
// Iterate over the files in the source folder
|
// Iterate over the files in the source folder
|
||||||
err := filepath.Walk(walkPath, func(path string, info os.FileInfo, err error) error {
|
err := filepath.Walk(walkPath, func(path string, info os.FileInfo, err error) error {
|
||||||
require.NoError(t, err)
|
s.Require().NoError(err)
|
||||||
|
|
||||||
// Skip directories
|
// Skip directories
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
@@ -49,39 +73,79 @@ func testLoadFolder(t *testing.T, cs, sourcePath, folder string) {
|
|||||||
referencePath := strings.TrimSuffix(basePath, filepath.Ext(basePath)) + ".png"
|
referencePath := strings.TrimSuffix(basePath, filepath.Ext(basePath)) + ".png"
|
||||||
|
|
||||||
// Construct the full path to the reference image (integration/ folder)
|
// 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)
|
// Construct the source URL for imgproxy (no processing)
|
||||||
sourceUrl := fmt.Sprintf("insecure/plain/local:///%s/%s@png", folder, basePath)
|
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))
|
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)
|
referenceFile, err := os.Open(referencePath)
|
||||||
require.NoError(t, err)
|
s.Require().NoError(err)
|
||||||
defer referenceFile.Close()
|
defer referenceFile.Close()
|
||||||
|
|
||||||
referenceImage, err := png.Decode(referenceFile)
|
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)
|
hash1, err := goimagehash.DifferenceHash(imgproxyImage)
|
||||||
require.NoError(t, err)
|
s.Require().NoError(err)
|
||||||
|
|
||||||
hash2, err := goimagehash.DifferenceHash(referenceImage)
|
hash2, err := goimagehash.DifferenceHash(referenceImage)
|
||||||
require.NoError(t, err)
|
s.Require().NoError(err)
|
||||||
|
|
||||||
distance, err := hash1.Distance(hash2)
|
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",
|
"Image %s differs from reference image %s by %d, which is greater than the allowed threshold of %d",
|
||||||
basePath, referencePath, distance, similarityThreshold)
|
basePath, referencePath, distance, similarityThreshold)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
require.NoError(t, err)
|
s.Require().NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchImage fetches an image from the imgproxy server
|
||||||
|
func (s *IntegrationSuite) 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 *IntegrationSuite) 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,
|
// TestLoadSaveToPng ensures that our load pipeline works,
|
||||||
@@ -89,53 +153,34 @@ func testLoadFolder(t *testing.T, cs, sourcePath, folder string) {
|
|||||||
// in the folder, it does the passthrough request through imgproxy:
|
// in the folder, it does the passthrough request through imgproxy:
|
||||||
// no processing, just convert format of the source file to png.
|
// no processing, just convert format of the source file to png.
|
||||||
// Then, it compares the result with the reference image.
|
// Then, it compares the result with the reference image.
|
||||||
func TestLoadSaveToPng(t *testing.T) {
|
func (s *IntegrationSuite) TestLoadSaveToPng() {
|
||||||
ctx := t.Context()
|
testCases := []struct {
|
||||||
|
name string
|
||||||
// TODO: Will be moved to test suite (like in processing_test.go)
|
imageType imagetype.Type
|
||||||
// Since we use SupportsLoad, we need to initialize vips
|
folderName string
|
||||||
defer vips.Shutdown() // either way it needs to be deinitialized
|
}{
|
||||||
err := vips.Init()
|
{"GIF", imagetype.GIF, "gif"},
|
||||||
require.NoError(t, err, "Failed to initialize vips")
|
{"JPEG", imagetype.JPEG, "jpg"},
|
||||||
|
{"HEIC", imagetype.HEIC, "heif"},
|
||||||
path, err := testImagesPath(t)
|
{"JXL", imagetype.JXL, "jxl"},
|
||||||
require.NoError(t, err)
|
{"SVG", imagetype.SVG, "svg"},
|
||||||
|
{"TIFF", imagetype.TIFF, "tiff"},
|
||||||
cs := startImgproxy(t, ctx, path)
|
{"WEBP", imagetype.WEBP, "webp"},
|
||||||
|
{"BMP", imagetype.BMP, "bmp"},
|
||||||
if vips.SupportsLoad(imagetype.GIF) {
|
{"ICO", imagetype.ICO, "ico"},
|
||||||
testLoadFolder(t, cs, path, "gif")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if vips.SupportsLoad(imagetype.JPEG) {
|
for _, tc := range testCases {
|
||||||
testLoadFolder(t, cs, path, "jpg")
|
s.T().Run(tc.name, func(t *testing.T) {
|
||||||
}
|
if vips.SupportsLoad(tc.imageType) {
|
||||||
|
s.testLoadFolder(tc.folderName)
|
||||||
if vips.SupportsLoad(imagetype.HEIC) {
|
} else {
|
||||||
testLoadFolder(t, cs, path, "heif")
|
t.Skipf("%s format not supported by VIPS", tc.name)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIntegration(t *testing.T) {
|
||||||
|
suite.Run(t, new(IntegrationSuite))
|
||||||
|
}
|
||||||
|
15
integration/main_test.go
Normal file
15
integration/main_test.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
package integration
|
||||||
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -11,70 +6,84 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/imgproxy/imgproxy/v3"
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/config"
|
"github.com/imgproxy/imgproxy/v3/config"
|
||||||
"github.com/imgproxy/imgproxy/v3/config/configurators"
|
"github.com/imgproxy/imgproxy/v3/config/configurators"
|
||||||
"github.com/imgproxy/imgproxy/v3/fetcher"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
||||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||||
"github.com/imgproxy/imgproxy/v3/server"
|
"github.com/imgproxy/imgproxy/v3/server"
|
||||||
"github.com/imgproxy/imgproxy/v3/svg"
|
"github.com/imgproxy/imgproxy/v3/svg"
|
||||||
"github.com/imgproxy/imgproxy/v3/testutil"
|
"github.com/imgproxy/imgproxy/v3/testutil"
|
||||||
"github.com/imgproxy/imgproxy/v3/transport"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/vips"
|
"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 {
|
type ProcessingHandlerTestSuite struct {
|
||||||
suite.Suite
|
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() {
|
func (s *ProcessingHandlerTestSuite) SetupSuite() {
|
||||||
config.Reset()
|
// Silence all the logs
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
logrus.SetOutput(io.Discard)
|
logrus.SetOutput(io.Discard)
|
||||||
|
|
||||||
cfg := server.NewDefaultConfig()
|
// Initialize test data provider (local test files)
|
||||||
r, err := server.NewRouter(&cfg)
|
s.testData = testutil.NewTestDataProvider(s.T())
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
s.router = buildRouter(r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TeardownSuite() {
|
func (s *ProcessingHandlerTestSuite) TeardownSuite() {
|
||||||
shutdown()
|
|
||||||
logrus.SetOutput(os.Stdout)
|
logrus.SetOutput(os.Stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) SetupTest() {
|
// setupObjs initializes lazy objects
|
||||||
wd, err := os.Getwd()
|
func (s *ProcessingHandlerTestSuite) setupObjs() {
|
||||||
s.Require().NoError(err)
|
s.config = testutil.NewLazyObj(s.T(), func() (*imgproxy.Config, error) {
|
||||||
|
c, err := imgproxy.LoadConfigFromEnv(nil)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
config.Reset()
|
c.Transport.Local.Root = s.testData.Root()
|
||||||
config.AllowLoopbackSourceAddresses = true
|
c.Transport.HTTP.ClientKeepAliveTimeout = 0
|
||||||
config.LocalFileSystemRoot = filepath.Join(wd, "/testdata")
|
|
||||||
config.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)
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
|
|
||||||
@@ -82,84 +91,43 @@ func (s *ProcessingHandlerTestSuite) send(path string, header ...http.Header) *h
|
|||||||
req.Header = header[0]
|
req.Header = header[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
s.router.ServeHTTP(rw, req)
|
s.router().ServeHTTP(rw, req)
|
||||||
|
|
||||||
return rw
|
return rw.Result()
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestSignatureValidationFailure() {
|
func (s *ProcessingHandlerTestSuite) TestSignatureValidationFailure() {
|
||||||
config.Keys = [][]byte{[]byte("test-key")}
|
config.Keys = [][]byte{[]byte("test-key")}
|
||||||
config.Salts = [][]byte{[]byte("test-salt")}
|
config.Salts = [][]byte{[]byte("test-salt")}
|
||||||
|
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
tt := []struct {
|
||||||
res := rw.Result()
|
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)
|
for _, tc := range tt {
|
||||||
}
|
s.Run(tc.name, func() {
|
||||||
|
res := s.GET(tc.url)
|
||||||
func (s *ProcessingHandlerTestSuite) TestSignatureValidationSuccess() {
|
s.Require().Equal(tc.statusCode, res.StatusCode)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestSourceValidation() {
|
func (s *ProcessingHandlerTestSuite) TestSourceValidation() {
|
||||||
@@ -176,19 +144,16 @@ func (s *ProcessingHandlerTestSuite) TestSourceValidation() {
|
|||||||
name: "match http URL without wildcard",
|
name: "match http URL without wildcard",
|
||||||
allowedSources: []string{"local://", "http://images.dev/"},
|
allowedSources: []string{"local://", "http://images.dev/"},
|
||||||
requestPath: "/unsafe/plain/http://images.dev/lorem/ipsum.jpg",
|
requestPath: "/unsafe/plain/http://images.dev/lorem/ipsum.jpg",
|
||||||
expectedError: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "match http URL with wildcard in hostname single level",
|
name: "match http URL with wildcard in hostname single level",
|
||||||
allowedSources: []string{"local://", "http://*.mycdn.dev/"},
|
allowedSources: []string{"local://", "http://*.mycdn.dev/"},
|
||||||
requestPath: "/unsafe/plain/http://a-1.mycdn.dev/lorem/ipsum.jpg",
|
requestPath: "/unsafe/plain/http://a-1.mycdn.dev/lorem/ipsum.jpg",
|
||||||
expectedError: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "match http URL with wildcard in hostname multiple levels",
|
name: "match http URL with wildcard in hostname multiple levels",
|
||||||
allowedSources: []string{"local://", "http://*.mycdn.dev/"},
|
allowedSources: []string{"local://", "http://*.mycdn.dev/"},
|
||||||
requestPath: "/unsafe/plain/http://a-1.b-2.mycdn.dev/lorem/ipsum.jpg",
|
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",
|
name: "no match s3 URL with allowed local and http URLs",
|
||||||
@@ -206,26 +171,24 @@ func (s *ProcessingHandlerTestSuite) TestSourceValidation() {
|
|||||||
|
|
||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
s.Run(tc.name, func() {
|
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 {
|
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 := s.GET(tc.requestPath)
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
if tc.expectedError {
|
if tc.expectedError {
|
||||||
s.Require().Equal(404, res.StatusCode)
|
s.Require().Equal(http.StatusNotFound, res.StatusCode)
|
||||||
} else {
|
} else {
|
||||||
s.Require().Equal(200, res.StatusCode)
|
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestSourceNetworkValidation() {
|
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) {
|
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
@@ -233,180 +196,158 @@ func (s *ProcessingHandlerTestSuite) TestSourceNetworkValidation() {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
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.Run("AllowLoopbackSourceAddressesFalse", func() {
|
||||||
s.Require().Equal(200, rw.Result().StatusCode)
|
config.AllowLoopbackSourceAddresses = false
|
||||||
|
res := s.GET(url)
|
||||||
config.AllowLoopbackSourceAddresses = false
|
s.Require().Equal(http.StatusNotFound, res.StatusCode)
|
||||||
rw = s.send(u)
|
})
|
||||||
s.Require().Equal(404, rw.Result().StatusCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestSourceFormatNotSupported() {
|
func (s *ProcessingHandlerTestSuite) TestSourceFormatNotSupported() {
|
||||||
vips.DisableLoadSupport(imagetype.PNG)
|
vips.DisableLoadSupport(imagetype.PNG)
|
||||||
defer vips.ResetLoadSupport()
|
defer vips.ResetLoadSupport()
|
||||||
|
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||||
res := rw.Result()
|
s.Require().Equal(http.StatusUnprocessableEntity, res.StatusCode)
|
||||||
|
|
||||||
s.Require().Equal(422, res.StatusCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestResultingFormatNotSupported() {
|
func (s *ProcessingHandlerTestSuite) TestResultingFormatNotSupported() {
|
||||||
vips.DisableSaveSupport(imagetype.PNG)
|
vips.DisableSaveSupport(imagetype.PNG)
|
||||||
defer vips.ResetSaveSupport()
|
defer vips.ResetSaveSupport()
|
||||||
|
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
|
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
|
||||||
res := rw.Result()
|
s.Require().Equal(http.StatusUnprocessableEntity, res.StatusCode)
|
||||||
|
|
||||||
s.Require().Equal(422, res.StatusCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingConfig() {
|
func (s *ProcessingHandlerTestSuite) TestSkipProcessingConfig() {
|
||||||
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
||||||
|
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||||
|
s.Require().True(s.testData.FileEqualsToReader("test1.png", res.Body))
|
||||||
expected := s.readTestImageData("test1.png")
|
|
||||||
|
|
||||||
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingPO() {
|
func (s *ProcessingHandlerTestSuite) TestSkipProcessingPO() {
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/skp:png/plain/local:///test1.png")
|
res := s.GET("/unsafe/rs:fill:4:4/skp:png/plain/local:///test1.png")
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||||
|
s.Require().True(s.testData.FileEqualsToReader("test1.png", res.Body))
|
||||||
expected := s.readTestImageData("test1.png")
|
|
||||||
|
|
||||||
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
|
func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
|
||||||
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
||||||
|
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
|
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||||
|
s.Require().True(s.testData.FileEqualsToReader("test1.png", res.Body))
|
||||||
expected := s.readTestImageData("test1.png")
|
|
||||||
|
|
||||||
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingDifferentFormat() {
|
func (s *ProcessingHandlerTestSuite) TestSkipProcessingDifferentFormat() {
|
||||||
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
|
||||||
|
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@jpg")
|
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@jpg")
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||||
|
s.Require().False(s.testData.FileEqualsToReader("test1.png", res.Body))
|
||||||
expected := s.readTestImageData("test1.png")
|
|
||||||
|
|
||||||
s.Require().False(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestSkipProcessingSVG() {
|
func (s *ProcessingHandlerTestSuite) TestSkipProcessingSVG() {
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.svg")
|
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.svg")
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
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().NoError(err)
|
||||||
|
|
||||||
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestNotSkipProcessingSVGToJPG() {
|
func (s *ProcessingHandlerTestSuite) TestNotSkipProcessingSVGToJPG() {
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.svg@jpg")
|
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.svg@jpg")
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||||
|
s.Require().False(s.testData.FileEqualsToReader("test1.svg", res.Body))
|
||||||
expected := s.readTestImageData("test1.svg")
|
|
||||||
|
|
||||||
s.Require().False(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestErrorSavingToSVG() {
|
func (s *ProcessingHandlerTestSuite) TestErrorSavingToSVG() {
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@svg")
|
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@svg")
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(422, res.StatusCode)
|
s.Require().Equal(http.StatusUnprocessableEntity, res.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughCacheControl() {
|
func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughCacheControl() {
|
||||||
config.CacheControlPassthrough = true
|
s.config().HeaderWriter.CacheControlPassthrough = true
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.Header().Set("Cache-Control", "max-age=1234, public")
|
rw.Header().Set(httpheaders.CacheControl, "max-age=1234, public")
|
||||||
rw.Header().Set("Expires", time.Now().Add(time.Hour).UTC().Format(http.TimeFormat))
|
rw.Header().Set(httpheaders.Expires, time.Now().Add(time.Hour).UTC().Format(http.TimeFormat))
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
rw.Write(s.readTestFile("test1.png"))
|
rw.Write(s.testData.Read("test1.png"))
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal("max-age=1234, public", res.Header.Get("Cache-Control"))
|
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||||
s.Require().Empty(res.Header.Get("Expires"))
|
s.Require().Equal("max-age=1234, public", res.Header.Get(httpheaders.CacheControl))
|
||||||
|
s.Require().Empty(res.Header.Get(httpheaders.Expires))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughExpires() {
|
func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughExpires() {
|
||||||
config.CacheControlPassthrough = true
|
config.CacheControlPassthrough = true
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
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.WriteHeader(200)
|
||||||
rw.Write(s.readTestFile("test1.png"))
|
rw.Write(s.testData.Read("test1.png"))
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
// Use regex to allow some delay
|
// Use regex to allow some delay
|
||||||
s.Require().Regexp("max-age=123[0-9], public", res.Header.Get("Cache-Control"))
|
s.Require().Regexp("max-age=123[0-9], public", res.Header.Get(httpheaders.CacheControl))
|
||||||
s.Require().Empty(res.Header.Get("Expires"))
|
s.Require().Empty(res.Header.Get(httpheaders.Expires))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughDisabled() {
|
func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughDisabled() {
|
||||||
config.CacheControlPassthrough = false
|
config.CacheControlPassthrough = false
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.Header().Set("Cache-Control", "max-age=1234, public")
|
rw.Header().Set(httpheaders.CacheControl, "max-age=1234, public")
|
||||||
rw.Header().Set("Expires", time.Now().Add(time.Hour).UTC().Format(http.TimeFormat))
|
rw.Header().Set(httpheaders.Expires, time.Now().Add(time.Hour).UTC().Format(http.TimeFormat))
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
rw.Write(s.readTestFile("test1.png"))
|
rw.Write(s.testData.Read("test1.png"))
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().NotEqual("max-age=1234, public", res.Header.Get("Cache-Control"))
|
s.Require().NotEqual("max-age=1234, public", res.Header.Get(httpheaders.CacheControl))
|
||||||
s.Require().Empty(res.Header.Get("Expires"))
|
s.Require().Empty(res.Header.Get(httpheaders.Expires))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestETagDisabled() {
|
func (s *ProcessingHandlerTestSuite) TestETagDisabled() {
|
||||||
config.ETagEnabled = false
|
config.ETagEnabled = false
|
||||||
|
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
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() {
|
func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
|
||||||
@@ -425,8 +366,7 @@ func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
|
|||||||
header := make(http.Header)
|
header := make(http.Header)
|
||||||
header.Set(httpheaders.IfNoneMatch, etag)
|
header.Set(httpheaders.IfNoneMatch, etag)
|
||||||
|
|
||||||
rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(304, res.StatusCode)
|
s.Require().Equal(304, res.StatusCode)
|
||||||
s.Require().Equal(etag, res.Header.Get(httpheaders.Etag))
|
s.Require().Equal(etag, res.Header.Get(httpheaders.Etag))
|
||||||
@@ -435,39 +375,37 @@ func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
|
|||||||
func (s *ProcessingHandlerTestSuite) TestLastModifiedEnabled() {
|
func (s *ProcessingHandlerTestSuite) TestLastModifiedEnabled() {
|
||||||
config.LastModifiedEnabled = true
|
config.LastModifiedEnabled = true
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
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.WriteHeader(200)
|
||||||
rw.Write(s.readTestFile("test1.png"))
|
rw.Write(s.testData.Read("test1.png"))
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
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() {
|
func (s *ProcessingHandlerTestSuite) TestLastModifiedDisabled() {
|
||||||
config.LastModifiedEnabled = false
|
config.LastModifiedEnabled = false
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
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.WriteHeader(200)
|
||||||
rw.Write(s.readTestFile("test1.png"))
|
rw.Write(s.testData.Read("test1.png"))
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Empty(res.Header.Get("Last-Modified"))
|
s.Require().Empty(res.Header.Get(httpheaders.LastModified))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqExactMatchLastModifiedDisabled() {
|
func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqExactMatchLastModifiedDisabled() {
|
||||||
config.LastModifiedEnabled = false
|
config.LastModifiedEnabled = false
|
||||||
data := s.readTestFile("test1.png")
|
data := s.testData.Read("test1.png")
|
||||||
lastModified := "Wed, 21 Oct 2015 07:28:00 GMT"
|
lastModified := "Wed, 21 Oct 2015 07:28:00 GMT"
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
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)
|
s.Empty(modifiedSince)
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
rw.Write(data)
|
rw.Write(data)
|
||||||
@@ -475,9 +413,8 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqExactMatchLastModifiedD
|
|||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
header := make(http.Header)
|
header := make(http.Header)
|
||||||
header.Set("If-Modified-Since", lastModified)
|
header.Set(httpheaders.IfModifiedSince, lastModified)
|
||||||
rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
s.Require().Equal(200, res.StatusCode)
|
||||||
}
|
}
|
||||||
@@ -486,25 +423,24 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqExactMatchLastModifiedE
|
|||||||
config.LastModifiedEnabled = true
|
config.LastModifiedEnabled = true
|
||||||
lastModified := "Wed, 21 Oct 2015 07:28:00 GMT"
|
lastModified := "Wed, 21 Oct 2015 07:28:00 GMT"
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
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)
|
s.Equal(lastModified, modifiedSince)
|
||||||
rw.WriteHeader(304)
|
rw.WriteHeader(304)
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
header := make(http.Header)
|
header := make(http.Header)
|
||||||
header.Set("If-Modified-Since", lastModified)
|
header.Set(httpheaders.IfModifiedSince, lastModified)
|
||||||
rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(304, res.StatusCode)
|
s.Require().Equal(304, res.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareMoreRecentLastModifiedDisabled() {
|
func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareMoreRecentLastModifiedDisabled() {
|
||||||
data := s.readTestFile("test1.png")
|
data := s.testData.Read("test1.png")
|
||||||
config.LastModifiedEnabled = false
|
config.LastModifiedEnabled = false
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
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)
|
s.Empty(modifiedSince)
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
rw.Write(data)
|
rw.Write(data)
|
||||||
@@ -514,10 +450,9 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareMoreRecentLastMo
|
|||||||
recentTimestamp := "Thu, 25 Feb 2021 01:45:00 GMT"
|
recentTimestamp := "Thu, 25 Feb 2021 01:45:00 GMT"
|
||||||
|
|
||||||
header := make(http.Header)
|
header := make(http.Header)
|
||||||
header.Set("If-Modified-Since", recentTimestamp)
|
header.Set(httpheaders.IfModifiedSince, recentTimestamp)
|
||||||
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(200, res.StatusCode)
|
s.Require().Equal(200, res.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,7 +460,7 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareMoreRecentLastMo
|
|||||||
config.LastModifiedEnabled = true
|
config.LastModifiedEnabled = true
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
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")
|
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)
|
parsedModifiedSince, err := time.Parse(http.TimeFormat, modifiedSince)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
s.True(fileLastModified.Before(parsedModifiedSince))
|
s.True(fileLastModified.Before(parsedModifiedSince))
|
||||||
@@ -536,18 +471,17 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareMoreRecentLastMo
|
|||||||
recentTimestamp := "Thu, 25 Feb 2021 01:45:00 GMT"
|
recentTimestamp := "Thu, 25 Feb 2021 01:45:00 GMT"
|
||||||
|
|
||||||
header := make(http.Header)
|
header := make(http.Header)
|
||||||
header.Set("If-Modified-Since", recentTimestamp)
|
header.Set(httpheaders.IfModifiedSince, recentTimestamp)
|
||||||
rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(304, res.StatusCode)
|
s.Require().Equal(304, res.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifiedDisabled() {
|
func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifiedDisabled() {
|
||||||
config.LastModifiedEnabled = false
|
s.config().ProcessingHandler.LastModifiedEnabled = false
|
||||||
data := s.readTestFile("test1.png")
|
data := s.testData.Read("test1.png")
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
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)
|
s.Empty(modifiedSince)
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
rw.Write(data)
|
rw.Write(data)
|
||||||
@@ -557,19 +491,18 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifi
|
|||||||
oldTimestamp := "Tue, 01 Oct 2013 17:31:00 GMT"
|
oldTimestamp := "Tue, 01 Oct 2013 17:31:00 GMT"
|
||||||
|
|
||||||
header := make(http.Header)
|
header := make(http.Header)
|
||||||
header.Set("If-Modified-Since", oldTimestamp)
|
header.Set(httpheaders.IfModifiedSince, oldTimestamp)
|
||||||
rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
s.Require().Equal(200, res.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifiedEnabled() {
|
func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifiedEnabled() {
|
||||||
config.LastModifiedEnabled = true
|
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) {
|
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")
|
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)
|
parsedModifiedSince, err := time.Parse(http.TimeFormat, modifiedSince)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
s.True(fileLastModified.After(parsedModifiedSince))
|
s.True(fileLastModified.After(parsedModifiedSince))
|
||||||
@@ -581,9 +514,8 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifi
|
|||||||
oldTimestamp := "Tue, 01 Oct 2013 17:31:00 GMT"
|
oldTimestamp := "Tue, 01 Oct 2013 17:31:00 GMT"
|
||||||
|
|
||||||
header := make(http.Header)
|
header := make(http.Header)
|
||||||
header.Set("If-Modified-Since", oldTimestamp)
|
header.Set(httpheaders.IfModifiedSince, oldTimestamp)
|
||||||
rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
s.Require().Equal(200, res.StatusCode)
|
||||||
}
|
}
|
||||||
@@ -591,43 +523,40 @@ func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifi
|
|||||||
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvg() {
|
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvg() {
|
||||||
config.AlwaysRasterizeSvg = true
|
config.AlwaysRasterizeSvg = true
|
||||||
|
|
||||||
rw := s.send("/unsafe/rs:fill:40:40/plain/local:///test1.svg")
|
res := s.GET("/unsafe/rs:fill:40:40/plain/local:///test1.svg")
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
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() {
|
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithEnforceAvif() {
|
||||||
config.AlwaysRasterizeSvg = true
|
config.AlwaysRasterizeSvg = true
|
||||||
config.EnforceWebp = true
|
config.EnforceWebp = true
|
||||||
|
|
||||||
rw := s.send("/unsafe/plain/local:///test1.svg", http.Header{"Accept": []string{"image/webp"}})
|
res := s.GET("/unsafe/plain/local:///test1.svg", http.Header{"Accept": []string{"image/webp"}})
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
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() {
|
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgDisabled() {
|
||||||
config.AlwaysRasterizeSvg = false
|
config.AlwaysRasterizeSvg = false
|
||||||
config.EnforceWebp = true
|
config.EnforceWebp = true
|
||||||
|
|
||||||
rw := s.send("/unsafe/plain/local:///test1.svg")
|
res := s.GET("/unsafe/plain/local:///test1.svg")
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
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() {
|
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithFormat() {
|
||||||
config.AlwaysRasterizeSvg = true
|
config.AlwaysRasterizeSvg = true
|
||||||
config.SkipProcessingFormats = []imagetype.Type{imagetype.SVG}
|
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(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() {
|
func (s *ProcessingHandlerTestSuite) TestMaxSrcFileSizeGlobal() {
|
||||||
@@ -635,12 +564,11 @@ func (s *ProcessingHandlerTestSuite) TestMaxSrcFileSizeGlobal() {
|
|||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
rw.Write(s.readTestFile("test1.png"))
|
rw.Write(s.testData.Read("test1.png"))
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(422, res.StatusCode)
|
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() {
|
|
||||||
vips.Shutdown()
|
|
||||||
monitoring.Stop()
|
|
||||||
errorreport.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
//go:build pprof
|
||||||
// +build pprof
|
// +build pprof
|
||||||
|
|
||||||
package main
|
package imgproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@@ -3,6 +3,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/config"
|
"github.com/imgproxy/imgproxy/v3/config"
|
||||||
@@ -30,6 +31,8 @@ type Config struct {
|
|||||||
DevelopmentErrorsMode bool // Enable development mode for detailed error messages
|
DevelopmentErrorsMode bool // Enable development mode for detailed error messages
|
||||||
SocketReusePort bool // Enable SO_REUSEPORT socket option
|
SocketReusePort bool // Enable SO_REUSEPORT socket option
|
||||||
HealthCheckPath string // Health check path from config
|
HealthCheckPath string // Health check path from config
|
||||||
|
FreeMemoryInterval time.Duration // Interval for freeing memory
|
||||||
|
LogMemStats bool // Log memory stats
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDefaultConfig returns default config values
|
// NewDefaultConfig returns default config values
|
||||||
@@ -48,6 +51,8 @@ func NewDefaultConfig() Config {
|
|||||||
DevelopmentErrorsMode: false,
|
DevelopmentErrorsMode: false,
|
||||||
SocketReusePort: false,
|
SocketReusePort: false,
|
||||||
HealthCheckPath: "",
|
HealthCheckPath: "",
|
||||||
|
FreeMemoryInterval: 10 * time.Second,
|
||||||
|
LogMemStats: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +72,8 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
|
|||||||
c.DevelopmentErrorsMode = config.DevelopmentErrorsMode
|
c.DevelopmentErrorsMode = config.DevelopmentErrorsMode
|
||||||
c.SocketReusePort = config.SoReuseport
|
c.SocketReusePort = config.SoReuseport
|
||||||
c.HealthCheckPath = config.HealthCheckPath
|
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
|
return c, nil
|
||||||
}
|
}
|
||||||
@@ -97,5 +104,9 @@ func (c *Config) Validate() error {
|
|||||||
return fmt.Errorf("graceful timeout should be greater than or equal to 0, now - %d", c.GracefulTimeout)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -127,14 +127,16 @@ func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
rw.Header().Set(httpheaders.XRequestID, reqID)
|
rw.Header().Set(httpheaders.XRequestID, reqID)
|
||||||
|
|
||||||
for _, rr := range r.routes {
|
for _, rr := range r.routes {
|
||||||
if rr.isMatch(req) {
|
if !rr.isMatch(req) {
|
||||||
if !rr.silent {
|
continue
|
||||||
LogRequest(reqID, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
rr.handler(reqID, rw, req)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !rr.silent {
|
||||||
|
LogRequest(reqID, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
rr.handler(reqID, rw, req)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Means that we have not found matching route
|
// Means that we have not found matching route
|
||||||
|
34
testutil/lazy_obj.go
Normal file
34
testutil/lazy_obj.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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()
|
||||||
|
if err != nil {
|
||||||
|
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