go-hep.org/x/hep@v0.38.1/groot/internal/httpio/reader.go (about)

     1  // Copyright ©2022 The go-hep Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package httpio
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"strconv"
    13  	"sync"
    14  )
    15  
    16  // Reader presents an HTTP resource as an io.Reader and io.ReaderAt.
    17  type Reader struct {
    18  	cli    *http.Client
    19  	req    *http.Request
    20  	ctx    context.Context
    21  	cancel context.CancelFunc
    22  
    23  	pool sync.Pool
    24  
    25  	r    *io.SectionReader
    26  	len  int64
    27  	etag string
    28  }
    29  
    30  // Open returns a Reader from the provided URL.
    31  func Open(uri string, opts ...Option) (r *Reader, err error) {
    32  	cfg := newConfig()
    33  	for _, opt := range opts {
    34  		err := opt(cfg)
    35  		if err != nil {
    36  			return nil, fmt.Errorf("httpio: could not open %q: %w", uri, err)
    37  		}
    38  	}
    39  
    40  	r = &Reader{
    41  		cli: cfg.cli,
    42  	}
    43  	r.ctx, r.cancel = context.WithCancel(cfg.ctx)
    44  
    45  	req, err := http.NewRequestWithContext(r.ctx, http.MethodGet, uri, nil)
    46  	if err != nil {
    47  		r.cancel()
    48  		return nil, fmt.Errorf("httpio: could not create HTTP request: %w", err)
    49  	}
    50  	if cfg.auth.usr != "" || cfg.auth.pwd != "" {
    51  		req.SetBasicAuth(cfg.auth.usr, cfg.auth.pwd)
    52  	}
    53  	r.req = req.Clone(r.ctx)
    54  
    55  	hdr, err := r.cli.Head(r.req.URL.String())
    56  	if err != nil {
    57  		r.cancel()
    58  		return nil, fmt.Errorf("httpio: could not send HEAD request: %w", err)
    59  	}
    60  	defer hdr.Body.Close()
    61  	_, _ = io.Copy(io.Discard, hdr.Body)
    62  
    63  	if hdr.StatusCode != http.StatusOK {
    64  		return nil, fmt.Errorf("httpio: invalid HEAD response code=%v", hdr.StatusCode)
    65  	}
    66  
    67  	if hdr.Header.Get("accept-ranges") != "bytes" {
    68  		return nil, fmt.Errorf("httpio: invalid HEAD response: %w", errAcceptRange)
    69  	}
    70  
    71  	r.len = hdr.ContentLength
    72  	r.etag = hdr.Header.Get("Etag")
    73  	r.r = io.NewSectionReader(r, 0, r.len)
    74  
    75  	r.req.Header.Set("Range", "")
    76  	r.pool = sync.Pool{
    77  		New: func() any {
    78  			return r.req.Clone(r.ctx)
    79  		},
    80  	}
    81  
    82  	return r, nil
    83  }
    84  
    85  // Size returns the number of bytes available for reading via ReadAt.
    86  func (r *Reader) Size() int64 {
    87  	return r.len
    88  }
    89  
    90  // Name returns the name of the file as presented to Open.
    91  func (r *Reader) Name() string {
    92  	return r.req.URL.String()
    93  }
    94  
    95  // Close implements the io.Closer interface.
    96  func (r *Reader) Close() error {
    97  	r.cancel()
    98  	r.cli = nil
    99  	r.req = nil
   100  	return nil
   101  }
   102  
   103  // Read implements the io.Reader interface.
   104  func (r *Reader) Read(p []byte) (int, error) {
   105  	return r.r.Read(p)
   106  }
   107  
   108  // Seek implements the io.Seeker interface.
   109  func (r *Reader) Seek(offset int64, whence int) (int64, error) {
   110  	return r.r.Seek(offset, whence)
   111  }
   112  
   113  // ReadAt implements the io.ReaderAt interface.
   114  func (r *Reader) ReadAt(p []byte, off int64) (int, error) {
   115  	if len(p) == 0 {
   116  		return 0, nil
   117  	}
   118  
   119  	rng := rng(off, off+int64(len(p))-1)
   120  	req := r.getReq(rng)
   121  	defer r.pool.Put(req)
   122  
   123  	resp, err := r.cli.Do(req)
   124  	if err != nil {
   125  		return 0, fmt.Errorf("httpio: could not send GET request: %w", err)
   126  	}
   127  	defer resp.Body.Close()
   128  
   129  	n, _ := io.ReadFull(resp.Body, p)
   130  
   131  	if etag := resp.Header.Get("Etag"); etag != r.etag {
   132  		return n, fmt.Errorf("httpio: resource changed")
   133  	}
   134  
   135  	switch resp.StatusCode {
   136  	case http.StatusPartialContent:
   137  		// ok.
   138  	case http.StatusRequestedRangeNotSatisfiable:
   139  		return 0, io.EOF
   140  	default:
   141  		return n, fmt.Errorf("httpio: invalid GET response: code=%v", resp.StatusCode)
   142  	}
   143  
   144  	if int64(len(p)) > r.len {
   145  		return n, io.EOF
   146  	}
   147  
   148  	return n, nil
   149  }
   150  
   151  func (r *Reader) getReq(rng string) *http.Request {
   152  	o := r.pool.Get().(*http.Request)
   153  	o.Header = r.req.Header.Clone()
   154  	o.Header["Range"][0] = rng
   155  	return o
   156  }
   157  
   158  func rng(beg, end int64) string {
   159  	return "bytes=" + strconv.Itoa(int(beg)) + "-" + strconv.Itoa(int(end))
   160  }
   161  
   162  var (
   163  	_ io.Reader   = (*Reader)(nil)
   164  	_ io.Seeker   = (*Reader)(nil)
   165  	_ io.ReaderAt = (*Reader)(nil)
   166  	_ io.Closer   = (*Reader)(nil)
   167  )