github.com/mika/distribution@v2.2.2-0.20160108133430-a75790e3d8e0+incompatible/registry/client/transport/http_reader.go (about) 1 package transport 2 3 import ( 4 "bufio" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 "os" 10 ) 11 12 // ReadSeekCloser combines io.ReadSeeker with io.Closer. 13 type ReadSeekCloser interface { 14 io.ReadSeeker 15 io.Closer 16 } 17 18 // NewHTTPReadSeeker handles reading from an HTTP endpoint using a GET 19 // request. When seeking and starting a read from a non-zero offset 20 // the a "Range" header will be added which sets the offset. 21 // TODO(dmcgowan): Move this into a separate utility package 22 func NewHTTPReadSeeker(client *http.Client, url string, errorHandler func(*http.Response) error) ReadSeekCloser { 23 return &httpReadSeeker{ 24 client: client, 25 url: url, 26 errorHandler: errorHandler, 27 } 28 } 29 30 type httpReadSeeker struct { 31 client *http.Client 32 url string 33 34 // errorHandler creates an error from an unsuccessful HTTP response. 35 // This allows the error to be created with the HTTP response body 36 // without leaking the body through a returned error. 37 errorHandler func(*http.Response) error 38 39 size int64 40 41 // rc is the remote read closer. 42 rc io.ReadCloser 43 // brd is a buffer for internal buffered io. 44 brd *bufio.Reader 45 // readerOffset tracks the offset as of the last read. 46 readerOffset int64 47 // seekOffset allows Seek to override the offset. Seek changes 48 // seekOffset instead of changing readOffset directly so that 49 // connection resets can be delayed and possibly avoided if the 50 // seek is undone (i.e. seeking to the end and then back to the 51 // beginning). 52 seekOffset int64 53 err error 54 } 55 56 func (hrs *httpReadSeeker) Read(p []byte) (n int, err error) { 57 if hrs.err != nil { 58 return 0, hrs.err 59 } 60 61 // If we seeked to a different position, we need to reset the 62 // connection. This logic is here instead of Seek so that if 63 // a seek is undone before the next read, the connection doesn't 64 // need to be closed and reopened. A common example of this is 65 // seeking to the end to determine the length, and then seeking 66 // back to the original position. 67 if hrs.readerOffset != hrs.seekOffset { 68 hrs.reset() 69 } 70 71 hrs.readerOffset = hrs.seekOffset 72 73 rd, err := hrs.reader() 74 if err != nil { 75 return 0, err 76 } 77 78 n, err = rd.Read(p) 79 hrs.seekOffset += int64(n) 80 hrs.readerOffset += int64(n) 81 82 // Simulate io.EOF error if we reach filesize. 83 if err == nil && hrs.size >= 0 && hrs.readerOffset >= hrs.size { 84 err = io.EOF 85 } 86 87 return n, err 88 } 89 90 func (hrs *httpReadSeeker) Seek(offset int64, whence int) (int64, error) { 91 if hrs.err != nil { 92 return 0, hrs.err 93 } 94 95 _, err := hrs.reader() 96 if err != nil { 97 return 0, err 98 } 99 100 newOffset := hrs.seekOffset 101 102 switch whence { 103 case os.SEEK_CUR: 104 newOffset += int64(offset) 105 case os.SEEK_END: 106 if hrs.size < 0 { 107 return 0, errors.New("content length not known") 108 } 109 newOffset = hrs.size + int64(offset) 110 case os.SEEK_SET: 111 newOffset = int64(offset) 112 } 113 114 if newOffset < 0 { 115 err = errors.New("cannot seek to negative position") 116 } else { 117 hrs.seekOffset = newOffset 118 } 119 120 return hrs.seekOffset, err 121 } 122 123 func (hrs *httpReadSeeker) Close() error { 124 if hrs.err != nil { 125 return hrs.err 126 } 127 128 // close and release reader chain 129 if hrs.rc != nil { 130 hrs.rc.Close() 131 } 132 133 hrs.rc = nil 134 hrs.brd = nil 135 136 hrs.err = errors.New("httpLayer: closed") 137 138 return nil 139 } 140 141 func (hrs *httpReadSeeker) reset() { 142 if hrs.err != nil { 143 return 144 } 145 if hrs.rc != nil { 146 hrs.rc.Close() 147 hrs.rc = nil 148 } 149 } 150 151 func (hrs *httpReadSeeker) reader() (io.Reader, error) { 152 if hrs.err != nil { 153 return nil, hrs.err 154 } 155 156 if hrs.rc != nil { 157 return hrs.brd, nil 158 } 159 160 req, err := http.NewRequest("GET", hrs.url, nil) 161 if err != nil { 162 return nil, err 163 } 164 165 if hrs.readerOffset > 0 { 166 // TODO(stevvooe): Get this working correctly. 167 168 // If we are at different offset, issue a range request from there. 169 req.Header.Add("Range", "1-") 170 // TODO: get context in here 171 // context.GetLogger(hrs.context).Infof("Range: %s", req.Header.Get("Range")) 172 } 173 174 resp, err := hrs.client.Do(req) 175 if err != nil { 176 return nil, err 177 } 178 179 // Normally would use client.SuccessStatus, but that would be a cyclic 180 // import 181 if resp.StatusCode >= 200 && resp.StatusCode <= 399 { 182 hrs.rc = resp.Body 183 if resp.StatusCode == http.StatusOK { 184 hrs.size = resp.ContentLength 185 } else { 186 hrs.size = -1 187 } 188 } else { 189 defer resp.Body.Close() 190 if hrs.errorHandler != nil { 191 return nil, hrs.errorHandler(resp) 192 } 193 return nil, fmt.Errorf("unexpected status resolving reader: %v", resp.Status) 194 } 195 196 if hrs.brd == nil { 197 hrs.brd = bufio.NewReader(hrs.rc) 198 } else { 199 hrs.brd.Reset(hrs.rc) 200 } 201 202 return hrs.brd, nil 203 }