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