github.com/dtroyer-salad/og2/v2@v2.0.0-20240412154159-c47231610877/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  }