oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/internal/httputil/seek.go (about) 1 /* 2 Copyright The ORAS Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package httputil 17 18 import ( 19 "errors" 20 "fmt" 21 "io" 22 "net/http" 23 ) 24 25 // Client is an interface for a HTTP client. 26 // This interface is defined inside this package to prevent potential import 27 // loop. 28 type Client interface { 29 // Do sends an HTTP request and returns an HTTP response. 30 Do(*http.Request) (*http.Response, error) 31 } 32 33 // readSeekCloser seeks http body by starting new connections. 34 type readSeekCloser struct { 35 client Client 36 req *http.Request 37 rc io.ReadCloser 38 size int64 39 offset int64 40 closed bool 41 } 42 43 // NewReadSeekCloser returns a seeker to make the HTTP response seekable. 44 // Callers should ensure that the server supports Range request. 45 func NewReadSeekCloser(client Client, req *http.Request, respBody io.ReadCloser, size int64) io.ReadSeekCloser { 46 return &readSeekCloser{ 47 client: client, 48 req: req, 49 rc: respBody, 50 size: size, 51 } 52 } 53 54 // Read reads the content body and counts offset. 55 func (rsc *readSeekCloser) Read(p []byte) (n int, err error) { 56 if rsc.closed { 57 return 0, errors.New("read: already closed") 58 } 59 n, err = rsc.rc.Read(p) 60 rsc.offset += int64(n) 61 return 62 } 63 64 // Seek starts a new connection to the remote for reading if position changes. 65 func (rsc *readSeekCloser) Seek(offset int64, whence int) (int64, error) { 66 if rsc.closed { 67 return 0, errors.New("seek: already closed") 68 } 69 switch whence { 70 case io.SeekCurrent: 71 offset += rsc.offset 72 case io.SeekStart: 73 // no-op 74 case io.SeekEnd: 75 offset += rsc.size 76 default: 77 return 0, errors.New("seek: invalid whence") 78 } 79 if offset < 0 { 80 return 0, errors.New("seek: an attempt was made to move the pointer before the beginning of the content") 81 } 82 if offset == rsc.offset { 83 return offset, nil 84 } 85 if offset >= rsc.size { 86 rsc.rc.Close() 87 rsc.rc = http.NoBody 88 rsc.offset = offset 89 return offset, nil 90 } 91 92 req := rsc.req.Clone(rsc.req.Context()) 93 req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, rsc.size-1)) 94 resp, err := rsc.client.Do(req) 95 if err != nil { 96 return 0, fmt.Errorf("seek: %s %q: %w", req.Method, req.URL, err) 97 } 98 if resp.StatusCode != http.StatusPartialContent { 99 resp.Body.Close() 100 return 0, fmt.Errorf("seek: %s %q: unexpected status code %d", resp.Request.Method, resp.Request.URL, resp.StatusCode) 101 } 102 103 rsc.rc.Close() 104 rsc.rc = resp.Body 105 rsc.offset = offset 106 return offset, nil 107 } 108 109 // Close closes the content body. 110 func (rsc *readSeekCloser) Close() error { 111 if rsc.closed { 112 return nil 113 } 114 rsc.closed = true 115 return rsc.rc.Close() 116 }