mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-10-09 19:52:30 +02:00
Etag passthough
This commit is contained in:
47
etag.go
47
etag.go
@@ -1,47 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"hash"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v2/imagedata"
|
|
||||||
"github.com/imgproxy/imgproxy/v2/options"
|
|
||||||
"github.com/imgproxy/imgproxy/v2/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
type eTagCalc struct {
|
|
||||||
hash hash.Hash
|
|
||||||
enc *json.Encoder
|
|
||||||
}
|
|
||||||
|
|
||||||
var eTagCalcPool = sync.Pool{
|
|
||||||
New: func() interface{} {
|
|
||||||
h := sha256.New()
|
|
||||||
|
|
||||||
enc := json.NewEncoder(h)
|
|
||||||
enc.SetEscapeHTML(false)
|
|
||||||
enc.SetIndent("", "")
|
|
||||||
|
|
||||||
return &eTagCalc{h, enc}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func calcETag(ctx context.Context, imgdata *imagedata.ImageData, po *options.ProcessingOptions) string {
|
|
||||||
c := eTagCalcPool.Get().(*eTagCalc)
|
|
||||||
defer eTagCalcPool.Put(c)
|
|
||||||
|
|
||||||
c.hash.Reset()
|
|
||||||
c.hash.Write(imgdata.Data)
|
|
||||||
footprint := c.hash.Sum(nil)
|
|
||||||
|
|
||||||
c.hash.Reset()
|
|
||||||
c.hash.Write(footprint)
|
|
||||||
c.hash.Write([]byte(version.Version()))
|
|
||||||
c.enc.Encode(po)
|
|
||||||
|
|
||||||
return hex.EncodeToString(c.hash.Sum(nil))
|
|
||||||
}
|
|
153
etag/etag.go
Normal file
153
etag/etag.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package etag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"net/textproto"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
type eTagCalc struct {
|
||||||
|
hash hash.Hash
|
||||||
|
enc *json.Encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
var eTagCalcPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
h := sha256.New()
|
||||||
|
|
||||||
|
enc := json.NewEncoder(h)
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
enc.SetIndent("", "")
|
||||||
|
|
||||||
|
return &eTagCalc{h, enc}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
poHashActual, poHashExpected string
|
||||||
|
|
||||||
|
imgEtagActual, imgEtagExpected string
|
||||||
|
imgHashActual, imgHashExpected string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ParseExpectedETag(etag string) {
|
||||||
|
// We suuport only a single ETag value
|
||||||
|
if i := strings.IndexByte(etag, ','); i >= 0 {
|
||||||
|
etag = textproto.TrimString(etag[:i])
|
||||||
|
}
|
||||||
|
|
||||||
|
etagLen := len(etag)
|
||||||
|
|
||||||
|
// ETag is empty or invalid
|
||||||
|
if etagLen < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We support strong ETags only
|
||||||
|
if etag[0] != '"' || etag[etagLen-1] != '"' {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove quotes
|
||||||
|
etag = etag[1 : etagLen-1]
|
||||||
|
|
||||||
|
i := strings.Index(etag, "/")
|
||||||
|
if i < 0 || i > etagLen-3 {
|
||||||
|
// Doesn't look like imgproxy ETag
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
poPart, imgPartMark, imgPart := etag[:i], etag[i+1], etag[i+2:]
|
||||||
|
|
||||||
|
switch imgPartMark {
|
||||||
|
case 'R':
|
||||||
|
imgPartDec, err := base64.RawStdEncoding.DecodeString(imgPart)
|
||||||
|
if err == nil {
|
||||||
|
h.imgEtagExpected = string(imgPartDec)
|
||||||
|
}
|
||||||
|
case 'D':
|
||||||
|
h.imgHashExpected = imgPart
|
||||||
|
default:
|
||||||
|
// Unknown image part mark
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.poHashExpected = poPart
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ProcessingOptionsMatch() bool {
|
||||||
|
return h.poHashActual == h.poHashExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) SetActualProcessingOptions(po *options.ProcessingOptions) bool {
|
||||||
|
c := eTagCalcPool.Get().(*eTagCalc)
|
||||||
|
defer eTagCalcPool.Put(c)
|
||||||
|
|
||||||
|
c.hash.Reset()
|
||||||
|
c.hash.Write([]byte(version.Version()))
|
||||||
|
c.enc.Encode(po)
|
||||||
|
|
||||||
|
h.poHashActual = base64.RawURLEncoding.EncodeToString(c.hash.Sum(nil))
|
||||||
|
|
||||||
|
return h.ProcessingOptionsMatch()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ImageEtagExpected() string {
|
||||||
|
return h.imgEtagExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) SetActualImageData(imgdata *imagedata.ImageData) bool {
|
||||||
|
var haveActualImgETag bool
|
||||||
|
h.imgEtagActual, haveActualImgETag = imgdata.Headers["ETag"]
|
||||||
|
haveActualImgETag = haveActualImgETag && len(h.imgEtagActual) > 0
|
||||||
|
|
||||||
|
// Just in case server didn't check ETag properly and returned the same one
|
||||||
|
// as we expected
|
||||||
|
if haveActualImgETag && h.imgEtagExpected == h.imgEtagActual {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
haveExpectedImgHash := len(h.imgHashExpected) != 0
|
||||||
|
|
||||||
|
if !haveActualImgETag || haveExpectedImgHash {
|
||||||
|
c := eTagCalcPool.Get().(*eTagCalc)
|
||||||
|
defer eTagCalcPool.Put(c)
|
||||||
|
|
||||||
|
c.hash.Reset()
|
||||||
|
c.hash.Write(imgdata.Data)
|
||||||
|
|
||||||
|
h.imgHashActual = base64.RawURLEncoding.EncodeToString(c.hash.Sum(nil))
|
||||||
|
|
||||||
|
return haveExpectedImgHash && h.imgHashActual == h.imgHashExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GenerateActualETag() string {
|
||||||
|
return h.generate(h.poHashActual, h.imgEtagActual, h.imgHashActual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GenerateExpectedETag() string {
|
||||||
|
return h.generate(h.poHashExpected, h.imgEtagExpected, h.imgHashExpected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) generate(poHash, imgEtag, imgHash string) string {
|
||||||
|
imgPartMark := 'D'
|
||||||
|
imgPart := imgHash
|
||||||
|
if len(imgEtag) != 0 {
|
||||||
|
imgPartMark = 'R'
|
||||||
|
imgPart = base64.RawURLEncoding.EncodeToString([]byte(imgEtag))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(`"%s/%c%s"`, poHash, imgPartMark, imgPart)
|
||||||
|
}
|
135
etag/etag_test.go
Normal file
135
etag/etag_test.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package etag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/options"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
po = options.NewProcessingOptions()
|
||||||
|
|
||||||
|
imgWithETag = imagedata.ImageData{
|
||||||
|
Data: []byte("Hello Test"),
|
||||||
|
Headers: map[string]string{"ETag": `"loremipsumdolor"`},
|
||||||
|
}
|
||||||
|
imgWithoutETag = imagedata.ImageData{
|
||||||
|
Data: []byte("Hello Test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
etagReq = `"ATeSQpxYMfaZVBSmCh-zpE8682vBUrZ1qxXgQkxtntA/RImxvcmVtaXBzdW1kb2xvciI"`
|
||||||
|
etagData = `"ATeSQpxYMfaZVBSmCh-zpE8682vBUrZ1qxXgQkxtntA/DvyChhMNu_sFX7jrjoyrgQbnFwfoOVv7kzp_Fbs6hQBg"`
|
||||||
|
)
|
||||||
|
|
||||||
|
type EtagTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
h Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) SetupSuite() {
|
||||||
|
logrus.SetOutput(ioutil.Discard)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) TeardownSuite() {
|
||||||
|
logrus.SetOutput(os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) SetupTest() {
|
||||||
|
s.h = Handler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) TestGenerateActualReq() {
|
||||||
|
s.h.SetActualProcessingOptions(po)
|
||||||
|
s.h.SetActualImageData(&imgWithETag)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), etagReq, s.h.GenerateActualETag())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) TestGenerateActualData() {
|
||||||
|
s.h.SetActualProcessingOptions(po)
|
||||||
|
s.h.SetActualImageData(&imgWithoutETag)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), etagData, s.h.GenerateActualETag())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) TestGenerateExpectedReq() {
|
||||||
|
s.h.ParseExpectedETag(etagReq)
|
||||||
|
assert.Equal(s.T(), etagReq, s.h.GenerateExpectedETag())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) TestGenerateExpectedData() {
|
||||||
|
s.h.ParseExpectedETag(etagData)
|
||||||
|
assert.Equal(s.T(), etagData, s.h.GenerateExpectedETag())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) TestProcessingOptionsCheckSuccess() {
|
||||||
|
s.h.ParseExpectedETag(etagReq)
|
||||||
|
|
||||||
|
assert.True(s.T(), s.h.SetActualProcessingOptions(po))
|
||||||
|
assert.True(s.T(), s.h.ProcessingOptionsMatch())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) TestProcessingOptionsCheckFailure() {
|
||||||
|
i := strings.Index(etagReq, "/")
|
||||||
|
wrongEtag := `"wrongpohash` + etagReq[i:]
|
||||||
|
|
||||||
|
s.h.ParseExpectedETag(wrongEtag)
|
||||||
|
|
||||||
|
assert.False(s.T(), s.h.SetActualProcessingOptions(po))
|
||||||
|
assert.False(s.T(), s.h.ProcessingOptionsMatch())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) TestImageETagExpectedPresent() {
|
||||||
|
s.h.ParseExpectedETag(etagReq)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), imgWithETag.Headers["ETag"], s.h.ImageEtagExpected())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) TestImageETagExpectedBlank() {
|
||||||
|
s.h.ParseExpectedETag(etagData)
|
||||||
|
|
||||||
|
assert.Empty(s.T(), s.h.ImageEtagExpected())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) TestImageDataCheckDataToDataSuccess() {
|
||||||
|
s.h.ParseExpectedETag(etagData)
|
||||||
|
assert.True(s.T(), s.h.SetActualImageData(&imgWithoutETag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) TestImageDataCheckDataToDataFailure() {
|
||||||
|
i := strings.Index(etagData, "/")
|
||||||
|
wrongEtag := etagData[:i] + `/Dwrongimghash"`
|
||||||
|
|
||||||
|
s.h.ParseExpectedETag(wrongEtag)
|
||||||
|
assert.False(s.T(), s.h.SetActualImageData(&imgWithoutETag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) TestImageDataCheckDataToReqSuccess() {
|
||||||
|
s.h.ParseExpectedETag(etagData)
|
||||||
|
assert.True(s.T(), s.h.SetActualImageData(&imgWithETag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) TestImageDataCheckDataToReqFailure() {
|
||||||
|
i := strings.Index(etagData, "/")
|
||||||
|
wrongEtag := etagData[:i] + `/Dwrongimghash"`
|
||||||
|
|
||||||
|
s.h.ParseExpectedETag(wrongEtag)
|
||||||
|
assert.False(s.T(), s.h.SetActualImageData(&imgWithETag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EtagTestSuite) TestImageDataCheckReqToDataFailure() {
|
||||||
|
s.h.ParseExpectedETag(etagReq)
|
||||||
|
assert.False(s.T(), s.h.SetActualImageData(&imgWithoutETag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEtag(t *testing.T) {
|
||||||
|
suite.Run(t, new(EtagTestSuite))
|
||||||
|
}
|
@@ -62,6 +62,15 @@ func Wrap(err error, skip int) *Error {
|
|||||||
return NewUnexpected(err.Error(), skip+1)
|
return NewUnexpected(err.Error(), skip+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WrapWithMessage(err error, skip int, msg string) *Error {
|
||||||
|
if ierr, ok := err.(*Error); ok {
|
||||||
|
newErr := *ierr
|
||||||
|
ierr.Message = msg
|
||||||
|
return &newErr
|
||||||
|
}
|
||||||
|
return NewUnexpected(err.Error(), skip+1)
|
||||||
|
}
|
||||||
|
|
||||||
func callers(skip int) []uintptr {
|
func callers(skip int) []uintptr {
|
||||||
stack := make([]uintptr, 10)
|
stack := make([]uintptr, 10)
|
||||||
n := runtime.Callers(skip, stack)
|
n := runtime.Callers(skip, stack)
|
||||||
@@ -78,3 +87,10 @@ func formatStack(stack []uintptr) string {
|
|||||||
|
|
||||||
return strings.Join(lines, "\n")
|
return strings.Join(lines, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StatusCode(err error) int {
|
||||||
|
if ierr, ok := err.(*Error); ok {
|
||||||
|
return ierr.StatusCode
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
@@ -24,10 +24,13 @@ var (
|
|||||||
imageHeadersToStore = []string{
|
imageHeadersToStore = []string{
|
||||||
"Cache-Control",
|
"Cache-Control",
|
||||||
"Expires",
|
"Expires",
|
||||||
|
"ETag",
|
||||||
}
|
}
|
||||||
|
|
||||||
// For tests
|
// For tests
|
||||||
redirectAllRequestsTo string
|
redirectAllRequestsTo string
|
||||||
|
|
||||||
|
ErrNotModified = ierrors.New(http.StatusNotModified, "Not Modified", "Not Modified")
|
||||||
)
|
)
|
||||||
|
|
||||||
const msgSourceImageIsUnreachable = "Source image is unreachable"
|
const msgSourceImageIsUnreachable = "Source image is unreachable"
|
||||||
@@ -81,7 +84,7 @@ func initDownloading() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestImage(imageURL string) (*http.Response, error) {
|
func requestImage(imageURL string, header http.Header) (*http.Response, error) {
|
||||||
req, err := http.NewRequest("GET", imageURL, nil)
|
req, err := http.NewRequest("GET", imageURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ierrors.New(404, err.Error(), msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
|
return nil, ierrors.New(404, err.Error(), msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
|
||||||
@@ -89,29 +92,39 @@ func requestImage(imageURL string) (*http.Response, error) {
|
|||||||
|
|
||||||
req.Header.Set("User-Agent", config.UserAgent)
|
req.Header.Set("User-Agent", config.UserAgent)
|
||||||
|
|
||||||
|
for k, v := range header {
|
||||||
|
if len(v) > 0 {
|
||||||
|
req.Header.Set(k, v[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res, err := downloadClient.Do(req)
|
res, err := downloadClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, ierrors.New(404, checkTimeoutErr(err).Error(), msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
|
return res, ierrors.New(404, checkTimeoutErr(err).Error(), msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if res.StatusCode == http.StatusNotModified {
|
||||||
|
return nil, ErrNotModified
|
||||||
|
}
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
if res.StatusCode != 200 {
|
||||||
body, _ := ioutil.ReadAll(res.Body)
|
body, _ := ioutil.ReadAll(res.Body)
|
||||||
res.Body.Close()
|
res.Body.Close()
|
||||||
|
|
||||||
msg := fmt.Sprintf("Can't download image; Status: %d; %s", res.StatusCode, string(body))
|
msg := fmt.Sprintf("Status: %d; %s", res.StatusCode, string(body))
|
||||||
return res, ierrors.New(404, msg, msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
|
return res, ierrors.New(404, msg, msgSourceImageIsUnreachable).SetUnexpected(config.ReportDownloadingErrors)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func download(imageURL string) (*ImageData, error) {
|
func download(imageURL string, header http.Header) (*ImageData, error) {
|
||||||
// We use this for testing
|
// We use this for testing
|
||||||
if len(redirectAllRequestsTo) > 0 {
|
if len(redirectAllRequestsTo) > 0 {
|
||||||
imageURL = redirectAllRequestsTo
|
imageURL = redirectAllRequestsTo
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := requestImage(imageURL)
|
res, err := requestImage(imageURL, header)
|
||||||
if res != nil {
|
if res != nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
}
|
}
|
||||||
|
@@ -4,11 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v2/config"
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/ierrors"
|
||||||
"github.com/imgproxy/imgproxy/v2/imagetype"
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,7 +70,7 @@ func loadWatermark() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(config.WatermarkURL) > 0 {
|
if len(config.WatermarkURL) > 0 {
|
||||||
Watermark, err = Download(config.WatermarkURL, "watermark")
|
Watermark, err = Download(config.WatermarkURL, "watermark", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +89,7 @@ func loadFallbackImage() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(config.FallbackImageURL) > 0 {
|
if len(config.FallbackImageURL) > 0 {
|
||||||
FallbackImage, err = Download(config.FallbackImageURL, "fallback image")
|
FallbackImage, err = Download(config.FallbackImageURL, "fallback image", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,10 +127,10 @@ func FromFile(path, desc string) (*ImageData, error) {
|
|||||||
return imgdata, nil
|
return imgdata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Download(imageURL, desc string) (*ImageData, error) {
|
func Download(imageURL, desc string, header http.Header) (*ImageData, error) {
|
||||||
imgdata, err := download(imageURL)
|
imgdata, err := download(imageURL, header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Can't download %s: %s", desc, err)
|
return nil, ierrors.WrapWithMessage(err, 1, fmt.Sprintf("Can't download %s: %s", desc, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return imgdata, nil
|
return imgdata, nil
|
||||||
|
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v2/config"
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
"github.com/imgproxy/imgproxy/v2/errorreport"
|
"github.com/imgproxy/imgproxy/v2/errorreport"
|
||||||
|
"github.com/imgproxy/imgproxy/v2/etag"
|
||||||
"github.com/imgproxy/imgproxy/v2/ierrors"
|
"github.com/imgproxy/imgproxy/v2/ierrors"
|
||||||
"github.com/imgproxy/imgproxy/v2/imagedata"
|
"github.com/imgproxy/imgproxy/v2/imagedata"
|
||||||
"github.com/imgproxy/imgproxy/v2/imagetype"
|
"github.com/imgproxy/imgproxy/v2/imagetype"
|
||||||
@@ -113,6 +114,17 @@ func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, res
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func respondWithNotModified(reqID string, r *http.Request, rw http.ResponseWriter, po *options.ProcessingOptions, originURL string) {
|
||||||
|
rw.WriteHeader(304)
|
||||||
|
router.LogResponse(
|
||||||
|
reqID, r, 304, nil,
|
||||||
|
log.Fields{
|
||||||
|
"image_url": originURL,
|
||||||
|
"processing_options": po,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx, timeoutCancel := context.WithTimeout(r.Context(), time.Duration(config.WriteTimeout)*time.Second)
|
ctx, timeoutCancel := context.WithTimeout(r.Context(), time.Duration(config.WriteTimeout)*time.Second)
|
||||||
defer timeoutCancel()
|
defer timeoutCancel()
|
||||||
@@ -162,6 +174,20 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
imgRequestHeader := make(http.Header)
|
||||||
|
|
||||||
|
var etagHandler etag.Handler
|
||||||
|
|
||||||
|
if config.ETagEnabled {
|
||||||
|
etagHandler.ParseExpectedETag(r.Header.Get("If-None-Match"))
|
||||||
|
|
||||||
|
if etagHandler.SetActualProcessingOptions(po) {
|
||||||
|
if imgEtag := etagHandler.ImageEtagExpected(); len(imgEtag) != 0 {
|
||||||
|
imgRequestHeader.Set("If-None-Match", imgEtag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The heavy part start here, so we need to restrict concurrency
|
// The heavy part start here, so we need to restrict concurrency
|
||||||
select {
|
select {
|
||||||
case processingSem <- struct{}{}:
|
case processingSem <- struct{}{}:
|
||||||
@@ -175,21 +201,26 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
originData, err := func() (*imagedata.ImageData, error) {
|
originData, err := func() (*imagedata.ImageData, error) {
|
||||||
defer metrics.StartDownloadingSegment(ctx)()
|
defer metrics.StartDownloadingSegment(ctx)()
|
||||||
return imagedata.Download(imageURL, "source image")
|
return imagedata.Download(imageURL, "source image", imgRequestHeader)
|
||||||
}()
|
}()
|
||||||
if err == nil {
|
switch {
|
||||||
|
case err == nil:
|
||||||
defer originData.Close()
|
defer originData.Close()
|
||||||
} else {
|
case ierrors.StatusCode(err) == http.StatusNotModified:
|
||||||
|
rw.Header().Set("ETag", etagHandler.GenerateExpectedETag())
|
||||||
|
respondWithNotModified(reqID, r, rw, po, imageURL)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
if ierr, ok := err.(*ierrors.Error); !ok || ierr.Unexpected {
|
||||||
|
errorreport.Report(err, r)
|
||||||
|
}
|
||||||
|
|
||||||
metrics.SendError(ctx, "download", err)
|
metrics.SendError(ctx, "download", err)
|
||||||
|
|
||||||
if imagedata.FallbackImage == nil {
|
if imagedata.FallbackImage == nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ierr, ok := err.(*ierrors.Error); !ok || ierr.Unexpected {
|
|
||||||
errorreport.Report(err, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Warningf("Could not load image %s. Using fallback image. %s", imageURL, err.Error())
|
log.Warningf("Could not load image %s. Using fallback image. %s", imageURL, err.Error())
|
||||||
r = r.WithContext(setFallbackImageUsedCtx(r.Context()))
|
r = r.WithContext(setFallbackImageUsedCtx(r.Context()))
|
||||||
originData = imagedata.FallbackImage
|
originData = imagedata.FallbackImage
|
||||||
@@ -198,12 +229,12 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
|||||||
router.CheckTimeout(ctx)
|
router.CheckTimeout(ctx)
|
||||||
|
|
||||||
if config.ETagEnabled && !getFallbackImageUsed(ctx) {
|
if config.ETagEnabled && !getFallbackImageUsed(ctx) {
|
||||||
eTag := calcETag(ctx, originData, po)
|
imgDataMatch := etagHandler.SetActualImageData(originData)
|
||||||
rw.Header().Set("ETag", eTag)
|
|
||||||
|
|
||||||
if eTag == r.Header.Get("If-None-Match") {
|
rw.Header().Set("ETag", etagHandler.GenerateActualETag())
|
||||||
rw.WriteHeader(304)
|
|
||||||
router.LogResponse(reqID, r, 304, nil, log.Fields{"image_url": imageURL})
|
if imgDataMatch && etagHandler.ProcessingOptionsMatch() {
|
||||||
|
respondWithNotModified(reqID, r, rw, po, imageURL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -289,11 +289,10 @@ func (s *ProcessingHandlerTestSuite) TestCacheControlPassthrough() {
|
|||||||
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) {
|
||||||
data := s.readTestFile("test1.png")
|
|
||||||
rw.Header().Set("Cache-Control", "fake-cache-control")
|
rw.Header().Set("Cache-Control", "fake-cache-control")
|
||||||
rw.Header().Set("Expires", "fake-expires")
|
rw.Header().Set("Expires", "fake-expires")
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
rw.Write(data)
|
rw.Write(s.readTestFile("test1.png"))
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
@@ -308,11 +307,10 @@ 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) {
|
||||||
data := s.readTestFile("test1.png")
|
|
||||||
rw.Header().Set("Cache-Control", "fake-cache-control")
|
rw.Header().Set("Cache-Control", "fake-cache-control")
|
||||||
rw.Header().Set("Expires", "fake-expires")
|
rw.Header().Set("Expires", "fake-expires")
|
||||||
rw.WriteHeader(200)
|
rw.WriteHeader(200)
|
||||||
rw.Write(data)
|
rw.Write(s.readTestFile("test1.png"))
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
@@ -323,6 +321,183 @@ func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughDisabled() {
|
|||||||
assert.NotEqual(s.T(), "fake-expires", res.Header.Get("Expires"))
|
assert.NotEqual(s.T(), "fake-expires", res.Header.Get("Expires"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestETagDisabled() {
|
||||||
|
config.ETagEnabled = false
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, res.StatusCode)
|
||||||
|
assert.Empty(s.T(), res.Header.Get("ETag"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestETagReqNoIfNotModified() {
|
||||||
|
config.ETagEnabled = true
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Empty(s.T(), r.Header.Get("If-None-Match"))
|
||||||
|
|
||||||
|
rw.Header().Set("ETag", `"loremipsumdolor"`)
|
||||||
|
rw.WriteHeader(200)
|
||||||
|
rw.Write(s.readTestFile("test1.png"))
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, res.StatusCode)
|
||||||
|
assert.Equal(
|
||||||
|
s.T(),
|
||||||
|
`"1Uuny6YTSUO08MMVZyantp9oaoOcsaYibQoNbBTJEzk/RImxvcmVtaXBzdW1kb2xvciI"`,
|
||||||
|
res.Header.Get("ETag"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestETagDataNoIfNotModified() {
|
||||||
|
config.ETagEnabled = true
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Empty(s.T(), r.Header.Get("If-None-Match"))
|
||||||
|
|
||||||
|
rw.WriteHeader(200)
|
||||||
|
rw.Write(s.readTestFile("test1.png"))
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, res.StatusCode)
|
||||||
|
assert.Equal(
|
||||||
|
s.T(),
|
||||||
|
`"1Uuny6YTSUO08MMVZyantp9oaoOcsaYibQoNbBTJEzk/Dl29MNvkqdLEqPyFvMlh_NlPbRrsC0GDG_AUlmMdX6HA"`,
|
||||||
|
res.Header.Get("ETag"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestETagReqMatch() {
|
||||||
|
config.ETagEnabled = true
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(s.T(), `"loremipsumdolor"`, r.Header.Get("If-None-Match"))
|
||||||
|
|
||||||
|
rw.WriteHeader(304)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
etag := `"1Uuny6YTSUO08MMVZyantp9oaoOcsaYibQoNbBTJEzk/RImxvcmVtaXBzdW1kb2xvciI"`
|
||||||
|
|
||||||
|
header := make(http.Header)
|
||||||
|
header.Set("If-None-Match", etag)
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/"+ts.URL, header)
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 304, res.StatusCode)
|
||||||
|
assert.Equal(s.T(), etag, res.Header.Get("ETag"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
|
||||||
|
config.ETagEnabled = true
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Empty(s.T(), r.Header.Get("If-None-Match"))
|
||||||
|
|
||||||
|
rw.WriteHeader(200)
|
||||||
|
rw.Write(s.readTestFile("test1.png"))
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
etag := `"1Uuny6YTSUO08MMVZyantp9oaoOcsaYibQoNbBTJEzk/Dl29MNvkqdLEqPyFvMlh_NlPbRrsC0GDG_AUlmMdX6HA"`
|
||||||
|
|
||||||
|
header := make(http.Header)
|
||||||
|
header.Set("If-None-Match", etag)
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/"+ts.URL, header)
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 304, res.StatusCode)
|
||||||
|
assert.Equal(s.T(), etag, res.Header.Get("ETag"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestETagReqNotMatch() {
|
||||||
|
config.ETagEnabled = true
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(s.T(), `"loremipsum"`, r.Header.Get("If-None-Match"))
|
||||||
|
|
||||||
|
rw.Header().Set("ETag", `"loremipsumdolor"`)
|
||||||
|
rw.WriteHeader(200)
|
||||||
|
rw.Write(s.readTestFile("test1.png"))
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
header := make(http.Header)
|
||||||
|
header.Set("If-None-Match", `"1Uuny6YTSUO08MMVZyantp9oaoOcsaYibQoNbBTJEzk/RImxvcmVtaXBzdW0i"`)
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/"+ts.URL, header)
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, res.StatusCode)
|
||||||
|
assert.Equal(
|
||||||
|
s.T(),
|
||||||
|
`"1Uuny6YTSUO08MMVZyantp9oaoOcsaYibQoNbBTJEzk/RImxvcmVtaXBzdW1kb2xvciI"`,
|
||||||
|
res.Header.Get("ETag"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestETagDataNotMatch() {
|
||||||
|
config.ETagEnabled = true
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Empty(s.T(), r.Header.Get("If-None-Match"))
|
||||||
|
|
||||||
|
rw.WriteHeader(200)
|
||||||
|
rw.Write(s.readTestFile("test1.png"))
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
header := make(http.Header)
|
||||||
|
header.Set("If-None-Match", `"1Uuny6YTSUO08MMVZyantp9oaoOcsaYibQoNbBTJEzk/Dl29MNvkqdLEq"`)
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/"+ts.URL, header)
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, res.StatusCode)
|
||||||
|
assert.Equal(
|
||||||
|
s.T(),
|
||||||
|
`"1Uuny6YTSUO08MMVZyantp9oaoOcsaYibQoNbBTJEzk/Dl29MNvkqdLEqPyFvMlh_NlPbRrsC0GDG_AUlmMdX6HA"`,
|
||||||
|
res.Header.Get("ETag"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingHandlerTestSuite) TestETagProcessingOptionsNotMatch() {
|
||||||
|
config.ETagEnabled = true
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Empty(s.T(), r.Header.Get("If-None-Match"))
|
||||||
|
|
||||||
|
rw.Header().Set("ETag", `"loremipsumdolor"`)
|
||||||
|
rw.WriteHeader(200)
|
||||||
|
rw.Write(s.readTestFile("test1.png"))
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
header := make(http.Header)
|
||||||
|
header.Set("If-None-Match", `"1Uuny6YTSUO08MMVZ/Dl29MNvkqdLEq"`)
|
||||||
|
|
||||||
|
rw := s.send("/unsafe/rs:fill:4:4/plain/"+ts.URL, header)
|
||||||
|
res := rw.Result()
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, res.StatusCode)
|
||||||
|
assert.Equal(
|
||||||
|
s.T(),
|
||||||
|
`"1Uuny6YTSUO08MMVZyantp9oaoOcsaYibQoNbBTJEzk/RImxvcmVtaXBzdW1kb2xvciI"`,
|
||||||
|
res.Header.Get("ETag"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func TestProcessingHandler(t *testing.T) {
|
func TestProcessingHandler(t *testing.T) {
|
||||||
suite.Run(t, new(ProcessingHandlerTestSuite))
|
suite.Run(t, new(ProcessingHandlerTestSuite))
|
||||||
}
|
}
|
||||||
|
@@ -46,5 +46,27 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.ETagEnabled {
|
||||||
|
etag := string(get.ETag())
|
||||||
|
|
||||||
|
if etag == req.Header.Get("If-None-Match") {
|
||||||
|
if body := get.Response().Body; body != nil {
|
||||||
|
get.Response().Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusNotModified,
|
||||||
|
Proto: "HTTP/1.0",
|
||||||
|
ProtoMajor: 1,
|
||||||
|
ProtoMinor: 0,
|
||||||
|
Header: make(http.Header),
|
||||||
|
ContentLength: 0,
|
||||||
|
Body: nil,
|
||||||
|
Close: false,
|
||||||
|
Request: req,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return get.Response(), nil
|
return get.Response(), nil
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
package fs
|
package fs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v2/config"
|
"github.com/imgproxy/imgproxy/v2/config"
|
||||||
@@ -31,16 +34,43 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error)
|
|||||||
return nil, fmt.Errorf("%s is a directory", req.URL.Path)
|
return nil, fmt.Errorf("%s is a directory", req.URL.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header := make(http.Header)
|
||||||
|
|
||||||
|
if config.ETagEnabled {
|
||||||
|
etag := BuildEtag(req.URL.Path, fi)
|
||||||
|
header.Set("ETag", etag)
|
||||||
|
|
||||||
|
if etag == req.Header.Get("If-None-Match") {
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusNotModified,
|
||||||
|
Proto: "HTTP/1.0",
|
||||||
|
ProtoMajor: 1,
|
||||||
|
ProtoMinor: 0,
|
||||||
|
Header: header,
|
||||||
|
ContentLength: 0,
|
||||||
|
Body: nil,
|
||||||
|
Close: false,
|
||||||
|
Request: req,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &http.Response{
|
return &http.Response{
|
||||||
Status: "200 OK",
|
Status: "200 OK",
|
||||||
StatusCode: 200,
|
StatusCode: 200,
|
||||||
Proto: "HTTP/1.0",
|
Proto: "HTTP/1.0",
|
||||||
ProtoMajor: 1,
|
ProtoMajor: 1,
|
||||||
ProtoMinor: 0,
|
ProtoMinor: 0,
|
||||||
Header: make(http.Header),
|
Header: header,
|
||||||
ContentLength: fi.Size(),
|
ContentLength: fi.Size(),
|
||||||
Body: f,
|
Body: f,
|
||||||
Close: true,
|
Close: true,
|
||||||
Request: req,
|
Request: req,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BuildEtag(path string, fi fs.FileInfo) string {
|
||||||
|
tag := fmt.Sprintf("%s__%d__%d", path, fi.Size(), fi.ModTime().UnixNano())
|
||||||
|
hash := md5.Sum([]byte(tag))
|
||||||
|
return `"` + string(base64.RawURLEncoding.EncodeToString(hash[:])) + `"`
|
||||||
|
}
|
||||||
|
@@ -43,13 +43,35 @@ func (t transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||||||
obj = obj.Generation(g)
|
obj = obj.Generation(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
reader, err := obj.NewReader(context.Background())
|
header := make(http.Header)
|
||||||
|
|
||||||
|
if config.ETagEnabled {
|
||||||
|
attrs, err := obj.Attrs(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
header.Set("ETag", attrs.Etag)
|
||||||
|
|
||||||
|
if attrs.Etag == req.Header.Get("If-None-Match") {
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusNotModified,
|
||||||
|
Proto: "HTTP/1.0",
|
||||||
|
ProtoMajor: 1,
|
||||||
|
ProtoMinor: 0,
|
||||||
|
Header: header,
|
||||||
|
ContentLength: 0,
|
||||||
|
Body: nil,
|
||||||
|
Close: false,
|
||||||
|
Request: req,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := obj.NewReader(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
header := make(http.Header)
|
|
||||||
header.Set("Cache-Control", reader.Attrs.CacheControl)
|
header.Set("Cache-Control", reader.Attrs.CacheControl)
|
||||||
|
|
||||||
return &http.Response{
|
return &http.Response{
|
||||||
|
@@ -50,6 +50,12 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error)
|
|||||||
input.VersionId = aws.String(req.URL.RawQuery)
|
input.VersionId = aws.String(req.URL.RawQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.ETagEnabled {
|
||||||
|
if ifNoneMatch := req.Header.Get("If-None-Match"); len(ifNoneMatch) > 0 {
|
||||||
|
input.IfNoneMatch = aws.String(ifNoneMatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s3req, _ := t.svc.GetObjectRequest(input)
|
s3req, _ := t.svc.GetObjectRequest(input)
|
||||||
|
|
||||||
if err := s3req.Send(); err != nil {
|
if err := s3req.Send(); err != nil {
|
||||||
|
Reference in New Issue
Block a user