2019-04-25 00:04:40 +08:00
|
|
|
package operations
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"io"
|
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
2019-07-29 01:47:38 +08:00
|
|
|
"github.com/rclone/rclone/fs"
|
|
|
|
"github.com/rclone/rclone/fs/accounting"
|
2019-04-25 00:04:40 +08:00
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
multithreadChunkSize = 64 << 10
|
|
|
|
multithreadChunkSizeMask = multithreadChunkSize - 1
|
|
|
|
multithreadBufferSize = 32 * 1024
|
|
|
|
)
|
|
|
|
|
2019-08-13 05:09:40 +08:00
|
|
|
// Return a boolean as to whether we should use multi thread copy for
|
|
|
|
// this transfer
|
2020-11-05 19:33:32 +08:00
|
|
|
func doMultiThreadCopy(ctx context.Context, f fs.Fs, src fs.Object) bool {
|
|
|
|
ci := fs.GetConfig(ctx)
|
|
|
|
|
2019-08-13 05:09:40 +08:00
|
|
|
// Disable multi thread if...
|
|
|
|
|
|
|
|
// ...it isn't configured
|
2020-11-05 19:33:32 +08:00
|
|
|
if ci.MultiThreadStreams <= 1 {
|
2019-08-13 05:09:40 +08:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
// ...size of object is less than cutoff
|
2020-11-05 19:33:32 +08:00
|
|
|
if src.Size() < int64(ci.MultiThreadCutoff) {
|
2019-08-13 05:09:40 +08:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
// ...source doesn't support it
|
|
|
|
dstFeatures := f.Features()
|
|
|
|
if dstFeatures.OpenWriterAt == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
// ...if --multi-thread-streams not in use and source and
|
|
|
|
// destination are both local
|
2020-11-05 19:33:32 +08:00
|
|
|
if !ci.MultiThreadSet && dstFeatures.IsLocal && src.Fs().Features().IsLocal {
|
2019-08-13 05:09:40 +08:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2019-04-25 00:04:40 +08:00
|
|
|
// state for a multi-thread copy
|
|
|
|
type multiThreadCopyState struct {
|
|
|
|
ctx context.Context
|
|
|
|
partSize int64
|
|
|
|
size int64
|
|
|
|
wc fs.WriterAtCloser
|
|
|
|
src fs.Object
|
|
|
|
acc *accounting.Account
|
|
|
|
streams int
|
|
|
|
}
|
|
|
|
|
|
|
|
// Copy a single stream into place
|
2019-06-17 16:34:30 +08:00
|
|
|
func (mc *multiThreadCopyState) copyStream(ctx context.Context, stream int) (err error) {
|
2020-11-05 19:33:32 +08:00
|
|
|
ci := fs.GetConfig(ctx)
|
2019-04-25 00:04:40 +08:00
|
|
|
defer func() {
|
|
|
|
if err != nil {
|
|
|
|
fs.Debugf(mc.src, "multi-thread copy: stream %d/%d failed: %v", stream+1, mc.streams, err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
start := int64(stream) * mc.partSize
|
|
|
|
if start >= mc.size {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
end := start + mc.partSize
|
|
|
|
if end > mc.size {
|
|
|
|
end = mc.size
|
|
|
|
}
|
|
|
|
|
|
|
|
fs.Debugf(mc.src, "multi-thread copy: stream %d/%d (%d-%d) size %v starting", stream+1, mc.streams, start, end, fs.SizeSuffix(end-start))
|
|
|
|
|
2020-11-05 19:33:32 +08:00
|
|
|
rc, err := NewReOpen(ctx, mc.src, ci.LowLevelRetries, &fs.RangeOption{Start: start, End: end - 1})
|
2019-04-25 00:04:40 +08:00
|
|
|
if err != nil {
|
Spelling fixes
Fix spelling of: above, already, anonymous, associated,
authentication, bandwidth, because, between, blocks, calculate,
candidates, cautious, changelog, cleaner, clipboard, command,
completely, concurrently, considered, constructs, corrupt, current,
daemon, dependencies, deprecated, directory, dispatcher, download,
eligible, ellipsis, encrypter, endpoint, entrieslist, essentially,
existing writers, existing, expires, filesystem, flushing, frequently,
hierarchy, however, implementation, implements, inaccurate,
individually, insensitive, longer, maximum, metadata, modified,
multipart, namedirfirst, nextcloud, obscured, opened, optional,
owncloud, pacific, passphrase, password, permanently, persimmon,
positive, potato, protocol, quota, receiving, recommends, referring,
requires, revisited, satisfied, satisfies, satisfy, semver,
serialized, session, storage, strategies, stringlist, successful,
supported, surprise, temporarily, temporary, transactions, unneeded,
update, uploads, wrapped
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2020-10-09 08:17:24 +08:00
|
|
|
return errors.Wrap(err, "multipart copy: failed to open source")
|
2019-04-25 00:04:40 +08:00
|
|
|
}
|
|
|
|
defer fs.CheckClose(rc, &err)
|
|
|
|
|
|
|
|
// Copy the data
|
|
|
|
buf := make([]byte, multithreadBufferSize)
|
|
|
|
offset := start
|
|
|
|
for {
|
|
|
|
// Check if context cancelled and exit if so
|
|
|
|
if mc.ctx.Err() != nil {
|
|
|
|
return mc.ctx.Err()
|
|
|
|
}
|
|
|
|
nr, er := rc.Read(buf)
|
|
|
|
if nr > 0 {
|
|
|
|
err = mc.acc.AccountRead(nr)
|
|
|
|
if err != nil {
|
Spelling fixes
Fix spelling of: above, already, anonymous, associated,
authentication, bandwidth, because, between, blocks, calculate,
candidates, cautious, changelog, cleaner, clipboard, command,
completely, concurrently, considered, constructs, corrupt, current,
daemon, dependencies, deprecated, directory, dispatcher, download,
eligible, ellipsis, encrypter, endpoint, entrieslist, essentially,
existing writers, existing, expires, filesystem, flushing, frequently,
hierarchy, however, implementation, implements, inaccurate,
individually, insensitive, longer, maximum, metadata, modified,
multipart, namedirfirst, nextcloud, obscured, opened, optional,
owncloud, pacific, passphrase, password, permanently, persimmon,
positive, potato, protocol, quota, receiving, recommends, referring,
requires, revisited, satisfied, satisfies, satisfy, semver,
serialized, session, storage, strategies, stringlist, successful,
supported, surprise, temporarily, temporary, transactions, unneeded,
update, uploads, wrapped
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2020-10-09 08:17:24 +08:00
|
|
|
return errors.Wrap(err, "multipart copy: accounting failed")
|
2019-04-25 00:04:40 +08:00
|
|
|
}
|
|
|
|
nw, ew := mc.wc.WriteAt(buf[0:nr], offset)
|
|
|
|
if nw > 0 {
|
|
|
|
offset += int64(nw)
|
|
|
|
}
|
|
|
|
if ew != nil {
|
Spelling fixes
Fix spelling of: above, already, anonymous, associated,
authentication, bandwidth, because, between, blocks, calculate,
candidates, cautious, changelog, cleaner, clipboard, command,
completely, concurrently, considered, constructs, corrupt, current,
daemon, dependencies, deprecated, directory, dispatcher, download,
eligible, ellipsis, encrypter, endpoint, entrieslist, essentially,
existing writers, existing, expires, filesystem, flushing, frequently,
hierarchy, however, implementation, implements, inaccurate,
individually, insensitive, longer, maximum, metadata, modified,
multipart, namedirfirst, nextcloud, obscured, opened, optional,
owncloud, pacific, passphrase, password, permanently, persimmon,
positive, potato, protocol, quota, receiving, recommends, referring,
requires, revisited, satisfied, satisfies, satisfy, semver,
serialized, session, storage, strategies, stringlist, successful,
supported, surprise, temporarily, temporary, transactions, unneeded,
update, uploads, wrapped
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2020-10-09 08:17:24 +08:00
|
|
|
return errors.Wrap(ew, "multipart copy: write failed")
|
2019-04-25 00:04:40 +08:00
|
|
|
}
|
|
|
|
if nr != nw {
|
Spelling fixes
Fix spelling of: above, already, anonymous, associated,
authentication, bandwidth, because, between, blocks, calculate,
candidates, cautious, changelog, cleaner, clipboard, command,
completely, concurrently, considered, constructs, corrupt, current,
daemon, dependencies, deprecated, directory, dispatcher, download,
eligible, ellipsis, encrypter, endpoint, entrieslist, essentially,
existing writers, existing, expires, filesystem, flushing, frequently,
hierarchy, however, implementation, implements, inaccurate,
individually, insensitive, longer, maximum, metadata, modified,
multipart, namedirfirst, nextcloud, obscured, opened, optional,
owncloud, pacific, passphrase, password, permanently, persimmon,
positive, potato, protocol, quota, receiving, recommends, referring,
requires, revisited, satisfied, satisfies, satisfy, semver,
serialized, session, storage, strategies, stringlist, successful,
supported, surprise, temporarily, temporary, transactions, unneeded,
update, uploads, wrapped
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2020-10-09 08:17:24 +08:00
|
|
|
return errors.Wrap(io.ErrShortWrite, "multipart copy")
|
2019-04-25 00:04:40 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if er != nil {
|
|
|
|
if er != io.EOF {
|
Spelling fixes
Fix spelling of: above, already, anonymous, associated,
authentication, bandwidth, because, between, blocks, calculate,
candidates, cautious, changelog, cleaner, clipboard, command,
completely, concurrently, considered, constructs, corrupt, current,
daemon, dependencies, deprecated, directory, dispatcher, download,
eligible, ellipsis, encrypter, endpoint, entrieslist, essentially,
existing writers, existing, expires, filesystem, flushing, frequently,
hierarchy, however, implementation, implements, inaccurate,
individually, insensitive, longer, maximum, metadata, modified,
multipart, namedirfirst, nextcloud, obscured, opened, optional,
owncloud, pacific, passphrase, password, permanently, persimmon,
positive, potato, protocol, quota, receiving, recommends, referring,
requires, revisited, satisfied, satisfies, satisfy, semver,
serialized, session, storage, strategies, stringlist, successful,
supported, surprise, temporarily, temporary, transactions, unneeded,
update, uploads, wrapped
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2020-10-09 08:17:24 +08:00
|
|
|
return errors.Wrap(er, "multipart copy: read failed")
|
2019-04-25 00:04:40 +08:00
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if offset != end {
|
Spelling fixes
Fix spelling of: above, already, anonymous, associated,
authentication, bandwidth, because, between, blocks, calculate,
candidates, cautious, changelog, cleaner, clipboard, command,
completely, concurrently, considered, constructs, corrupt, current,
daemon, dependencies, deprecated, directory, dispatcher, download,
eligible, ellipsis, encrypter, endpoint, entrieslist, essentially,
existing writers, existing, expires, filesystem, flushing, frequently,
hierarchy, however, implementation, implements, inaccurate,
individually, insensitive, longer, maximum, metadata, modified,
multipart, namedirfirst, nextcloud, obscured, opened, optional,
owncloud, pacific, passphrase, password, permanently, persimmon,
positive, potato, protocol, quota, receiving, recommends, referring,
requires, revisited, satisfied, satisfies, satisfy, semver,
serialized, session, storage, strategies, stringlist, successful,
supported, surprise, temporarily, temporary, transactions, unneeded,
update, uploads, wrapped
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2020-10-09 08:17:24 +08:00
|
|
|
return errors.Errorf("multipart copy: wrote %d bytes but expected to write %d", offset-start, end-start)
|
2019-04-25 00:04:40 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
fs.Debugf(mc.src, "multi-thread copy: stream %d/%d (%d-%d) size %v finished", stream+1, mc.streams, start, end, fs.SizeSuffix(end-start))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate the chunk sizes and updated number of streams
|
|
|
|
func (mc *multiThreadCopyState) calculateChunks() {
|
|
|
|
partSize := mc.size / int64(mc.streams)
|
|
|
|
// Round partition size up so partSize * streams >= size
|
|
|
|
if (mc.size % int64(mc.streams)) != 0 {
|
|
|
|
partSize++
|
|
|
|
}
|
|
|
|
// round partSize up to nearest multithreadChunkSize boundary
|
|
|
|
mc.partSize = (partSize + multithreadChunkSizeMask) &^ multithreadChunkSizeMask
|
|
|
|
// recalculate number of streams
|
|
|
|
mc.streams = int(mc.size / mc.partSize)
|
|
|
|
// round streams up so partSize * streams >= size
|
|
|
|
if (mc.size % mc.partSize) != 0 {
|
|
|
|
mc.streams++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Copy src to (f, remote) using streams download threads and the OpenWriterAt feature
|
2019-07-16 19:56:20 +08:00
|
|
|
func multiThreadCopy(ctx context.Context, f fs.Fs, remote string, src fs.Object, streams int, tr *accounting.Transfer) (newDst fs.Object, err error) {
|
2019-04-25 00:04:40 +08:00
|
|
|
openWriterAt := f.Features().OpenWriterAt
|
|
|
|
if openWriterAt == nil {
|
|
|
|
return nil, errors.New("multi-thread copy: OpenWriterAt not supported")
|
|
|
|
}
|
|
|
|
if src.Size() < 0 {
|
|
|
|
return nil, errors.New("multi-thread copy: can't copy unknown sized file")
|
|
|
|
}
|
|
|
|
if src.Size() == 0 {
|
|
|
|
return nil, errors.New("multi-thread copy: can't copy zero sized file")
|
|
|
|
}
|
|
|
|
|
2019-07-01 16:33:21 +08:00
|
|
|
g, gCtx := errgroup.WithContext(ctx)
|
2019-04-25 00:04:40 +08:00
|
|
|
mc := &multiThreadCopyState{
|
2019-07-01 16:33:21 +08:00
|
|
|
ctx: gCtx,
|
2019-04-25 00:04:40 +08:00
|
|
|
size: src.Size(),
|
|
|
|
src: src,
|
|
|
|
streams: streams,
|
|
|
|
}
|
|
|
|
mc.calculateChunks()
|
|
|
|
|
|
|
|
// Make accounting
|
2020-06-04 22:09:03 +08:00
|
|
|
mc.acc = tr.Account(ctx, nil)
|
2019-04-25 00:04:40 +08:00
|
|
|
|
|
|
|
// create write file handle
|
2019-07-01 16:33:21 +08:00
|
|
|
mc.wc, err = openWriterAt(gCtx, remote, mc.size)
|
2019-04-25 00:04:40 +08:00
|
|
|
if err != nil {
|
Spelling fixes
Fix spelling of: above, already, anonymous, associated,
authentication, bandwidth, because, between, blocks, calculate,
candidates, cautious, changelog, cleaner, clipboard, command,
completely, concurrently, considered, constructs, corrupt, current,
daemon, dependencies, deprecated, directory, dispatcher, download,
eligible, ellipsis, encrypter, endpoint, entrieslist, essentially,
existing writers, existing, expires, filesystem, flushing, frequently,
hierarchy, however, implementation, implements, inaccurate,
individually, insensitive, longer, maximum, metadata, modified,
multipart, namedirfirst, nextcloud, obscured, opened, optional,
owncloud, pacific, passphrase, password, permanently, persimmon,
positive, potato, protocol, quota, receiving, recommends, referring,
requires, revisited, satisfied, satisfies, satisfy, semver,
serialized, session, storage, strategies, stringlist, successful,
supported, surprise, temporarily, temporary, transactions, unneeded,
update, uploads, wrapped
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2020-10-09 08:17:24 +08:00
|
|
|
return nil, errors.Wrap(err, "multipart copy: failed to open destination")
|
2019-04-25 00:04:40 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
fs.Debugf(src, "Starting multi-thread copy with %d parts of size %v", mc.streams, fs.SizeSuffix(mc.partSize))
|
|
|
|
for stream := 0; stream < mc.streams; stream++ {
|
|
|
|
stream := stream
|
|
|
|
g.Go(func() (err error) {
|
2019-07-01 16:33:21 +08:00
|
|
|
return mc.copyStream(gCtx, stream)
|
2019-04-25 00:04:40 +08:00
|
|
|
})
|
|
|
|
}
|
|
|
|
err = g.Wait()
|
2020-02-24 18:22:09 +08:00
|
|
|
closeErr := mc.wc.Close()
|
2019-04-25 00:04:40 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-02-24 18:22:09 +08:00
|
|
|
if closeErr != nil {
|
|
|
|
return nil, errors.Wrap(closeErr, "multi-thread copy: failed to close object after copy")
|
|
|
|
}
|
2019-04-25 00:04:40 +08:00
|
|
|
|
2019-06-17 16:34:30 +08:00
|
|
|
obj, err := f.NewObject(ctx, remote)
|
2019-04-25 00:04:40 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "multi-thread copy: failed to find object after copy")
|
|
|
|
}
|
|
|
|
|
2019-06-17 16:34:30 +08:00
|
|
|
err = obj.SetModTime(ctx, src.ModTime(ctx))
|
2019-04-25 00:04:40 +08:00
|
|
|
switch err {
|
|
|
|
case nil, fs.ErrorCantSetModTime, fs.ErrorCantSetModTimeWithoutDelete:
|
|
|
|
default:
|
|
|
|
return nil, errors.Wrap(err, "multi-thread copy: failed to set modification time")
|
|
|
|
}
|
|
|
|
|
|
|
|
fs.Debugf(src, "Finished multi-thread copy with %d parts of size %v", mc.streams, fs.SizeSuffix(mc.partSize))
|
|
|
|
return obj, nil
|
|
|
|
}
|