From 20c9cf97c4a650585832985fb6b0021e862482b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Thu, 7 Jan 2021 10:54:56 +0000 Subject: [PATCH] send Content-Length and Accept-Ranges headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- pkg/rhttp/datatx/utils/download/download.go | 120 +++++++++++--------- 1 file changed, 66 insertions(+), 54 deletions(-) diff --git a/pkg/rhttp/datatx/utils/download/download.go b/pkg/rhttp/datatx/utils/download/download.go index 84b015597b3..292087d9df0 100644 --- a/pkg/rhttp/datatx/utils/download/download.go +++ b/pkg/rhttp/datatx/utils/download/download.go @@ -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" @@ -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" {