github.com/puellanivis/breton@v0.2.16/lib/files/httpfiles/reader.go (about)

     1  package httpfiles
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"io"
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  	"time"
    11  
    12  	"github.com/puellanivis/breton/lib/files"
    13  	"github.com/puellanivis/breton/lib/files/wrapper"
    14  )
    15  
    16  type reader struct {
    17  	r    io.Reader
    18  	s    io.Seeker
    19  	info *wrapper.Info
    20  
    21  	*request
    22  	header http.Header
    23  
    24  	err     error
    25  	loading <-chan struct{}
    26  }
    27  
    28  func (r *reader) Header() (http.Header, error) {
    29  	for range r.loading {
    30  	}
    31  
    32  	if r.err != nil {
    33  		return nil, r.err
    34  	}
    35  
    36  	return r.header, nil
    37  }
    38  
    39  func (r *reader) Stat() (os.FileInfo, error) {
    40  	for range r.loading {
    41  	}
    42  
    43  	if r.err != nil {
    44  		return nil, r.err
    45  	}
    46  
    47  	return r.info, nil
    48  }
    49  
    50  func (r *reader) Read(b []byte) (n int, err error) {
    51  	for range r.loading {
    52  	}
    53  
    54  	if r.err != nil {
    55  		return 0, r.err
    56  	}
    57  
    58  	return r.r.Read(b)
    59  }
    60  
    61  func (r *reader) Seek(offset int64, whence int) (int64, error) {
    62  	// TODO: if we’ve not loaded yet, it’s possible to try and use http Range header? (header has poor support, so… blech for now.
    63  	for range r.loading {
    64  	}
    65  
    66  	if r.err != nil {
    67  		return 0, r.err
    68  	}
    69  
    70  	if r.s == nil {
    71  		switch s := r.r.(type) {
    72  		case io.Seeker:
    73  			r.s = s
    74  		default:
    75  			return 0, os.ErrInvalid
    76  		}
    77  	}
    78  
    79  	return r.s.Seek(offset, whence)
    80  }
    81  
    82  func (r *reader) Close() error {
    83  	for range r.loading {
    84  	}
    85  
    86  	// Ignore the r.err, as it is a request-scope error, and not relevant to closing.
    87  
    88  	if c, ok := r.r.(io.Closer); ok {
    89  		return c.Close()
    90  	}
    91  
    92  	return nil
    93  }
    94  
    95  func (h *handler) Open(ctx context.Context, uri *url.URL) (files.Reader, error) {
    96  	uri = elideDefaultPort(uri)
    97  
    98  	cl, ok := getClient(ctx)
    99  	if !ok {
   100  		cl = http.DefaultClient
   101  	}
   102  
   103  	req := newHTTPRequest(http.MethodGet, uri)
   104  	req = req.WithContext(ctx)
   105  
   106  	if ua, ok := getUserAgent(ctx); ok {
   107  		req.Header.Set("User-Agent", ua)
   108  	}
   109  
   110  	loading := make(chan struct{})
   111  	r := &reader{
   112  		loading: loading,
   113  
   114  		request: &request{
   115  			name: uri.String(),
   116  			req:  req,
   117  		},
   118  	}
   119  
   120  	go func() {
   121  		// So, all of the file operations block on a range over the loading channel.
   122  		// They will not end this blocking until loading is closed.
   123  		// But they will also swallow any sends, though sends will block until someone is receiving.
   124  		//
   125  		// So, we will block on the first send until someone receives from the loading channel,
   126  		// or the context expires.
   127  		//
   128  		// But none of the receivers will actually unblock until the loading channel is _closed_.
   129  		// And once the channel is closed, each range over loading won’t even stop to block.
   130  
   131  		defer close(loading)
   132  
   133  		select {
   134  		case loading <- struct{}{}:
   135  		case <-ctx.Done():
   136  			r.err = files.PathError("open", r.name, ctx.Err())
   137  			return
   138  		}
   139  
   140  		// So, we will not arrive here until someone is ranging over the loading channel.
   141  		//
   142  		// This ensures the actual http request HAPPENS AFTER the first file operation is called,
   143  		// but that all file operation behavior HAPPENS AFTER the actual http request is made.
   144  		//
   145  		// This lets us apply files.Option functions after files.Open,
   146  		// and change the http.Request before actually doing it.
   147  
   148  		resp, err := cl.Do(req)
   149  		if err != nil {
   150  			r.err = files.PathError("open", r.name, err)
   151  			return
   152  		}
   153  
   154  		r.header = resp.Header
   155  		uri := resp.Request.URL
   156  
   157  		t := time.Now()
   158  		if lastmod := r.header.Get("Last-Modified"); lastmod != "" {
   159  			if t1, err := http.ParseTime(lastmod); err == nil {
   160  				t = t1
   161  			}
   162  		}
   163  
   164  		r.info = wrapper.NewInfo(uri, int(resp.ContentLength), t)
   165  
   166  		if err := getErr(resp); err != nil {
   167  			resp.Body.Close()
   168  
   169  			r.err = files.PathError("open", uri.String(), err)
   170  			return
   171  		}
   172  
   173  		if resp.ContentLength < 0 {
   174  			r.r = resp.Body
   175  			return
   176  		}
   177  
   178  		b, err := files.ReadFrom(resp.Body)
   179  		if err != nil {
   180  			r.err = files.PathError("read", uri.String(), err)
   181  			return
   182  		}
   183  		resp.Body.Close()
   184  
   185  		r.r = bytes.NewReader(b)
   186  	}()
   187  
   188  	return r, nil
   189  }