From 741fdbda3db3f5776e8c5e049d0492f555efac09 Mon Sep 17 00:00:00 2001 From: Jarek Kowalski Date: Thu, 4 Nov 2021 23:24:19 -0700 Subject: [PATCH] added ReadStreamRange() method to efficiently read a range of data It passes "Range: bytes=X-Y" and if the server returns HTTP 206, we know it complied with the request. For servers that don't understand range and return HTTP 200 instead we discard some bytes and limit the result to emulate this behavior. This will greatly help https://github.com/kopia/kopia which relies on partial reads from pack blobs. --- client.go | 38 ++++++++++++++++++++++++++++++++++++++ utils.go | 25 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/client.go b/client.go index 0d31467..76d24dc 100644 --- a/client.go +++ b/client.go @@ -3,6 +3,7 @@ package gowebdav import ( "bytes" "encoding/xml" + "fmt" "io" "net/http" "net/url" @@ -346,6 +347,43 @@ func (c *Client) ReadStream(path string) (io.ReadCloser, error) { return nil, newPathError("ReadStream", path, rs.StatusCode) } +// ReadStreamRange reads the stream representing a subset of bytes for a given path, +// utilizing HTTP Range Requests if the server supports it. +// The range is expressed as offset from the start of the file and length, for example +// offset=10, length=10 will return bytes 10 through 19. +// +// If the server does not support partial content requests and returns full content instead, +// this function will emulate the behavior by skipping `offset` bytes and limiting the result +// to `length`. +func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error) { + rs, err := c.req("GET", path, nil, func(r *http.Request) { + r.Header.Add("Range", fmt.Sprintf("bytes=%v-%v", offset, offset+length-1)) + }) + if err != nil { + return nil, newPathErrorErr("ReadStreamRange", path, err) + } + + if rs.StatusCode == http.StatusPartialContent { + // server supported partial content, return as-is. + return rs.Body, nil + } + + // server returned success, but did not support partial content, so we have the whole + // stream in rs.Body + if rs.StatusCode == 200 { + // discard first 'offset' bytes. + if _, err := io.Copy(io.Discard, io.LimitReader(rs.Body, offset)); err != nil { + return nil, newPathErrorErr("ReadStreamRange", path, err) + } + + // return a io.ReadCloser that is limited to `length` bytes. + return &limitedReadCloser{rs.Body, int(length)}, nil + } + + rs.Body.Close() + return nil, newPathError("ReadStream", path, rs.StatusCode) +} + // Write writes data to a given path func (c *Client) Write(path string, data []byte, _ os.FileMode) error { s := c.put(path, bytes.NewReader(data)) diff --git a/utils.go b/utils.go index 5b96468..f82592a 100644 --- a/utils.go +++ b/utils.go @@ -108,3 +108,28 @@ func parseXML(data io.Reader, resp interface{}, parse func(resp interface{}) err } return nil } + +// limitedReadCloser wraps a io.ReadCloser and limits the number of bytes that can be read from it. +type limitedReadCloser struct { + rc io.ReadCloser + remaining int +} + +func (l *limitedReadCloser) Read(buf []byte) (int, error) { + if l.remaining <= 0 { + return 0, io.EOF + } + + if len(buf) > l.remaining { + buf = buf[0:l.remaining] + } + + n, err := l.rc.Read(buf) + l.remaining -= n + + return n, err +} + +func (l *limitedReadCloser) Close() error { + return l.rc.Close() +}