mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-01-03 10:43:58 +02:00
Add ranged requests suport to transports
This commit is contained in:
parent
0f7281e56e
commit
58fd025f89
73
httprange/httprange.go
Normal file
73
httprange/httprange.go
Normal file
@ -0,0 +1,73 @@
|
||||
package httprange
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Parse(s string) (int64, int64, error) {
|
||||
if s == "" {
|
||||
return 0, 0, nil // header not present
|
||||
}
|
||||
|
||||
const b = "bytes="
|
||||
if !strings.HasPrefix(s, b) {
|
||||
return 0, 0, errors.New("invalid range")
|
||||
}
|
||||
|
||||
for _, ra := range strings.Split(s[len(b):], ",") {
|
||||
ra = textproto.TrimString(ra)
|
||||
if ra == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
i := strings.Index(ra, "-")
|
||||
if i < 0 {
|
||||
return 0, 0, errors.New("invalid range")
|
||||
}
|
||||
|
||||
start, end := textproto.TrimString(ra[:i]), textproto.TrimString(ra[i+1:])
|
||||
|
||||
if start == "" {
|
||||
// Don't support ranges without start since it looks like FFmpeg doen't use ones
|
||||
return 0, 0, errors.New("invalid range")
|
||||
}
|
||||
|
||||
istart, err := strconv.ParseInt(start, 10, 64)
|
||||
if err != nil || i < 0 {
|
||||
return 0, 0, errors.New("invalid range")
|
||||
}
|
||||
|
||||
var iend int64
|
||||
|
||||
if end == "" {
|
||||
iend = -1
|
||||
} else {
|
||||
iend, err = strconv.ParseInt(end, 10, 64)
|
||||
if err != nil || istart > iend {
|
||||
return 0, 0, errors.New("invalid range")
|
||||
}
|
||||
}
|
||||
|
||||
return istart, iend, nil
|
||||
}
|
||||
|
||||
return 0, 0, errors.New("invalid range")
|
||||
}
|
||||
|
||||
func InvalidHTTPRangeResponse(req *http.Request) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusRequestedRangeNotSatisfiable,
|
||||
Proto: "HTTP/1.0",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 0,
|
||||
Header: make(http.Header),
|
||||
ContentLength: 0,
|
||||
Body: nil,
|
||||
Close: false,
|
||||
Request: req,
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/httprange"
|
||||
)
|
||||
|
||||
type transport struct {
|
||||
@ -40,12 +41,22 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error)
|
||||
containerURL := t.serviceURL.NewContainerURL(strings.ToLower(req.URL.Host))
|
||||
blobURL := containerURL.NewBlockBlobURL(strings.TrimPrefix(req.URL.Path, "/"))
|
||||
|
||||
get, err := blobURL.Download(req.Context(), 0, azblob.CountToEnd, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{})
|
||||
start, end, err := httprange.Parse(req.Header.Get("Range"))
|
||||
if err != nil {
|
||||
return httprange.InvalidHTTPRangeResponse(req), nil
|
||||
}
|
||||
|
||||
length := end - start + 1
|
||||
if end <= 0 {
|
||||
length = azblob.CountToEnd
|
||||
}
|
||||
|
||||
get, err := blobURL.Download(req.Context(), start, length, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.ETagEnabled {
|
||||
if config.ETagEnabled && start == 0 && end == azblob.CountToEnd {
|
||||
etag := string(get.ETag())
|
||||
|
||||
if etag == req.Header.Get("If-None-Match") {
|
||||
|
27
transport/fs/file_limiter.go
Normal file
27
transport/fs/file_limiter.go
Normal file
@ -0,0 +1,27 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type fileLimiter struct {
|
||||
f http.File
|
||||
left int
|
||||
}
|
||||
|
||||
func (lr *fileLimiter) Read(p []byte) (n int, err error) {
|
||||
if lr.left <= 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if len(p) > lr.left {
|
||||
p = p[0:lr.left]
|
||||
}
|
||||
n, err = lr.f.Read(p)
|
||||
lr.left -= n
|
||||
return
|
||||
}
|
||||
|
||||
func (lr *fileLimiter) Close() error {
|
||||
return lr.f.Close()
|
||||
}
|
@ -6,11 +6,15 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/httprange"
|
||||
)
|
||||
|
||||
type transport struct {
|
||||
@ -41,7 +45,32 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error)
|
||||
return respNotFound(req, fmt.Sprintf("%s is directory", req.URL.Path)), nil
|
||||
}
|
||||
|
||||
if config.ETagEnabled {
|
||||
statusCode := 200
|
||||
size := fi.Size()
|
||||
body := io.ReadCloser(f)
|
||||
|
||||
mime := mime.TypeByExtension(filepath.Ext(fi.Name()))
|
||||
header.Set("Content-Type", mime)
|
||||
|
||||
start, end, err := httprange.Parse(req.Header.Get("Range"))
|
||||
switch {
|
||||
case err != nil:
|
||||
f.Close()
|
||||
return httprange.InvalidHTTPRangeResponse(req), nil
|
||||
|
||||
case end != 0:
|
||||
if end < 0 {
|
||||
end = size - 1
|
||||
}
|
||||
|
||||
f.Seek(start, io.SeekStart)
|
||||
|
||||
statusCode = http.StatusPartialContent
|
||||
size = end - start + 1
|
||||
body = &fileLimiter{f: f, left: int(size)}
|
||||
header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fi.Size()))
|
||||
|
||||
case config.ETagEnabled:
|
||||
etag := BuildEtag(req.URL.Path, fi)
|
||||
header.Set("ETag", etag)
|
||||
|
||||
@ -62,15 +91,16 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error)
|
||||
}
|
||||
}
|
||||
|
||||
header.Set("Content-Length", strconv.Itoa(int(size)))
|
||||
|
||||
return &http.Response{
|
||||
Status: "200 OK",
|
||||
StatusCode: 200,
|
||||
StatusCode: statusCode,
|
||||
Proto: "HTTP/1.0",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 0,
|
||||
Header: header,
|
||||
ContentLength: fi.Size(),
|
||||
Body: f,
|
||||
ContentLength: size,
|
||||
Body: body,
|
||||
Close: true,
|
||||
Request: req,
|
||||
}, nil
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"google.golang.org/api/option"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
"github.com/imgproxy/imgproxy/v3/httprange"
|
||||
)
|
||||
|
||||
// For tests
|
||||
@ -58,40 +59,82 @@ func (t transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
obj = obj.Generation(g)
|
||||
}
|
||||
|
||||
var (
|
||||
reader *storage.Reader
|
||||
statusCode int
|
||||
size int64
|
||||
)
|
||||
|
||||
header := make(http.Header)
|
||||
|
||||
if config.ETagEnabled {
|
||||
attrs, err := obj.Attrs(req.Context())
|
||||
if r := req.Header.Get("Range"); len(r) != 0 {
|
||||
start, end, err := httprange.Parse(r)
|
||||
if err != nil {
|
||||
return httprange.InvalidHTTPRangeResponse(req), nil
|
||||
}
|
||||
|
||||
if end != 0 {
|
||||
length := end - start + 1
|
||||
if end < 0 {
|
||||
length = -1
|
||||
}
|
||||
|
||||
reader, err = obj.NewRangeReader(req.Context(), start, length)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if end < 0 || end >= reader.Attrs.Size {
|
||||
end = reader.Attrs.Size - 1
|
||||
}
|
||||
|
||||
size = end - reader.Attrs.StartOffset + 1
|
||||
|
||||
statusCode = http.StatusPartialContent
|
||||
header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", reader.Attrs.StartOffset, end, reader.Attrs.Size))
|
||||
}
|
||||
}
|
||||
|
||||
// We haven't initialize reader yet, this means that we need non-ranged reader
|
||||
if reader == nil {
|
||||
if config.ETagEnabled {
|
||||
attrs, err := obj.Attrs(req.Context())
|
||||
if err != nil {
|
||||
return handleError(req, err)
|
||||
}
|
||||
header.Set("ETag", attrs.Etag)
|
||||
|
||||
if etag := req.Header.Get("If-None-Match"); len(etag) > 0 && attrs.Etag == etag {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
reader, err = obj.NewReader(req.Context())
|
||||
if err != nil {
|
||||
return handleError(req, err)
|
||||
}
|
||||
header.Set("ETag", attrs.Etag)
|
||||
|
||||
if etag := req.Header.Get("If-None-Match"); len(etag) > 0 && attrs.Etag == etag {
|
||||
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(req.Context())
|
||||
if err != nil {
|
||||
return handleError(req, err)
|
||||
statusCode = 200
|
||||
size = reader.Attrs.Size
|
||||
}
|
||||
|
||||
header.Set("Content-Length", strconv.Itoa(int(size)))
|
||||
header.Set("Content-Type", reader.Attrs.ContentType)
|
||||
header.Set("Cache-Control", reader.Attrs.CacheControl)
|
||||
|
||||
return &http.Response{
|
||||
Status: "200 OK",
|
||||
StatusCode: 200,
|
||||
StatusCode: statusCode,
|
||||
Proto: "HTTP/1.0",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 0,
|
||||
|
@ -53,7 +53,9 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error)
|
||||
input.VersionId = aws.String(req.URL.RawQuery)
|
||||
}
|
||||
|
||||
if config.ETagEnabled {
|
||||
if r := req.Header.Get("Range"); len(r) != 0 {
|
||||
input.Range = aws.String(r)
|
||||
} else if config.ETagEnabled {
|
||||
if ifNoneMatch := req.Header.Get("If-None-Match"); len(ifNoneMatch) > 0 {
|
||||
input.IfNoneMatch = aws.String(ifNoneMatch)
|
||||
}
|
||||
|
@ -46,7 +46,12 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error)
|
||||
container := req.URL.Host
|
||||
objectName := strings.TrimPrefix(req.URL.Path, "/")
|
||||
|
||||
object, objectHeaders, err := t.con.ObjectOpen(req.Context(), container, objectName, false, make(swift.Headers))
|
||||
reqHeaders := make(swift.Headers)
|
||||
if r := req.Header.Get("Range"); len(r) > 0 {
|
||||
reqHeaders["Range"] = r
|
||||
}
|
||||
|
||||
object, objectHeaders, err := t.con.ObjectOpen(req.Context(), container, objectName, false, reqHeaders)
|
||||
|
||||
header := make(http.Header)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user