New release, minor features, bimg upgrade and several fixes (#311)
* feat: add fly.io * feat(readme): fly image width * feat(readme): add html image + badge * fix(docs): try space non-URL encoding * Update README.md * Update README.md * Update README.md * feat(docs): add fly.io info and links * feat(#305, #309, #308, #305. #284) * fix(ci): golinter * fix(Dockerfile): use root folder for go test / ci linter * fix(ci): allow to fail go 1.14 build, still not sure why * fix(ci): bad yaml format * fix(ci): use travis-compatible regex * fix(ci): lets skip go1.14 for now * fix(mod): update dependencies
This commit is contained in:
parent
b2a142adb8
commit
384bb00b15
33
.travis.yml
33
.travis.yml
|
@ -3,21 +3,20 @@ language: go
|
||||||
services:
|
services:
|
||||||
- docker
|
- docker
|
||||||
|
|
||||||
dist: trusty
|
dist: focal
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.12.x
|
- "1.12"
|
||||||
- 1.11.x
|
- "1.13"
|
||||||
|
# - "1.14"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- GOLANG_VERSION="${TRAVIS_GO_VERSION}"
|
- GOLANG_VERSION="${TRAVIS_GO_VERSION}"
|
||||||
- IMAGINARY_VERSION="${TRAVIS_TAG:-dev}"
|
- IMAGINARY_VERSION="${TRAVIS_TAG:-dev}"
|
||||||
matrix:
|
matrix:
|
||||||
- LIBVIPS=8.7.3
|
- LIBVIPS=8.8.4
|
||||||
- LIBVIPS=8.7.4
|
- LIBVIPS=8.9.2
|
||||||
- LIBVIPS=8.8.0
|
|
||||||
- LIBVIPS=8.8.1
|
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- docker pull h2non/imaginary:latest || true
|
- docker pull h2non/imaginary:latest || true
|
||||||
|
@ -28,14 +27,14 @@ install:
|
||||||
script:
|
script:
|
||||||
- docker build --pull --cache-from h2non/imaginary:latest --build-arg GOLANG_VERSION="${GOLANG_VERSION%.x}" --build-arg LIBVIPS_VERSION="${LIBVIPS}" --build-arg IMAGINARY_VERSION="${IMAGINARY_VERSION#v}" --tag h2non/imaginary:${IMAGINARY_VERSION} .
|
- docker build --pull --cache-from h2non/imaginary:latest --build-arg GOLANG_VERSION="${GOLANG_VERSION%.x}" --build-arg LIBVIPS_VERSION="${LIBVIPS}" --build-arg IMAGINARY_VERSION="${IMAGINARY_VERSION#v}" --tag h2non/imaginary:${IMAGINARY_VERSION} .
|
||||||
|
|
||||||
# before_deploy:
|
before_deploy:
|
||||||
# - docker login -u "$REGISTRY_USER" -p "$REGISTRY_PASS"
|
- docker login -u "$DOCKER_LOGIN" -p "$DOCKER_PWD"
|
||||||
|
|
||||||
# deploy:
|
deploy:
|
||||||
# provider: script
|
provider: script
|
||||||
# script:
|
script:
|
||||||
# - docker tag h2non/imaginary:${IMAGINARY_VERSION} latest
|
- docker tag h2non/imaginary:${IMAGINARY_VERSION} h2non/imaginary:latest
|
||||||
# - docker push h2non/imaginary:${IMAGINARY_VERSION}
|
- docker push h2non/imaginary:${IMAGINARY_VERSION}
|
||||||
# - docker push h2non/imaginary:latest
|
- docker push h2non/imaginary:latest
|
||||||
# on:
|
on:
|
||||||
# condition: "${TRAVIS_TAG} =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$"
|
condition: "${TRAVIS_TAG} =~ ^v([0-9]+).([0-9]+).([0-9]+)$ AND env(GOLANG_VERSION) = 1.13"
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
ARG GOLANG_VERSION=1.13.7
|
ARG GOLANG_VERSION=1.14
|
||||||
FROM golang:${GOLANG_VERSION} as builder
|
FROM golang:${GOLANG_VERSION} as builder
|
||||||
|
|
||||||
ARG IMAGINARY_VERSION=dev
|
ARG IMAGINARY_VERSION=dev
|
||||||
ARG LIBVIPS_VERSION=8.9.1
|
ARG LIBVIPS_VERSION=8.9.2
|
||||||
ARG GOLANGCILINT_VERSION=1.23.3
|
ARG GOLANGCILINT_VERSION=1.23.3
|
||||||
|
|
||||||
# Installs libvips + required libraries
|
# Installs libvips + required libraries
|
||||||
|
@ -50,8 +50,8 @@ RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Run quality control
|
# Run quality control
|
||||||
RUN go test -test.v -test.race -test.covermode=atomic ./...
|
RUN go test -test.v -test.race -test.covermode=atomic .
|
||||||
RUN golangci-lint run ./...
|
RUN golangci-lint run .
|
||||||
|
|
||||||
# Compile imaginary
|
# Compile imaginary
|
||||||
RUN go build -a \
|
RUN go build -a \
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# imaginary [](https://travis-ci.org/h2non/imaginary) [](https://hub.docker.com/r/h2non/imaginary/) [](https://hub.docker.com/r/h2non/imaginary/) [](https://goreportcard.com/report/h2non/imaginary) [](https://fly.io/launch/github/h2non/imaginary)
|
# imaginary [](https://travis-ci.org/h2non/imaginary) [](https://hub.docker.com/r/h2non/imaginary/) [](https://hub.docker.com/r/h2non/imaginary/) [](https://goreportcard.com/report/h2non/imaginary) [](https://fly.io/launch/github/h2non/imaginary)
|
||||||
|
|
||||||
**[Fast](#benchmarks) HTTP [microservice](http://microservices.io/patterns/microservices.html)** written in Go **for high-level image processing** backed by [bimg](https://github.com/h2non/bimg) and [libvips](https://github.com/jcupitt/libvips). `imaginary` can be used as private or public HTTP service for massive image processing with first-class support for [Docker](#docker) & [Fly.io](#flyio).
|
**[Fast](#benchmarks) HTTP [microservice](http://microservices.io/patterns/microservices.html)** written in Go **for high-level image processing** backed by [bimg](https://github.com/h2non/bimg) and [libvips](https://github.com/jcupitt/libvips). `imaginary` can be used as private or public HTTP service for massive image processing with first-class support for [Docker](#docker) & [Fly.io](#flyio).
|
||||||
It's almost dependency-free and only uses [`net/http`](http://golang.org/pkg/net/http/) native package without additional abstractions for better [performance](#performance).
|
It's almost dependency-free and only uses [`net/http`](http://golang.org/pkg/net/http/) native package without additional abstractions for better [performance](#performance).
|
||||||
|
@ -83,7 +83,7 @@ go get -u github.com/h2non/imaginary
|
||||||
|
|
||||||
Also, be sure you have the latest version of `bimg`:
|
Also, be sure you have the latest version of `bimg`:
|
||||||
```bash
|
```bash
|
||||||
go get -u gopkg.in/h2non/bimg.v1
|
go get -u github.com/h2non/bimg
|
||||||
```
|
```
|
||||||
|
|
||||||
### libvips
|
### libvips
|
||||||
|
@ -553,7 +553,7 @@ Image measures are always in pixels, unless otherwise indicated.
|
||||||
- **url** `string` - Fetch the image from a remote HTTP server. In order to use this you must pass the `-enable-url-source` flag.
|
- **url** `string` - Fetch the image from a remote HTTP server. In order to use this you must pass the `-enable-url-source` flag.
|
||||||
- **colorspace** `string` - Use a custom color space for the output image. Allowed values are: `srgb` or `bw` (black&white)
|
- **colorspace** `string` - Use a custom color space for the output image. Allowed values are: `srgb` or `bw` (black&white)
|
||||||
- **field** `string` - Custom image form field name if using `multipart/form`. Defaults to: `file`
|
- **field** `string` - Custom image form field name if using `multipart/form`. Defaults to: `file`
|
||||||
- **extend** `string` - Extend represents the image extend mode used when the edges of an image are extended. Allowed values are: `black`, `copy`, `mirror`, `white` and `background`. If `background` value is specified, you can define the desired extend RGB color via `background` param, such as `?extend=background&background=250,20,10`. For more info, see [libvips docs](http://www.vips.ecs.soton.ac.uk/supported/8.4/doc/html/libvips/libvips-conversion.html#VIPS-EXTEND-BACKGROUND:CAPS).
|
- **extend** `string` - Extend represents the image extend mode used when the edges of an image are extended. Defaults to `copy`. Allowed values are: `black`, `copy`, `mirror`, `white`, `lastpixel` and `background`. If `background` value is specified, you can define the desired extend RGB color via `background` param, such as `?extend=background&background=250,20,10`. For more info, see [libvips docs](http://www.vips.ecs.soton.ac.uk/supported/8.4/doc/html/libvips/libvips-conversion.html#VIPS-EXTEND-BACKGROUND:CAPS).
|
||||||
- **background** `string` - Background RGB decimal base color to use when flattening transparent PNGs. Example: `255,200,150`
|
- **background** `string` - Background RGB decimal base color to use when flattening transparent PNGs. Example: `255,200,150`
|
||||||
- **sigma** `float` - Size of the gaussian mask to use when blurring an image. Example: `15.0`
|
- **sigma** `float` - Size of the gaussian mask to use when blurring an image. Example: `15.0`
|
||||||
- **minampl** `float` - Minimum amplitude of the gaussian filter to use when blurring an image. Default: Example: `0.5`
|
- **minampl** `float` - Minimum amplitude of the gaussian filter to use when blurring an image. Default: Example: `0.5`
|
||||||
|
|
|
@ -44,7 +44,11 @@ func imageController(o ServerOptions, operation Operation) func(http.ResponseWri
|
||||||
|
|
||||||
buf, err := imageSource.GetImage(req)
|
buf, err := imageSource.GetImage(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorReply(req, w, NewError(err.Error(), BadRequest), o)
|
if xerr, ok := err.(Error); ok {
|
||||||
|
ErrorReply(req, w, xerr, o)
|
||||||
|
} else {
|
||||||
|
ErrorReply(req, w, NewError(err.Error(), http.StatusBadRequest), o)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +104,7 @@ func imageHandler(w http.ResponseWriter, r *http.Request, buf []byte, operation
|
||||||
|
|
||||||
opts, err := buildParamsFromQuery(r.URL.Query())
|
opts, err := buildParamsFromQuery(r.URL.Query())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorReply(r, w, NewError("Error while processing parameters, "+err.Error(), BadRequest), o)
|
ErrorReply(r, w, NewError("Error while processing parameters, "+err.Error(), http.StatusBadRequest), o)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +119,7 @@ func imageHandler(w http.ResponseWriter, r *http.Request, buf []byte, operation
|
||||||
|
|
||||||
image, err := operation.Run(buf, opts)
|
image, err := operation.Run(buf, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorReply(r, w, NewError("Error while processing the image: "+err.Error(), BadRequest), o)
|
ErrorReply(r, w, NewError("Error while processing the image: "+err.Error(), http.StatusBadRequest), o)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
73
error.go
73
error.go
|
@ -9,38 +9,26 @@ import (
|
||||||
"github.com/h2non/bimg"
|
"github.com/h2non/bimg"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
_ uint8 = iota
|
|
||||||
BadRequest
|
|
||||||
NotAllowed
|
|
||||||
Unsupported
|
|
||||||
Unauthorized
|
|
||||||
InternalError
|
|
||||||
NotFound
|
|
||||||
NotImplemented
|
|
||||||
Forbidden
|
|
||||||
NotAcceptable
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNotFound = NewError("not found", NotFound)
|
ErrNotFound = NewError("Not found", http.StatusNotFound)
|
||||||
ErrInvalidAPIKey = NewError("invalid or missing API key", Unauthorized)
|
ErrInvalidAPIKey = NewError("Invalid or missing API key", http.StatusUnauthorized)
|
||||||
ErrMethodNotAllowed = NewError("method not allowed", NotAllowed)
|
ErrMethodNotAllowed = NewError("HTTP method not allowed. Try with a POST or GET method (-enable-url-source flag must be defined)", http.StatusMethodNotAllowed)
|
||||||
ErrUnsupportedMedia = NewError("unsupported media type", Unsupported)
|
ErrGetMethodNotAllowed = NewError("GET method not allowed. Make sure remote URL source is enabled by using the flag: -enable-url-source", http.StatusMethodNotAllowed)
|
||||||
ErrOutputFormat = NewError("unsupported output image format", BadRequest)
|
ErrUnsupportedMedia = NewError("Unsupported media type", http.StatusNotAcceptable)
|
||||||
ErrEmptyBody = NewError("empty image", BadRequest)
|
ErrOutputFormat = NewError("Unsupported output image format", http.StatusBadRequest)
|
||||||
ErrMissingParamFile = NewError("missing required param: file", BadRequest)
|
ErrEmptyBody = NewError("Empty or unreadable image", http.StatusBadRequest)
|
||||||
ErrInvalidFilePath = NewError("invalid file path", BadRequest)
|
ErrMissingParamFile = NewError("Missing required param: file", http.StatusBadRequest)
|
||||||
ErrInvalidImageURL = NewError("invalid image URL", BadRequest)
|
ErrInvalidFilePath = NewError("Invalid file path", http.StatusBadRequest)
|
||||||
ErrMissingImageSource = NewError("cannot process the image due to missing or invalid params", BadRequest)
|
ErrInvalidImageURL = NewError("Unvalid image URL", http.StatusBadRequest)
|
||||||
ErrNotImplemented = NewError("not implemented endpoint", NotImplemented)
|
ErrMissingImageSource = NewError("Cannot process the image due to missing or invalid params", http.StatusBadRequest)
|
||||||
ErrInvalidURLSignature = NewError("invalid URL signature", BadRequest)
|
ErrNotImplemented = NewError("Not implemented endpoint", http.StatusNotImplemented)
|
||||||
ErrURLSignatureMismatch = NewError("URL signature mismatch", Forbidden)
|
ErrInvalidURLSignature = NewError("Invalid URL signature", http.StatusBadRequest)
|
||||||
|
ErrURLSignatureMismatch = NewError("URL signature mismatch", http.StatusForbidden)
|
||||||
)
|
)
|
||||||
|
|
||||||
type Error struct {
|
type Error struct {
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
Code uint8 `json:"code"`
|
Code int `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Error) JSON() []byte {
|
func (e Error) JSON() []byte {
|
||||||
|
@ -53,34 +41,21 @@ func (e Error) Error() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Error) HTTPCode() int {
|
func (e Error) HTTPCode() int {
|
||||||
var codes = map[uint8]int{
|
if e.Code >= 400 && e.Code <= 511 {
|
||||||
BadRequest: http.StatusBadRequest,
|
return e.Code
|
||||||
NotAllowed: http.StatusMethodNotAllowed,
|
|
||||||
Unsupported: http.StatusUnsupportedMediaType,
|
|
||||||
InternalError: http.StatusInternalServerError,
|
|
||||||
Unauthorized: http.StatusUnauthorized,
|
|
||||||
NotFound: http.StatusNotFound,
|
|
||||||
NotImplemented: http.StatusNotImplemented,
|
|
||||||
Forbidden: http.StatusForbidden,
|
|
||||||
NotAcceptable: http.StatusNotAcceptable,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, ok := codes[e.Code]; ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.StatusServiceUnavailable
|
return http.StatusServiceUnavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewError(err string, code uint8) Error {
|
func NewError(err string, code int) Error {
|
||||||
err = strings.Replace(err, "\n", "", -1)
|
err = strings.Replace(err, "\n", "", -1)
|
||||||
return Error{err, code}
|
return Error{Message: err, Code: code}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendErrorResponse(w http.ResponseWriter, httpStatusCode int, imaginaryErrorCode uint8, err error) {
|
func sendErrorResponse(w http.ResponseWriter, httpStatusCode int, err error) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(httpStatusCode)
|
w.WriteHeader(httpStatusCode)
|
||||||
_, _ = w.Write([]byte(fmt.Sprintf("{\"error\":\"%s\", \"code\": %d}", err.Error(), imaginaryErrorCode)))
|
_, _ = w.Write([]byte(fmt.Sprintf("{\"error\":\"%s\", \"status\": %d}", err.Error(), httpStatusCode)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func replyWithPlaceholder(req *http.Request, w http.ResponseWriter, errCaller Error, o ServerOptions) error {
|
func replyWithPlaceholder(req *http.Request, w http.ResponseWriter, errCaller Error, o ServerOptions) error {
|
||||||
|
@ -94,20 +69,20 @@ func replyWithPlaceholder(req *http.Request, w http.ResponseWriter, errCaller Er
|
||||||
|
|
||||||
bimgOptions.Width, err = parseInt(req.URL.Query().Get("width"))
|
bimgOptions.Width, err = parseInt(req.URL.Query().Get("width"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendErrorResponse(w, http.StatusBadRequest, BadRequest, err)
|
sendErrorResponse(w, http.StatusBadRequest, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
bimgOptions.Height, err = parseInt(req.URL.Query().Get("height"))
|
bimgOptions.Height, err = parseInt(req.URL.Query().Get("height"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendErrorResponse(w, http.StatusBadRequest, BadRequest, err)
|
sendErrorResponse(w, http.StatusBadRequest, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize placeholder to expected output
|
// Resize placeholder to expected output
|
||||||
buf, err := bimg.Resize(o.PlaceholderImage, bimgOptions)
|
buf, err := bimg.Resize(o.PlaceholderImage, bimgOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendErrorResponse(w, http.StatusBadRequest, BadRequest, err)
|
sendErrorResponse(w, http.StatusBadRequest, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,23 +2,23 @@ package main
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestError(t *testing.T) {
|
func TestDefaultError(t *testing.T) {
|
||||||
err := NewError("oops!\n\n", 1)
|
err := NewError("oops!\n\n", 503)
|
||||||
|
|
||||||
if err.Error() != "oops!" {
|
if err.Error() != "oops!" {
|
||||||
t.Fatal("Invalid error message")
|
t.Fatal("Invalid error message")
|
||||||
}
|
}
|
||||||
if err.Code != 1 {
|
if err.Code != 503 {
|
||||||
t.Fatal("Invalid error code")
|
t.Fatal("Invalid error code")
|
||||||
}
|
}
|
||||||
|
|
||||||
code := err.HTTPCode()
|
code := err.HTTPCode()
|
||||||
if code != 400 {
|
if code != 503 {
|
||||||
t.Fatalf("Invalid HTTP error status: %d", code)
|
t.Fatalf("Invalid HTTP error status: %d", code)
|
||||||
}
|
}
|
||||||
|
|
||||||
json := string(err.JSON())
|
json := string(err.JSON())
|
||||||
if json != "{\"message\":\"oops!\",\"code\":1}" {
|
if json != "{\"message\":\"oops!\",\"status\":503}" {
|
||||||
t.Fatalf("Invalid JSON output: %s", json)
|
t.Fatalf("Invalid JSON output: %s", json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -6,7 +6,7 @@ require (
|
||||||
github.com/garyburd/redigo v1.6.0 // indirect
|
github.com/garyburd/redigo v1.6.0 // indirect
|
||||||
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad // indirect
|
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad // indirect
|
||||||
github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3
|
github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3
|
||||||
github.com/h2non/bimg v1.0.20-0.20200405220655-daafbf6d972d
|
github.com/h2non/bimg v1.1.0
|
||||||
github.com/h2non/filetype v1.0.12
|
github.com/h2non/filetype v1.1.0
|
||||||
gopkg.in/throttled/throttled.v2 v2.0.3
|
gopkg.in/throttled/throttled.v2 v2.0.3
|
||||||
)
|
)
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -1,12 +1,10 @@
|
||||||
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
|
|
||||||
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||||
|
github.com/h2non/bimg v1.1.0/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8=
|
||||||
|
github.com/h2non/filetype v1.1.0 h1:Or/gjocJrJRNK/Cri/TDEKFjAR+cfG6eK65NGYB6gBA=
|
||||||
|
github.com/h2non/filetype v1.1.0/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
||||||
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad h1:eMxs9EL0PvIGS9TTtxg4R+JxuPGav82J8rA+GFnY7po=
|
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad h1:eMxs9EL0PvIGS9TTtxg4R+JxuPGav82J8rA+GFnY7po=
|
||||||
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3 h1:86ukAHRTa2CXdBnWJHcjjPPGTyLGEF488OFRsbBAuFs=
|
github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3 h1:86ukAHRTa2CXdBnWJHcjjPPGTyLGEF488OFRsbBAuFs=
|
||||||
github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||||
github.com/h2non/bimg v1.0.20-0.20200405220655-daafbf6d972d h1:AhGp7Xiew8DAYGJ1MojrE11chxHVOv4NvE/y5i2IvBI=
|
|
||||||
github.com/h2non/bimg v1.0.20-0.20200405220655-daafbf6d972d/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8=
|
|
||||||
github.com/h2non/filetype v1.0.12 h1:yHCsIe0y2cvbDARtJhGBTD2ecvqMSTvlIcph9En/Zao=
|
|
||||||
github.com/h2non/filetype v1.0.12/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
|
||||||
gopkg.in/throttled/throttled.v2 v2.0.3 h1:PGm7nfjjexecEyI2knw1akeLcrjzqxuYSU9a04R8rfU=
|
gopkg.in/throttled/throttled.v2 v2.0.3 h1:PGm7nfjjexecEyI2knw1akeLcrjzqxuYSU9a04R8rfU=
|
||||||
gopkg.in/throttled/throttled.v2 v2.0.3/go.mod h1:L4cTNZO77XKEXtn8HNFRCMNGZPtRRKAhyuJBSvK/T90=
|
gopkg.in/throttled/throttled.v2 v2.0.3/go.mod h1:L4cTNZO77XKEXtn8HNFRCMNGZPtRRKAhyuJBSvK/T90=
|
||||||
|
|
44
image.go
44
image.go
|
@ -65,7 +65,7 @@ func Info(buf []byte, o ImageOptions) (Image, error) {
|
||||||
|
|
||||||
meta, err := bimg.Metadata(buf)
|
meta, err := bimg.Metadata(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return image, NewError("Cannot retrieve image metadata: %s"+err.Error(), BadRequest)
|
return image, NewError("Cannot retrieve image metadata: %s"+err.Error(), http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
info := ImageInfo{
|
info := ImageInfo{
|
||||||
|
@ -87,7 +87,7 @@ func Info(buf []byte, o ImageOptions) (Image, error) {
|
||||||
|
|
||||||
func Resize(buf []byte, o ImageOptions) (Image, error) {
|
func Resize(buf []byte, o ImageOptions) (Image, error) {
|
||||||
if o.Width == 0 && o.Height == 0 {
|
if o.Width == 0 && o.Height == 0 {
|
||||||
return Image{}, NewError("Missing required param: height or width", BadRequest)
|
return Image{}, NewError("Missing required param: height or width", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := BimgOptions(o)
|
opts := BimgOptions(o)
|
||||||
|
@ -102,7 +102,7 @@ func Resize(buf []byte, o ImageOptions) (Image, error) {
|
||||||
|
|
||||||
func Fit(buf []byte, o ImageOptions) (Image, error) {
|
func Fit(buf []byte, o ImageOptions) (Image, error) {
|
||||||
if o.Width == 0 || o.Height == 0 {
|
if o.Width == 0 || o.Height == 0 {
|
||||||
return Image{}, NewError("Missing required params: height, width", BadRequest)
|
return Image{}, NewError("Missing required params: height, width", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata, err := bimg.Metadata(buf)
|
metadata, err := bimg.Metadata(buf)
|
||||||
|
@ -113,7 +113,7 @@ func Fit(buf []byte, o ImageOptions) (Image, error) {
|
||||||
dims := metadata.Size
|
dims := metadata.Size
|
||||||
|
|
||||||
if dims.Width == 0 || dims.Height == 0 {
|
if dims.Width == 0 || dims.Height == 0 {
|
||||||
return Image{}, NewError("Width or height of requested image is zero", NotAcceptable)
|
return Image{}, NewError("Width or height of requested image is zero", http.StatusNotAcceptable)
|
||||||
}
|
}
|
||||||
|
|
||||||
// metadata.Orientation
|
// metadata.Orientation
|
||||||
|
@ -165,7 +165,7 @@ func calculateDestinationFitDimension(imageWidth, imageHeight, fitWidth, fitHeig
|
||||||
|
|
||||||
func Enlarge(buf []byte, o ImageOptions) (Image, error) {
|
func Enlarge(buf []byte, o ImageOptions) (Image, error) {
|
||||||
if o.Width == 0 || o.Height == 0 {
|
if o.Width == 0 || o.Height == 0 {
|
||||||
return Image{}, NewError("Missing required params: height, width", BadRequest)
|
return Image{}, NewError("Missing required params: height, width", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := BimgOptions(o)
|
opts := BimgOptions(o)
|
||||||
|
@ -179,7 +179,7 @@ func Enlarge(buf []byte, o ImageOptions) (Image, error) {
|
||||||
|
|
||||||
func Extract(buf []byte, o ImageOptions) (Image, error) {
|
func Extract(buf []byte, o ImageOptions) (Image, error) {
|
||||||
if o.AreaWidth == 0 || o.AreaHeight == 0 {
|
if o.AreaWidth == 0 || o.AreaHeight == 0 {
|
||||||
return Image{}, NewError("Missing required params: areawidth or areaheight", BadRequest)
|
return Image{}, NewError("Missing required params: areawidth or areaheight", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := BimgOptions(o)
|
opts := BimgOptions(o)
|
||||||
|
@ -193,7 +193,7 @@ func Extract(buf []byte, o ImageOptions) (Image, error) {
|
||||||
|
|
||||||
func Crop(buf []byte, o ImageOptions) (Image, error) {
|
func Crop(buf []byte, o ImageOptions) (Image, error) {
|
||||||
if o.Width == 0 && o.Height == 0 {
|
if o.Width == 0 && o.Height == 0 {
|
||||||
return Image{}, NewError("Missing required param: height or width", BadRequest)
|
return Image{}, NewError("Missing required param: height or width", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := BimgOptions(o)
|
opts := BimgOptions(o)
|
||||||
|
@ -203,7 +203,7 @@ func Crop(buf []byte, o ImageOptions) (Image, error) {
|
||||||
|
|
||||||
func SmartCrop(buf []byte, o ImageOptions) (Image, error) {
|
func SmartCrop(buf []byte, o ImageOptions) (Image, error) {
|
||||||
if o.Width == 0 && o.Height == 0 {
|
if o.Width == 0 && o.Height == 0 {
|
||||||
return Image{}, NewError("Missing required param: height or width", BadRequest)
|
return Image{}, NewError("Missing required param: height or width", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := BimgOptions(o)
|
opts := BimgOptions(o)
|
||||||
|
@ -214,7 +214,7 @@ func SmartCrop(buf []byte, o ImageOptions) (Image, error) {
|
||||||
|
|
||||||
func Rotate(buf []byte, o ImageOptions) (Image, error) {
|
func Rotate(buf []byte, o ImageOptions) (Image, error) {
|
||||||
if o.Rotate == 0 {
|
if o.Rotate == 0 {
|
||||||
return Image{}, NewError("Missing required param: rotate", BadRequest)
|
return Image{}, NewError("Missing required param: rotate", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := BimgOptions(o)
|
opts := BimgOptions(o)
|
||||||
|
@ -235,7 +235,7 @@ func Flop(buf []byte, o ImageOptions) (Image, error) {
|
||||||
|
|
||||||
func Thumbnail(buf []byte, o ImageOptions) (Image, error) {
|
func Thumbnail(buf []byte, o ImageOptions) (Image, error) {
|
||||||
if o.Width == 0 && o.Height == 0 {
|
if o.Width == 0 && o.Height == 0 {
|
||||||
return Image{}, NewError("Missing required params: width or height", BadRequest)
|
return Image{}, NewError("Missing required params: width or height", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Process(buf, BimgOptions(o))
|
return Process(buf, BimgOptions(o))
|
||||||
|
@ -243,14 +243,14 @@ func Thumbnail(buf []byte, o ImageOptions) (Image, error) {
|
||||||
|
|
||||||
func Zoom(buf []byte, o ImageOptions) (Image, error) {
|
func Zoom(buf []byte, o ImageOptions) (Image, error) {
|
||||||
if o.Factor == 0 {
|
if o.Factor == 0 {
|
||||||
return Image{}, NewError("Missing required param: factor", BadRequest)
|
return Image{}, NewError("Missing required param: factor", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := BimgOptions(o)
|
opts := BimgOptions(o)
|
||||||
|
|
||||||
if o.Top > 0 || o.Left > 0 {
|
if o.Top > 0 || o.Left > 0 {
|
||||||
if o.AreaWidth == 0 && o.AreaHeight == 0 {
|
if o.AreaWidth == 0 && o.AreaHeight == 0 {
|
||||||
return Image{}, NewError("Missing required params: areawidth, areaheight", BadRequest)
|
return Image{}, NewError("Missing required params: areawidth, areaheight", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.Top = o.Top
|
opts.Top = o.Top
|
||||||
|
@ -269,10 +269,10 @@ func Zoom(buf []byte, o ImageOptions) (Image, error) {
|
||||||
|
|
||||||
func Convert(buf []byte, o ImageOptions) (Image, error) {
|
func Convert(buf []byte, o ImageOptions) (Image, error) {
|
||||||
if o.Type == "" {
|
if o.Type == "" {
|
||||||
return Image{}, NewError("Missing required param: type", BadRequest)
|
return Image{}, NewError("Missing required param: type", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
if ImageType(o.Type) == bimg.UNKNOWN {
|
if ImageType(o.Type) == bimg.UNKNOWN {
|
||||||
return Image{}, NewError("Invalid image type: "+o.Type, BadRequest)
|
return Image{}, NewError("Invalid image type: "+o.Type, http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
opts := BimgOptions(o)
|
opts := BimgOptions(o)
|
||||||
|
|
||||||
|
@ -281,7 +281,7 @@ func Convert(buf []byte, o ImageOptions) (Image, error) {
|
||||||
|
|
||||||
func Watermark(buf []byte, o ImageOptions) (Image, error) {
|
func Watermark(buf []byte, o ImageOptions) (Image, error) {
|
||||||
if o.Text == "" {
|
if o.Text == "" {
|
||||||
return Image{}, NewError("Missing required param: text", BadRequest)
|
return Image{}, NewError("Missing required param: text", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := BimgOptions(o)
|
opts := BimgOptions(o)
|
||||||
|
@ -302,11 +302,11 @@ func Watermark(buf []byte, o ImageOptions) (Image, error) {
|
||||||
|
|
||||||
func WatermarkImage(buf []byte, o ImageOptions) (Image, error) {
|
func WatermarkImage(buf []byte, o ImageOptions) (Image, error) {
|
||||||
if o.Image == "" {
|
if o.Image == "" {
|
||||||
return Image{}, NewError("Missing required param: image", BadRequest)
|
return Image{}, NewError("Missing required param: image", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
response, err := http.Get(o.Image)
|
response, err := http.Get(o.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Image{}, NewError(fmt.Sprintf("Unable to retrieve watermark image. %s", o.Image), BadRequest)
|
return Image{}, NewError(fmt.Sprintf("Unable to retrieve watermark image. %s", o.Image), http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = response.Body.Close()
|
_ = response.Body.Close()
|
||||||
|
@ -322,7 +322,7 @@ func WatermarkImage(buf []byte, o ImageOptions) (Image, error) {
|
||||||
errMessage = fmt.Sprintf("%s. %s", errMessage, err.Error())
|
errMessage = fmt.Sprintf("%s. %s", errMessage, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return Image{}, NewError(errMessage, BadRequest)
|
return Image{}, NewError(errMessage, http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := BimgOptions(o)
|
opts := BimgOptions(o)
|
||||||
|
@ -336,7 +336,7 @@ func WatermarkImage(buf []byte, o ImageOptions) (Image, error) {
|
||||||
|
|
||||||
func GaussianBlur(buf []byte, o ImageOptions) (Image, error) {
|
func GaussianBlur(buf []byte, o ImageOptions) (Image, error) {
|
||||||
if o.Sigma == 0 && o.MinAmpl == 0 {
|
if o.Sigma == 0 && o.MinAmpl == 0 {
|
||||||
return Image{}, NewError("Missing required param: sigma or minampl", BadRequest)
|
return Image{}, NewError("Missing required param: sigma or minampl", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
opts := BimgOptions(o)
|
opts := BimgOptions(o)
|
||||||
return Process(buf, opts)
|
return Process(buf, opts)
|
||||||
|
@ -344,10 +344,10 @@ func GaussianBlur(buf []byte, o ImageOptions) (Image, error) {
|
||||||
|
|
||||||
func Pipeline(buf []byte, o ImageOptions) (Image, error) {
|
func Pipeline(buf []byte, o ImageOptions) (Image, error) {
|
||||||
if len(o.Operations) == 0 {
|
if len(o.Operations) == 0 {
|
||||||
return Image{}, NewError("Missing or invalid pipeline operations JSON", BadRequest)
|
return Image{}, NewError("Missing or invalid pipeline operations JSON", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
if len(o.Operations) > 10 {
|
if len(o.Operations) > 10 {
|
||||||
return Image{}, NewError("Maximum allowed pipeline operations exceeded", BadRequest)
|
return Image{}, NewError("Maximum allowed pipeline operations exceeded", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate and built operations
|
// Validate and built operations
|
||||||
|
@ -355,7 +355,7 @@ func Pipeline(buf []byte, o ImageOptions) (Image, error) {
|
||||||
// Validate supported operation name
|
// Validate supported operation name
|
||||||
var exists bool
|
var exists bool
|
||||||
if operation.Operation, exists = OperationsMap[operation.Name]; !exists {
|
if operation.Operation, exists = OperationsMap[operation.Name]; !exists {
|
||||||
return Image{}, NewError(fmt.Sprintf("Unsupported operation name: %s", operation.Name), BadRequest)
|
return Image{}, NewError(fmt.Sprintf("Unsupported operation name: %s", operation.Name), http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and construct operation options
|
// Parse and construct operation options
|
||||||
|
|
|
@ -114,7 +114,7 @@ type URLSignature struct {
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
_, _ = fmt.Fprint(os.Stderr, fmt.Sprintf(usage, Version, runtime.NumCPU()))
|
_, _ = fmt.Fprint(os.Stderr, usage, Version, runtime.NumCPU())
|
||||||
}
|
}
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/cors"
|
|
||||||
"github.com/h2non/bimg"
|
"github.com/h2non/bimg"
|
||||||
|
"github.com/rs/cors"
|
||||||
"gopkg.in/throttled/throttled.v2"
|
"gopkg.in/throttled/throttled.v2"
|
||||||
"gopkg.in/throttled/throttled.v2/store/memstore"
|
"gopkg.in/throttled/throttled.v2/store/memstore"
|
||||||
)
|
)
|
||||||
|
@ -105,7 +105,7 @@ func validateImage(next http.Handler, o ServerOptions) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Method == http.MethodGet && o.Mount == "" && !o.EnableURLSource {
|
if r.Method == http.MethodGet && o.Mount == "" && !o.EnableURLSource {
|
||||||
ErrorReply(r, w, ErrMethodNotAllowed, o)
|
ErrorReply(r, w, ErrGetMethodNotAllowed, o)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
params.go
16
params.go
|
@ -344,9 +344,11 @@ func coerceInterlace(io *ImageOptions, param interface{}) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildParamsFromOperation(op PipelineOperation) (ImageOptions, error) {
|
func buildParamsFromOperation(op PipelineOperation) (ImageOptions, error) {
|
||||||
|
|
||||||
var options ImageOptions
|
var options ImageOptions
|
||||||
|
|
||||||
|
// Apply defaults
|
||||||
|
options.Extend = bimg.ExtendCopy
|
||||||
|
|
||||||
for key, value := range op.Params {
|
for key, value := range op.Params {
|
||||||
fn, ok := paramTypeCoercions[key]
|
fn, ok := paramTypeCoercions[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -366,6 +368,9 @@ func buildParamsFromOperation(op PipelineOperation) (ImageOptions, error) {
|
||||||
func buildParamsFromQuery(query url.Values) (ImageOptions, error) {
|
func buildParamsFromQuery(query url.Values) (ImageOptions, error) {
|
||||||
var options ImageOptions
|
var options ImageOptions
|
||||||
|
|
||||||
|
// Apply defaults
|
||||||
|
options.Extend = bimg.ExtendCopy
|
||||||
|
|
||||||
// Extract only known parameters
|
// Extract only known parameters
|
||||||
for key := range query {
|
for key := range query {
|
||||||
fn, ok := paramTypeCoercions[key]
|
fn, ok := paramTypeCoercions[key]
|
||||||
|
@ -448,8 +453,8 @@ func parseExtendMode(val string) bimg.Extend {
|
||||||
if val == "white" {
|
if val == "white" {
|
||||||
return bimg.ExtendWhite
|
return bimg.ExtendWhite
|
||||||
}
|
}
|
||||||
if val == "copy" {
|
if val == "black" {
|
||||||
return bimg.ExtendCopy
|
return bimg.ExtendBlack
|
||||||
}
|
}
|
||||||
if val == "mirror" {
|
if val == "mirror" {
|
||||||
return bimg.ExtendMirror
|
return bimg.ExtendMirror
|
||||||
|
@ -457,7 +462,10 @@ func parseExtendMode(val string) bimg.Extend {
|
||||||
if val == "background" {
|
if val == "background" {
|
||||||
return bimg.ExtendBackground
|
return bimg.ExtendBackground
|
||||||
}
|
}
|
||||||
return bimg.ExtendBlack
|
if val == "lastpixel" {
|
||||||
|
return bimg.ExtendLast
|
||||||
|
}
|
||||||
|
return bimg.ExtendCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseGravity(val string) bimg.Gravity {
|
func parseGravity(val string) bimg.Gravity {
|
||||||
|
|
|
@ -141,10 +141,11 @@ func TestParseExtend(t *testing.T) {
|
||||||
{"black", bimg.ExtendBlack},
|
{"black", bimg.ExtendBlack},
|
||||||
{"copy", bimg.ExtendCopy},
|
{"copy", bimg.ExtendCopy},
|
||||||
{"mirror", bimg.ExtendMirror},
|
{"mirror", bimg.ExtendMirror},
|
||||||
|
{"lastpixel", bimg.ExtendLast},
|
||||||
{"background", bimg.ExtendBackground},
|
{"background", bimg.ExtendBackground},
|
||||||
{" BACKGROUND ", bimg.ExtendBackground},
|
{" BACKGROUND ", bimg.ExtendBackground},
|
||||||
{"invalid", bimg.ExtendBlack},
|
{"invalid", bimg.ExtendCopy},
|
||||||
{"", bimg.ExtendBlack},
|
{"", bimg.ExtendCopy},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, extend := range cases {
|
for _, extend := range cases {
|
||||||
|
|
|
@ -41,11 +41,11 @@ func (s *HTTPImageSource) fetchImage(url *url.URL, ireq *http.Request) ([]byte,
|
||||||
req := newHTTPRequest(s, ireq, http.MethodHead, url)
|
req := newHTTPRequest(s, ireq, http.MethodHead, url)
|
||||||
res, err := http.DefaultClient.Do(req)
|
res, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error fetching image http headers: %v", err)
|
return nil, fmt.Errorf("error fetching remote http image headers: %v", err)
|
||||||
}
|
}
|
||||||
_ = res.Body.Close()
|
_ = res.Body.Close()
|
||||||
if res.StatusCode < 200 && res.StatusCode > 206 {
|
if res.StatusCode < 200 && res.StatusCode > 206 {
|
||||||
return nil, fmt.Errorf("error fetching image http headers: (status=%d) (url=%s)", res.StatusCode, req.URL.String())
|
return nil, NewError(fmt.Sprintf("error fetching remote http image headers: (status=%d) (url=%s)", res.StatusCode, req.URL.String()), res.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
contentLength, _ := strconv.Atoi(res.Header.Get("Content-Length"))
|
contentLength, _ := strconv.Atoi(res.Header.Get("Content-Length"))
|
||||||
|
@ -58,11 +58,11 @@ func (s *HTTPImageSource) fetchImage(url *url.URL, ireq *http.Request) ([]byte,
|
||||||
req := newHTTPRequest(s, ireq, http.MethodGet, url)
|
req := newHTTPRequest(s, ireq, http.MethodGet, url)
|
||||||
res, err := http.DefaultClient.Do(req)
|
res, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error downloading image: %v", err)
|
return nil, fmt.Errorf("error fetching remote http image: %v", err)
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != 200 {
|
if res.StatusCode != 200 {
|
||||||
return nil, fmt.Errorf("error downloading image: (status=%d) (url=%s)", res.StatusCode, req.URL.String())
|
return nil, NewError(fmt.Sprintf("error fetching remote http image: (status=%d) (url=%s)", res.StatusCode, req.URL.String()), res.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the body
|
// Read the body
|
||||||
|
|
Loading…
Reference in New Issue