github.com/ncw/rclone@v1.48.1-0.20190724201158-a35aa1360e3e/fs/chunkedreader/chunkedreader.go (about) 1 package chunkedreader 2 3 import ( 4 "context" 5 "errors" 6 "io" 7 "sync" 8 9 "github.com/ncw/rclone/fs" 10 ) 11 12 // io related errors returned by ChunkedReader 13 var ( 14 ErrorFileClosed = errors.New("file already closed") 15 ErrorInvalidSeek = errors.New("invalid seek position") 16 ) 17 18 // ChunkedReader is a reader for a Object with the possibility 19 // of reading the source in chunks of given size 20 // 21 // A initialChunkSize of <= 0 will disable chunked reading. 22 type ChunkedReader struct { 23 ctx context.Context 24 mu sync.Mutex // protects following fields 25 o fs.Object // source to read from 26 rc io.ReadCloser // reader for the current open chunk 27 offset int64 // offset the next Read will start. -1 forces a reopen of o 28 chunkOffset int64 // beginning of the current or next chunk 29 chunkSize int64 // length of the current or next chunk. -1 will open o from chunkOffset to the end 30 initialChunkSize int64 // default chunkSize after the chunk specified by RangeSeek is complete 31 maxChunkSize int64 // consecutive read chunks will double in size until reached. -1 means no limit 32 customChunkSize bool // is the current chunkSize set by RangeSeek? 33 closed bool // has Close been called? 34 } 35 36 // New returns a ChunkedReader for the Object. 37 // 38 // A initialChunkSize of <= 0 will disable chunked reading. 39 // If maxChunkSize is greater than initialChunkSize, the chunk size will be 40 // doubled after each chunk read with a maximun of maxChunkSize. 41 // A Seek or RangeSeek will reset the chunk size to it's initial value 42 func New(ctx context.Context, o fs.Object, initialChunkSize int64, maxChunkSize int64) *ChunkedReader { 43 if initialChunkSize <= 0 { 44 initialChunkSize = -1 45 } 46 if maxChunkSize != -1 && maxChunkSize < initialChunkSize { 47 maxChunkSize = initialChunkSize 48 } 49 return &ChunkedReader{ 50 ctx: ctx, 51 o: o, 52 offset: -1, 53 chunkSize: initialChunkSize, 54 initialChunkSize: initialChunkSize, 55 maxChunkSize: maxChunkSize, 56 } 57 } 58 59 // Read from the file - for details see io.Reader 60 func (cr *ChunkedReader) Read(p []byte) (n int, err error) { 61 cr.mu.Lock() 62 defer cr.mu.Unlock() 63 64 if cr.closed { 65 return 0, ErrorFileClosed 66 } 67 68 for reqSize := int64(len(p)); reqSize > 0; reqSize = int64(len(p)) { 69 // the current chunk boundary. valid only when chunkSize > 0 70 chunkEnd := cr.chunkOffset + cr.chunkSize 71 72 fs.Debugf(cr.o, "ChunkedReader.Read at %d length %d chunkOffset %d chunkSize %d", cr.offset, reqSize, cr.chunkOffset, cr.chunkSize) 73 74 switch { 75 case cr.chunkSize > 0 && cr.offset == chunkEnd: // last chunk read completely 76 cr.chunkOffset = cr.offset 77 if cr.customChunkSize { // last chunkSize was set by RangeSeek 78 cr.customChunkSize = false 79 cr.chunkSize = cr.initialChunkSize 80 } else { 81 cr.chunkSize *= 2 82 if cr.chunkSize > cr.maxChunkSize && cr.maxChunkSize != -1 { 83 cr.chunkSize = cr.maxChunkSize 84 } 85 } 86 // recalculate the chunk boundary. valid only when chunkSize > 0 87 chunkEnd = cr.chunkOffset + cr.chunkSize 88 fallthrough 89 case cr.offset == -1: // first Read or Read after RangeSeek 90 err = cr.openRange() 91 if err != nil { 92 return 93 } 94 } 95 96 var buf []byte 97 chunkRest := chunkEnd - cr.offset 98 // limit read to chunk boundaries if chunkSize > 0 99 if reqSize > chunkRest && cr.chunkSize > 0 { 100 buf, p = p[0:chunkRest], p[chunkRest:] 101 } else { 102 buf, p = p, nil 103 } 104 var rn int 105 rn, err = io.ReadFull(cr.rc, buf) 106 n += rn 107 cr.offset += int64(rn) 108 if err != nil { 109 if err == io.ErrUnexpectedEOF { 110 err = io.EOF 111 } 112 return 113 } 114 } 115 return n, nil 116 } 117 118 // Close the file - for details see io.Closer 119 // 120 // All methods on ChunkedReader will return ErrorFileClosed afterwards 121 func (cr *ChunkedReader) Close() error { 122 cr.mu.Lock() 123 defer cr.mu.Unlock() 124 125 if cr.closed { 126 return ErrorFileClosed 127 } 128 cr.closed = true 129 130 return cr.resetReader(nil, 0) 131 } 132 133 // Seek the file - for details see io.Seeker 134 func (cr *ChunkedReader) Seek(offset int64, whence int) (int64, error) { 135 return cr.RangeSeek(context.TODO(), offset, whence, -1) 136 } 137 138 // RangeSeek the file - for details see RangeSeeker 139 // 140 // The specified length will only apply to the next chunk opened. 141 // RangeSeek will not reopen the source until Read is called. 142 func (cr *ChunkedReader) RangeSeek(ctx context.Context, offset int64, whence int, length int64) (int64, error) { 143 cr.mu.Lock() 144 defer cr.mu.Unlock() 145 146 fs.Debugf(cr.o, "ChunkedReader.RangeSeek from %d to %d length %d", cr.offset, offset, length) 147 148 if cr.closed { 149 return 0, ErrorFileClosed 150 } 151 152 size := cr.o.Size() 153 switch whence { 154 case io.SeekStart: 155 cr.offset = 0 156 case io.SeekEnd: 157 cr.offset = size 158 } 159 // set the new chunk start 160 cr.chunkOffset = cr.offset + offset 161 // force reopen on next Read 162 cr.offset = -1 163 if length > 0 { 164 cr.customChunkSize = true 165 cr.chunkSize = length 166 } else { 167 cr.chunkSize = cr.initialChunkSize 168 } 169 if cr.chunkOffset < 0 || cr.chunkOffset >= size { 170 cr.chunkOffset = 0 171 return 0, ErrorInvalidSeek 172 } 173 return cr.chunkOffset, nil 174 } 175 176 // Open forces the connection to be opened 177 func (cr *ChunkedReader) Open() (*ChunkedReader, error) { 178 cr.mu.Lock() 179 defer cr.mu.Unlock() 180 181 if cr.rc != nil && cr.offset != -1 { 182 return cr, nil 183 } 184 return cr, cr.openRange() 185 } 186 187 // openRange will open the source Object with the current chunk range 188 // 189 // If the current open reader implements RangeSeeker, it is tried first. 190 // When RangeSeek fails, o.Open with a RangeOption is used. 191 // 192 // A length <= 0 will request till the end of the file 193 func (cr *ChunkedReader) openRange() error { 194 offset, length := cr.chunkOffset, cr.chunkSize 195 fs.Debugf(cr.o, "ChunkedReader.openRange at %d length %d", offset, length) 196 197 if cr.closed { 198 return ErrorFileClosed 199 } 200 201 if rs, ok := cr.rc.(fs.RangeSeeker); ok { 202 n, err := rs.RangeSeek(cr.ctx, offset, io.SeekStart, length) 203 if err == nil && n == offset { 204 cr.offset = offset 205 return nil 206 } 207 if err != nil { 208 fs.Debugf(cr.o, "ChunkedReader.openRange seek failed (%s). Trying Open", err) 209 } else { 210 fs.Debugf(cr.o, "ChunkedReader.openRange seeked to wrong offset. Wanted %d, got %d. Trying Open", offset, n) 211 } 212 } 213 214 var rc io.ReadCloser 215 var err error 216 if length <= 0 { 217 if offset == 0 { 218 rc, err = cr.o.Open(cr.ctx) 219 } else { 220 rc, err = cr.o.Open(cr.ctx, &fs.RangeOption{Start: offset, End: -1}) 221 } 222 } else { 223 rc, err = cr.o.Open(cr.ctx, &fs.RangeOption{Start: offset, End: offset + length - 1}) 224 } 225 if err != nil { 226 return err 227 } 228 return cr.resetReader(rc, offset) 229 } 230 231 // resetReader switches the current reader to the given reader. 232 // The old reader will be Close'd before setting the new reader. 233 func (cr *ChunkedReader) resetReader(rc io.ReadCloser, offset int64) error { 234 if cr.rc != nil { 235 if err := cr.rc.Close(); err != nil { 236 return err 237 } 238 } 239 cr.rc = rc 240 cr.offset = offset 241 return nil 242 } 243 244 var ( 245 _ io.ReadCloser = (*ChunkedReader)(nil) 246 _ io.Seeker = (*ChunkedReader)(nil) 247 _ fs.RangeSeeker = (*ChunkedReader)(nil) 248 )