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  )