Skip to content

Commit

Permalink
send Content-Length and Accept-Ranges headers
Browse files Browse the repository at this point in the history
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
  • Loading branch information
butonic committed Jan 7, 2021
1 parent 697b994 commit 20c9cf9
Showing 1 changed file with 66 additions and 54 deletions.
120 changes: 66 additions & 54 deletions pkg/rhttp/datatx/utils/download/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"io"
"mime/multipart"
"net/http"
"strconv"

provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/pkg/appctx"
Expand Down Expand Up @@ -91,67 +92,78 @@ func GetOrHeadFile(w http.ResponseWriter, r *http.Request, fs storage.FS) {

sendSize := int64(md.Size)
var sendContent io.Reader = content
var s io.Seeker
if len(ranges) > 0 {
sublog.Debug().Int64("start", ranges[0].Start).Int64("length", ranges[0].Length).Msg("range request")
if s, ok = content.(io.Seeker); !ok {
sublog.Error().Int64("start", ranges[0].Start).Int64("length", ranges[0].Length).Msg("ReadCloser is not seekable")
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return
if md.Size >= 0 { // we only need to check the range if we have bytes to transfer
var s io.Seeker
if s, ok = content.(io.Seeker); ok {
// tell clients they can send range requests
w.Header().Set("Accept-Ranges", "bytes")
}
switch {
case len(ranges) == 1:
// RFC 7233, Section 4.1:
// "If a single part is being transferred, the server
// generating the 206 response MUST generate a
// Content-Range header field, describing what range
// of the selected representation is enclosed, and a
// payload consisting of the range.
// ...
// A server MUST NOT generate a multipart response to
// a request for a single range, since a client that
// does not request multiple parts might not support
// multipart responses."
ra := ranges[0]
if _, err := s.Seek(ra.Start, io.SeekStart); err != nil {
sublog.Error().Err(err).Int64("start", ra.Start).Int64("length", ra.Length).Msg("content is not seekable")
if len(ranges) > 0 {
sublog.Debug().Int64("start", ranges[0].Start).Int64("length", ranges[0].Length).Msg("range request")
if s == nil {
sublog.Error().Int64("start", ranges[0].Start).Int64("length", ranges[0].Length).Msg("ReadCloser is not seekable")
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return
}
sendSize = ra.Length
code = http.StatusPartialContent
w.Header().Set("Content-Range", ra.ContentRange(int64(md.Size)))
case len(ranges) > 1:
sendSize = RangesMIMESize(ranges, md.MimeType, int64(md.Size))
code = http.StatusPartialContent

pr, pw := io.Pipe()
mw := multipart.NewWriter(pw)
w.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary())
sendContent = pr
defer pr.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish.
go func() {
for _, ra := range ranges {
part, err := mw.CreatePart(ra.MimeHeader(md.MimeType, int64(md.Size)))
if err != nil {
_ = pw.CloseWithError(err) // CloseWithError always returns nil
return
}
if _, err := s.Seek(ra.Start, io.SeekStart); err != nil {
_ = pw.CloseWithError(err) // CloseWithError always returns nil
return
}
if _, err := io.CopyN(part, content, ra.Length); err != nil {
_ = pw.CloseWithError(err) // CloseWithError always returns nil
return
}
}
mw.Close()
pw.Close()
}()

switch {
case len(ranges) == 1:
// RFC 7233, Section 4.1:
// "If a single part is being transferred, the server
// generating the 206 response MUST generate a
// Content-Range header field, describing what range
// of the selected representation is enclosed, and a
// payload consisting of the range.
// ...
// A server MUST NOT generate a multipart response to
// a request for a single range, since a client that
// does not request multiple parts might not support
// multipart responses."
ra := ranges[0]
if _, err := s.Seek(ra.Start, io.SeekStart); err != nil {
sublog.Error().Err(err).Int64("start", ra.Start).Int64("length", ra.Length).Msg("content is not seekable")
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return
}
sendSize = ra.Length
code = http.StatusPartialContent
w.Header().Set("Content-Range", ra.ContentRange(int64(md.Size)))
case len(ranges) > 1:
sendSize = RangesMIMESize(ranges, md.MimeType, int64(md.Size))
code = http.StatusPartialContent

pr, pw := io.Pipe()
mw := multipart.NewWriter(pw)
w.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary())
sendContent = pr
defer pr.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish.
go func() {
for _, ra := range ranges {
part, err := mw.CreatePart(ra.MimeHeader(md.MimeType, int64(md.Size)))
if err != nil {
_ = pw.CloseWithError(err) // CloseWithError always returns nil
return
}
if _, err := s.Seek(ra.Start, io.SeekStart); err != nil {
_ = pw.CloseWithError(err) // CloseWithError always returns nil
return
}
if _, err := io.CopyN(part, content, ra.Length); err != nil {
_ = pw.CloseWithError(err) // CloseWithError always returns nil
return
}
}
mw.Close()
pw.Close()
}()
}
}
}

if w.Header().Get("Content-Encoding") == "" {
w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10))
}

w.WriteHeader(code)

if r.Method != "HEAD" {
Expand Down

0 comments on commit 20c9cf9

Please sign in to comment.