1
0
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:
DarthSim 2022-09-07 17:09:43 +06:00
parent 0f7281e56e
commit 58fd025f89
7 changed files with 223 additions and 32 deletions

73
httprange/httprange.go Normal file
View 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,
}
}

View File

@ -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") {

View 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()
}

View File

@ -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

View File

@ -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,

View File

@ -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)
}

View File

@ -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)