diff --git a/CHANGELOG.md b/CHANGELOG.md index d0990c8e..e31660ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] +### Added +- Fallback images. ## [2.12.0] - 2020-04-07 ### Addded diff --git a/docs/configuration.md b/docs/configuration.md index 1b792a68..4ef6cb7f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -155,6 +155,14 @@ imgproxy Pro can extract specific frames of videos to create thumbnails. The fea Read more about watermarks in the [Watermark](watermark.md) guide. +## Fallback image + +You can set up a fallback image that will be used in case imgproxy can't fetch the requested one. Use one of the following variables: + +* `IMGPROXY_FALLBACK_IMAGE_DATA`: Base64-encoded image data. You can easily calculate it with `base64 tmp/fallback.png | tr -d '\n'`; +* `IMGPROXY_FALLBACK_IMAGE_PATH`: path to the locally stored image; +* `IMGPROXY_FALLBACK_IMAGE_URL`: fallback image URL. + ## Presets Read about imgproxy presets in the [Presets](presets.md) guide. diff --git a/download.go b/download.go index 3075e72a..90d64cb2 100644 --- a/download.go +++ b/download.go @@ -30,19 +30,6 @@ const msgSourceImageIsUnreachable = "Source image is unreachable" var downloadBufPool *bufPool -type imageData struct { - Data []byte - Type imageType - - cancel context.CancelFunc -} - -func (d *imageData) Close() { - if d.cancel != nil { - d.cancel() - } -} - type limitReader struct { r io.Reader left int diff --git a/image_data.go b/image_data.go index 01bab47e..96953f98 100644 --- a/image_data.go +++ b/image_data.go @@ -2,22 +2,36 @@ package main import ( "bytes" + "context" "encoding/base64" "fmt" "os" ) +type imageData struct { + Data []byte + Type imageType + + cancel context.CancelFunc +} + +func (d *imageData) Close() { + if d.cancel != nil { + d.cancel() + } +} + func getWatermarkData() (*imageData, error) { if len(conf.WatermarkData) > 0 { - return base64ImageData(conf.WatermarkData) + return base64ImageData(conf.WatermarkData, "watermark") } if len(conf.WatermarkPath) > 0 { - return fileImageData(conf.WatermarkPath) + return fileImageData(conf.WatermarkPath, "watermark") } if len(conf.WatermarkURL) > 0 { - return remoteImageData(conf.WatermarkURL) + return remoteImageData(conf.WatermarkURL, "watermark") } return nil, nil @@ -25,65 +39,65 @@ func getWatermarkData() (*imageData, error) { func getFallbackImageData() (*imageData, error) { if len(conf.FallbackImageData) > 0 { - return base64ImageData(conf.FallbackImageData) + return base64ImageData(conf.FallbackImageData, "fallback image") } if len(conf.FallbackImagePath) > 0 { - return fileImageData(conf.FallbackImagePath) + return fileImageData(conf.FallbackImagePath, "fallback image") } if len(conf.FallbackImageURL) > 0 { - return remoteImageData(conf.FallbackImageURL) + return remoteImageData(conf.FallbackImageURL, "fallback image") } return nil, nil } -func base64ImageData(encoded string) (*imageData, error) { +func base64ImageData(encoded, desc string) (*imageData, error) { data, err := base64.StdEncoding.DecodeString(encoded) if err != nil { - return nil, fmt.Errorf("Can't decode image data: %s", err) + return nil, fmt.Errorf("Can't decode %s data: %s", desc, err) } imgtype, err := checkTypeAndDimensions(bytes.NewReader(data)) if err != nil { - return nil, fmt.Errorf("Can't decode image: %s", err) + return nil, fmt.Errorf("Can't decode %s: %s", desc, err) } return &imageData{Data: data, Type: imgtype}, nil } -func fileImageData(path string) (*imageData, error) { +func fileImageData(path, desc string) (*imageData, error) { f, err := os.Open(path) if err != nil { - return nil, fmt.Errorf("Can't read watermark: %s", err) + return nil, fmt.Errorf("Can't read %s: %s", desc, err) } fi, err := f.Stat() if err != nil { - return nil, fmt.Errorf("Can't read watermark: %s", err) + return nil, fmt.Errorf("Can't read %s: %s", desc, err) } imgdata, err := readAndCheckImage(f, int(fi.Size())) if err != nil { - return nil, fmt.Errorf("Can't read watermark: %s", err) + return nil, fmt.Errorf("Can't read %s: %s", desc, err) } return imgdata, err } -func remoteImageData(imageURL string) (*imageData, error) { +func remoteImageData(imageURL, desc string) (*imageData, error) { res, err := requestImage(imageURL) if res != nil { defer res.Body.Close() } if err != nil { - return nil, fmt.Errorf("Can't download image: %s", err) + return nil, fmt.Errorf("Can't download %s: %s", desc, err) } imgdata, err := readAndCheckImage(res.Body, int(res.ContentLength)) if err != nil { - return nil, fmt.Errorf("Can't download image: %s", err) + return nil, fmt.Errorf("Can't download %s: %s", desc, err) } return imgdata, err diff --git a/processing_handler.go b/processing_handler.go index 4a7c55ca..ca06e583 100644 --- a/processing_handler.go +++ b/processing_handler.go @@ -16,14 +16,15 @@ var ( processingSem chan struct{} headerVaryValue string - fallback *imageData + fallbackImage *imageData ) func initProcessingHandler() error { + var err error + processingSem = make(chan struct{}, conf.Concurrency) if conf.GZipCompression > 0 { - var err error responseGzipBufPool = newBufPool("gzip", conf.Concurrency, conf.GZipBufferSize) if responseGzipPool, err = newGzipPool(conf.Concurrency); err != nil { return err @@ -46,7 +47,7 @@ func initProcessingHandler() error { headerVaryValue = strings.Join(vary, ", ") - if err := loadFallback(); err != nil { + if fallbackImage, err = getFallbackImageData(); err != nil { return err } @@ -158,12 +159,17 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) { if prometheusEnabled { incrementPrometheusErrorsTotal("download") } - if fallback != nil { - logError("Could not load image. Using fallback image: %s", err.Error()) - ctx = context.WithValue(ctx, imageDataCtxKey, fallback) - } else { + + if fallbackImage == nil { panic(err) } + + if ierr, ok := err.(*imgproxyError); !ok || ierr.Unexpected { + reportError(err, r) + } + + logWarning("Could not load image. Using fallback image: %s", err.Error()) + ctx = context.WithValue(ctx, imageDataCtxKey, fallbackImage) } checkTimeout(ctx) @@ -196,11 +202,3 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) { respondWithImage(ctx, reqID, r, rw, imageData) } - -func loadFallback() (err error) { - fallback, err = getFallbackImageData() - if err != nil { - logError("Could not load fallback data. Fallback images will not be available: %s", err.Error()) - } - return err -}