github.com/10XDev/rclone@v1.52.3-0.20200626220027-16af9ab76b2a/backend/putio/fs.go (about)

     1  package putio
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/base64"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"path"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/pkg/errors"
    17  	"github.com/putdotio/go-putio/putio"
    18  	"github.com/rclone/rclone/fs"
    19  	"github.com/rclone/rclone/fs/config/configmap"
    20  	"github.com/rclone/rclone/fs/config/configstruct"
    21  	"github.com/rclone/rclone/fs/fshttp"
    22  	"github.com/rclone/rclone/fs/hash"
    23  	"github.com/rclone/rclone/lib/dircache"
    24  	"github.com/rclone/rclone/lib/oauthutil"
    25  	"github.com/rclone/rclone/lib/pacer"
    26  	"github.com/rclone/rclone/lib/readers"
    27  )
    28  
    29  // Fs represents a remote Putio server
    30  type Fs struct {
    31  	name        string             // name of this remote
    32  	root        string             // the path we are working on
    33  	features    *fs.Features       // optional features
    34  	opt         Options            // options for this Fs
    35  	client      *putio.Client      // client for making API calls to Put.io
    36  	pacer       *fs.Pacer          // To pace the API calls
    37  	dirCache    *dircache.DirCache // Map of directory path to directory id
    38  	httpClient  *http.Client       // base http client
    39  	oAuthClient *http.Client       // http client with oauth Authorization
    40  }
    41  
    42  // ------------------------------------------------------------
    43  
    44  // Name of the remote (as passed into NewFs)
    45  func (f *Fs) Name() string {
    46  	return f.name
    47  }
    48  
    49  // Root of the remote (as passed into NewFs)
    50  func (f *Fs) Root() string {
    51  	return f.root
    52  }
    53  
    54  // String converts this Fs to a string
    55  func (f *Fs) String() string {
    56  	return fmt.Sprintf("Putio root '%s'", f.root)
    57  }
    58  
    59  // Features returns the optional features of this Fs
    60  func (f *Fs) Features() *fs.Features {
    61  	return f.features
    62  }
    63  
    64  // parsePath parses a putio 'url'
    65  func parsePath(path string) (root string) {
    66  	root = strings.Trim(path, "/")
    67  	return
    68  }
    69  
    70  // NewFs constructs an Fs from the path, container:path
    71  func NewFs(name, root string, m configmap.Mapper) (f fs.Fs, err error) {
    72  	// defer log.Trace(name, "root=%v", root)("f=%+v, err=%v", &f, &err)
    73  	// Parse config into Options struct
    74  	opt := new(Options)
    75  	err = configstruct.Set(m, opt)
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  	root = parsePath(root)
    80  	httpClient := fshttp.NewClient(fs.Config)
    81  	oAuthClient, _, err := oauthutil.NewClientWithBaseClient(name, m, putioConfig, httpClient)
    82  	if err != nil {
    83  		return nil, errors.Wrap(err, "failed to configure putio")
    84  	}
    85  	p := &Fs{
    86  		name:        name,
    87  		root:        root,
    88  		opt:         *opt,
    89  		pacer:       fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
    90  		client:      putio.NewClient(oAuthClient),
    91  		httpClient:  httpClient,
    92  		oAuthClient: oAuthClient,
    93  	}
    94  	p.features = (&fs.Features{
    95  		DuplicateFiles:          true,
    96  		ReadMimeType:            true,
    97  		CanHaveEmptyDirectories: true,
    98  	}).Fill(p)
    99  	p.dirCache = dircache.New(root, "0", p)
   100  	ctx := context.Background()
   101  	// Find the current root
   102  	err = p.dirCache.FindRoot(ctx, false)
   103  	if err != nil {
   104  		// Assume it is a file
   105  		newRoot, remote := dircache.SplitPath(root)
   106  		tempF := *p
   107  		tempF.dirCache = dircache.New(newRoot, "0", &tempF)
   108  		tempF.root = newRoot
   109  		// Make new Fs which is the parent
   110  		err = tempF.dirCache.FindRoot(ctx, false)
   111  		if err != nil {
   112  			// No root so return old f
   113  			return p, nil
   114  		}
   115  		_, err := tempF.NewObject(ctx, remote)
   116  		if err != nil {
   117  			// unable to list folder so return old f
   118  			return p, nil
   119  		}
   120  		// XXX: update the old f here instead of returning tempF, since
   121  		// `features` were already filled with functions having *f as a receiver.
   122  		// See https://github.com/rclone/rclone/issues/2182
   123  		p.dirCache = tempF.dirCache
   124  		p.root = tempF.root
   125  		return p, fs.ErrorIsFile
   126  	}
   127  	// fs.Debugf(p, "Root id: %s", p.dirCache.RootID())
   128  	return p, nil
   129  }
   130  
   131  func itoa(i int64) string {
   132  	return strconv.FormatInt(i, 10)
   133  }
   134  
   135  func atoi(a string) int64 {
   136  	i, err := strconv.ParseInt(a, 10, 64)
   137  	if err != nil {
   138  		panic(err)
   139  	}
   140  	return i
   141  }
   142  
   143  // CreateDir makes a directory with pathID as parent and name leaf
   144  func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) {
   145  	// defer log.Trace(f, "pathID=%v, leaf=%v", pathID, leaf)("newID=%v, err=%v", newID, &err)
   146  	parentID := atoi(pathID)
   147  	var entry putio.File
   148  	err = f.pacer.Call(func() (bool, error) {
   149  		// fs.Debugf(f, "creating folder. part: %s, parentID: %d", leaf, parentID)
   150  		entry, err = f.client.Files.CreateFolder(ctx, f.opt.Enc.FromStandardName(leaf), parentID)
   151  		return shouldRetry(err)
   152  	})
   153  	return itoa(entry.ID), err
   154  }
   155  
   156  // FindLeaf finds a directory of name leaf in the folder with ID pathID
   157  func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) {
   158  	// defer log.Trace(f, "pathID=%v, leaf=%v", pathID, leaf)("pathIDOut=%v, found=%v, err=%v", pathIDOut, found, &err)
   159  	if pathID == "0" && leaf == "" {
   160  		// that's the root directory
   161  		return pathID, true, nil
   162  	}
   163  	fileID := atoi(pathID)
   164  	var children []putio.File
   165  	err = f.pacer.Call(func() (bool, error) {
   166  		// fs.Debugf(f, "listing file: %d", fileID)
   167  		children, _, err = f.client.Files.List(ctx, fileID)
   168  		return shouldRetry(err)
   169  	})
   170  	if err != nil {
   171  		if perr, ok := err.(*putio.ErrorResponse); ok && perr.Response.StatusCode == 404 {
   172  			err = nil
   173  		}
   174  		return
   175  	}
   176  	for _, child := range children {
   177  		if f.opt.Enc.ToStandardName(child.Name) == leaf {
   178  			found = true
   179  			pathIDOut = itoa(child.ID)
   180  			if !child.IsDir() {
   181  				err = fs.ErrorIsFile
   182  			}
   183  			return
   184  		}
   185  	}
   186  	return
   187  }
   188  
   189  // List the objects and directories in dir into entries.  The
   190  // entries can be returned in any order but should be for a
   191  // complete directory.
   192  //
   193  // dir should be "" to list the root, and should not have
   194  // trailing slashes.
   195  //
   196  // This should return ErrDirNotFound if the directory isn't
   197  // found.
   198  func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
   199  	// defer log.Trace(f, "dir=%v", dir)("err=%v", &err)
   200  	err = f.dirCache.FindRoot(ctx, false)
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  	directoryID, err := f.dirCache.FindDir(ctx, dir, false)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  	parentID := atoi(directoryID)
   209  	var children []putio.File
   210  	err = f.pacer.Call(func() (bool, error) {
   211  		// fs.Debugf(f, "listing files inside List: %d", parentID)
   212  		children, _, err = f.client.Files.List(ctx, parentID)
   213  		return shouldRetry(err)
   214  	})
   215  	if err != nil {
   216  		return
   217  	}
   218  	for _, child := range children {
   219  		remote := path.Join(dir, f.opt.Enc.ToStandardName(child.Name))
   220  		// fs.Debugf(f, "child: %s", remote)
   221  		if child.IsDir() {
   222  			f.dirCache.Put(remote, itoa(child.ID))
   223  			d := fs.NewDir(remote, child.UpdatedAt.Time)
   224  			entries = append(entries, d)
   225  		} else {
   226  			o, err := f.newObjectWithInfo(ctx, remote, child)
   227  			if err != nil {
   228  				return nil, err
   229  			}
   230  			entries = append(entries, o)
   231  		}
   232  	}
   233  	return
   234  }
   235  
   236  // Put the object
   237  //
   238  // Copy the reader in to the new object which is returned
   239  //
   240  // The new object may have been created if an error is returned
   241  func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (o fs.Object, err error) {
   242  	// defer log.Trace(f, "src=%+v", src)("o=%+v, err=%v", &o, &err)
   243  	exisitingObj, err := f.NewObject(ctx, src.Remote())
   244  	switch err {
   245  	case nil:
   246  		return exisitingObj, exisitingObj.Update(ctx, in, src, options...)
   247  	case fs.ErrorObjectNotFound:
   248  		// Not found so create it
   249  		return f.PutUnchecked(ctx, in, src, options...)
   250  	default:
   251  		return nil, err
   252  	}
   253  }
   254  
   255  // PutUnchecked uploads the object
   256  //
   257  // This will create a duplicate if we upload a new file without
   258  // checking to see if there is one already - use Put() for that.
   259  func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (o fs.Object, err error) {
   260  	// defer log.Trace(f, "src=%+v", src)("o=%+v, err=%v", &o, &err)
   261  	size := src.Size()
   262  	remote := src.Remote()
   263  	leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, remote, true)
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  	loc, err := f.createUpload(ctx, leaf, size, directoryID, src.ModTime(ctx), options)
   268  	if err != nil {
   269  		return nil, err
   270  	}
   271  	fileID, err := f.sendUpload(ctx, loc, size, in)
   272  	if err != nil {
   273  		return nil, err
   274  	}
   275  	var entry putio.File
   276  	err = f.pacer.Call(func() (bool, error) {
   277  		// fs.Debugf(f, "getting file: %d", fileID)
   278  		entry, err = f.client.Files.Get(ctx, fileID)
   279  		return shouldRetry(err)
   280  	})
   281  	if err != nil {
   282  		return nil, err
   283  	}
   284  	return f.newObjectWithInfo(ctx, remote, entry)
   285  }
   286  
   287  func (f *Fs) createUpload(ctx context.Context, name string, size int64, parentID string, modTime time.Time, options []fs.OpenOption) (location string, err error) {
   288  	// defer log.Trace(f, "name=%v, size=%v, parentID=%v, modTime=%v", name, size, parentID, modTime.String())("location=%v, err=%v", location, &err)
   289  	err = f.pacer.Call(func() (bool, error) {
   290  		req, err := http.NewRequest("POST", "https://upload.put.io/files/", nil)
   291  		if err != nil {
   292  			return false, err
   293  		}
   294  		req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
   295  		req.Header.Set("tus-resumable", "1.0.0")
   296  		req.Header.Set("upload-length", strconv.FormatInt(size, 10))
   297  		b64name := base64.StdEncoding.EncodeToString([]byte(f.opt.Enc.FromStandardName(name)))
   298  		b64true := base64.StdEncoding.EncodeToString([]byte("true"))
   299  		b64parentID := base64.StdEncoding.EncodeToString([]byte(parentID))
   300  		b64modifiedAt := base64.StdEncoding.EncodeToString([]byte(modTime.Format(time.RFC3339)))
   301  		req.Header.Set("upload-metadata", fmt.Sprintf("name %s,no-torrent %s,parent_id %s,updated-at %s", b64name, b64true, b64parentID, b64modifiedAt))
   302  		fs.OpenOptionAddHTTPHeaders(req.Header, options)
   303  		resp, err := f.oAuthClient.Do(req)
   304  		retry, err := shouldRetry(err)
   305  		if retry {
   306  			return true, err
   307  		}
   308  		if err != nil {
   309  			return false, err
   310  		}
   311  		if resp.StatusCode != 201 {
   312  			return false, fmt.Errorf("unexpected status code from upload create: %d", resp.StatusCode)
   313  		}
   314  		location = resp.Header.Get("location")
   315  		if location == "" {
   316  			return false, errors.New("empty location header from upload create")
   317  		}
   318  		return false, nil
   319  	})
   320  	return
   321  }
   322  
   323  func (f *Fs) sendUpload(ctx context.Context, location string, size int64, in io.Reader) (fileID int64, err error) {
   324  	// defer log.Trace(f, "location=%v, size=%v", location, size)("fileID=%v, err=%v", &fileID, &err)
   325  	if size == 0 {
   326  		err = f.pacer.Call(func() (bool, error) {
   327  			fs.Debugf(f, "Sending zero length chunk")
   328  			_, fileID, err = f.transferChunk(ctx, location, 0, bytes.NewReader([]byte{}), 0)
   329  			return shouldRetry(err)
   330  		})
   331  		return
   332  	}
   333  	var clientOffset int64
   334  	var offsetMismatch bool
   335  	buf := make([]byte, defaultChunkSize)
   336  	for clientOffset < size {
   337  		chunkSize := size - clientOffset
   338  		if chunkSize >= int64(defaultChunkSize) {
   339  			chunkSize = int64(defaultChunkSize)
   340  		}
   341  		chunk := readers.NewRepeatableLimitReaderBuffer(in, buf, chunkSize)
   342  		chunkStart := clientOffset
   343  		reqSize := chunkSize
   344  		transferOffset := clientOffset
   345  		fs.Debugf(f, "chunkStart: %d, reqSize: %d", chunkStart, reqSize)
   346  
   347  		// Transfer the chunk
   348  		err = f.pacer.Call(func() (bool, error) {
   349  			if offsetMismatch {
   350  				// Get file offset and seek to the position
   351  				offset, err := f.getServerOffset(ctx, location)
   352  				if err != nil {
   353  					return shouldRetry(err)
   354  				}
   355  				sentBytes := offset - chunkStart
   356  				fs.Debugf(f, "sentBytes: %d", sentBytes)
   357  				_, err = chunk.Seek(sentBytes, io.SeekStart)
   358  				if err != nil {
   359  					return shouldRetry(err)
   360  				}
   361  				transferOffset = offset
   362  				reqSize = chunkSize - sentBytes
   363  				offsetMismatch = false
   364  			}
   365  			fs.Debugf(f, "Sending chunk. transferOffset: %d length: %d", transferOffset, reqSize)
   366  			var serverOffset int64
   367  			serverOffset, fileID, err = f.transferChunk(ctx, location, transferOffset, chunk, reqSize)
   368  			if cerr, ok := err.(*statusCodeError); ok && cerr.response.StatusCode == 409 {
   369  				offsetMismatch = true
   370  				return true, err
   371  			}
   372  			if serverOffset != (transferOffset + reqSize) {
   373  				offsetMismatch = true
   374  				return true, errors.New("connection broken")
   375  			}
   376  			return shouldRetry(err)
   377  		})
   378  		if err != nil {
   379  			return
   380  		}
   381  
   382  		clientOffset += chunkSize
   383  	}
   384  	return
   385  }
   386  
   387  func (f *Fs) getServerOffset(ctx context.Context, location string) (offset int64, err error) {
   388  	// defer log.Trace(f, "location=%v", location)("offset=%v, err=%v", &offset, &err)
   389  	req, err := f.makeUploadHeadRequest(ctx, location)
   390  	if err != nil {
   391  		return 0, err
   392  	}
   393  	resp, err := f.oAuthClient.Do(req)
   394  	if err != nil {
   395  		return 0, err
   396  	}
   397  	err = checkStatusCode(resp, 200)
   398  	if err != nil {
   399  		return 0, err
   400  	}
   401  	return strconv.ParseInt(resp.Header.Get("upload-offset"), 10, 64)
   402  }
   403  
   404  func (f *Fs) transferChunk(ctx context.Context, location string, start int64, chunk io.ReadSeeker, chunkSize int64) (serverOffset, fileID int64, err error) {
   405  	// defer log.Trace(f, "location=%v, start=%v, chunkSize=%v", location, start, chunkSize)("fileID=%v, err=%v", &fileID, &err)
   406  	req, err := f.makeUploadPatchRequest(ctx, location, chunk, start, chunkSize)
   407  	if err != nil {
   408  		return
   409  	}
   410  	resp, err := f.oAuthClient.Do(req)
   411  	if err != nil {
   412  		return
   413  	}
   414  	defer func() {
   415  		_ = resp.Body.Close()
   416  	}()
   417  	err = checkStatusCode(resp, 204)
   418  	if err != nil {
   419  		return
   420  	}
   421  	serverOffset, err = strconv.ParseInt(resp.Header.Get("upload-offset"), 10, 64)
   422  	if err != nil {
   423  		return
   424  	}
   425  	sfid := resp.Header.Get("putio-file-id")
   426  	if sfid != "" {
   427  		fileID, err = strconv.ParseInt(sfid, 10, 64)
   428  		if err != nil {
   429  			return
   430  		}
   431  	}
   432  	return
   433  }
   434  
   435  func (f *Fs) makeUploadHeadRequest(ctx context.Context, location string) (*http.Request, error) {
   436  	req, err := http.NewRequest("HEAD", location, nil)
   437  	if err != nil {
   438  		return nil, err
   439  	}
   440  	req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
   441  	req.Header.Set("tus-resumable", "1.0.0")
   442  	return req, nil
   443  }
   444  
   445  func (f *Fs) makeUploadPatchRequest(ctx context.Context, location string, in io.Reader, offset, length int64) (*http.Request, error) {
   446  	req, err := http.NewRequest("PATCH", location, in)
   447  	if err != nil {
   448  		return nil, err
   449  	}
   450  	req = req.WithContext(ctx) // go1.13 can use NewRequestWithContext
   451  	req.Header.Set("tus-resumable", "1.0.0")
   452  	req.Header.Set("upload-offset", strconv.FormatInt(offset, 10))
   453  	req.Header.Set("content-length", strconv.FormatInt(length, 10))
   454  	req.Header.Set("content-type", "application/offset+octet-stream")
   455  	return req, nil
   456  }
   457  
   458  // Mkdir creates the container if it doesn't exist
   459  func (f *Fs) Mkdir(ctx context.Context, dir string) (err error) {
   460  	// defer log.Trace(f, "dir=%v", dir)("err=%v", &err)
   461  	err = f.dirCache.FindRoot(ctx, true)
   462  	if err != nil {
   463  		return err
   464  	}
   465  	if dir != "" {
   466  		_, err = f.dirCache.FindDir(ctx, dir, true)
   467  	}
   468  	return err
   469  }
   470  
   471  // Rmdir deletes the container
   472  //
   473  // Returns an error if it isn't empty
   474  func (f *Fs) Rmdir(ctx context.Context, dir string) (err error) {
   475  	// defer log.Trace(f, "dir=%v", dir)("err=%v", &err)
   476  
   477  	root := strings.Trim(path.Join(f.root, dir), "/")
   478  
   479  	// can't remove root
   480  	if root == "" {
   481  		return errors.New("can't remove root directory")
   482  	}
   483  
   484  	// check directory exists
   485  	directoryID, err := f.dirCache.FindDir(ctx, dir, false)
   486  	if err != nil {
   487  		return errors.Wrap(err, "Rmdir")
   488  	}
   489  	dirID := atoi(directoryID)
   490  
   491  	// check directory empty
   492  	var children []putio.File
   493  	err = f.pacer.Call(func() (bool, error) {
   494  		// fs.Debugf(f, "listing files: %d", dirID)
   495  		children, _, err = f.client.Files.List(ctx, dirID)
   496  		return shouldRetry(err)
   497  	})
   498  	if err != nil {
   499  		return errors.Wrap(err, "Rmdir")
   500  	}
   501  	if len(children) != 0 {
   502  		return errors.New("directory not empty")
   503  	}
   504  
   505  	// remove it
   506  	err = f.pacer.Call(func() (bool, error) {
   507  		// fs.Debugf(f, "deleting file: %d", dirID)
   508  		err = f.client.Files.Delete(ctx, dirID)
   509  		return shouldRetry(err)
   510  	})
   511  	f.dirCache.FlushDir(dir)
   512  	return err
   513  }
   514  
   515  // Precision returns the precision
   516  func (f *Fs) Precision() time.Duration {
   517  	return time.Second
   518  }
   519  
   520  // Purge deletes all the files and the container
   521  //
   522  // Optional interface: Only implement this if you have a way of
   523  // deleting all the files quicker than just running Remove() on the
   524  // result of List()
   525  func (f *Fs) Purge(ctx context.Context) (err error) {
   526  	// defer log.Trace(f, "")("err=%v", &err)
   527  
   528  	if f.root == "" {
   529  		return errors.New("can't purge root directory")
   530  	}
   531  	err = f.dirCache.FindRoot(ctx, false)
   532  	if err != nil {
   533  		return err
   534  	}
   535  
   536  	rootID := atoi(f.dirCache.RootID())
   537  	// Let putio delete the filesystem tree
   538  	err = f.pacer.Call(func() (bool, error) {
   539  		// fs.Debugf(f, "deleting file: %d", rootID)
   540  		err = f.client.Files.Delete(ctx, rootID)
   541  		return shouldRetry(err)
   542  	})
   543  	f.dirCache.ResetRoot()
   544  	return err
   545  }
   546  
   547  // Copy src to this remote using server side copy operations.
   548  //
   549  // This is stored with the remote path given
   550  //
   551  // It returns the destination Object and a possible error
   552  //
   553  // Will only be called if src.Fs().Name() == f.Name()
   554  //
   555  // If it isn't possible then return fs.ErrorCantCopy
   556  func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (o fs.Object, err error) {
   557  	// defer log.Trace(f, "src=%+v, remote=%v", src, remote)("o=%+v, err=%v", &o, &err)
   558  	srcObj, ok := src.(*Object)
   559  	if !ok {
   560  		return nil, fs.ErrorCantCopy
   561  	}
   562  	leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, remote, true)
   563  	if err != nil {
   564  		return nil, err
   565  	}
   566  	err = f.pacer.Call(func() (bool, error) {
   567  		params := url.Values{}
   568  		params.Set("file_id", strconv.FormatInt(srcObj.file.ID, 10))
   569  		params.Set("parent_id", directoryID)
   570  		params.Set("name", f.opt.Enc.FromStandardName(leaf))
   571  		req, err := f.client.NewRequest(ctx, "POST", "/v2/files/copy", strings.NewReader(params.Encode()))
   572  		if err != nil {
   573  			return false, err
   574  		}
   575  		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   576  		// fs.Debugf(f, "copying file (%d) to parent_id: %s", srcObj.file.ID, directoryID)
   577  		_, err = f.client.Do(req, nil)
   578  		return shouldRetry(err)
   579  	})
   580  	if err != nil {
   581  		return nil, err
   582  	}
   583  	return f.NewObject(ctx, remote)
   584  }
   585  
   586  // Move src to this remote using server side move operations.
   587  //
   588  // This is stored with the remote path given
   589  //
   590  // It returns the destination Object and a possible error
   591  //
   592  // Will only be called if src.Fs().Name() == f.Name()
   593  //
   594  // If it isn't possible then return fs.ErrorCantMove
   595  func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (o fs.Object, err error) {
   596  	// defer log.Trace(f, "src=%+v, remote=%v", src, remote)("o=%+v, err=%v", &o, &err)
   597  	srcObj, ok := src.(*Object)
   598  	if !ok {
   599  		return nil, fs.ErrorCantMove
   600  	}
   601  	leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, remote, true)
   602  	if err != nil {
   603  		return nil, err
   604  	}
   605  	err = f.pacer.Call(func() (bool, error) {
   606  		params := url.Values{}
   607  		params.Set("file_id", strconv.FormatInt(srcObj.file.ID, 10))
   608  		params.Set("parent_id", directoryID)
   609  		params.Set("name", f.opt.Enc.FromStandardName(leaf))
   610  		req, err := f.client.NewRequest(ctx, "POST", "/v2/files/move", strings.NewReader(params.Encode()))
   611  		if err != nil {
   612  			return false, err
   613  		}
   614  		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   615  		// fs.Debugf(f, "moving file (%d) to parent_id: %s", srcObj.file.ID, directoryID)
   616  		_, err = f.client.Do(req, nil)
   617  		return shouldRetry(err)
   618  	})
   619  	if err != nil {
   620  		return nil, err
   621  	}
   622  	return f.NewObject(ctx, remote)
   623  }
   624  
   625  // DirMove moves src, srcRemote to this remote at dstRemote
   626  // using server side move operations.
   627  //
   628  // Will only be called if src.Fs().Name() == f.Name()
   629  //
   630  // If it isn't possible then return fs.ErrorCantDirMove
   631  //
   632  // If destination exists then return fs.ErrorDirExists
   633  func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) (err error) {
   634  	// defer log.Trace(f, "src=%+v, srcRemote=%v, dstRemote", src, srcRemote, dstRemote)("err=%v", &err)
   635  	srcFs, ok := src.(*Fs)
   636  	if !ok {
   637  		return fs.ErrorCantDirMove
   638  	}
   639  	srcPath := path.Join(srcFs.root, srcRemote)
   640  	dstPath := path.Join(f.root, dstRemote)
   641  
   642  	// Refuse to move to or from the root
   643  	if srcPath == "" || dstPath == "" {
   644  		return errors.New("can't move root directory")
   645  	}
   646  
   647  	// find the root src directory
   648  	err = srcFs.dirCache.FindRoot(ctx, false)
   649  	if err != nil {
   650  		return err
   651  	}
   652  
   653  	// find the root dst directory
   654  	if dstRemote != "" {
   655  		err = f.dirCache.FindRoot(ctx, true)
   656  		if err != nil {
   657  			return err
   658  		}
   659  	} else {
   660  		if f.dirCache.FoundRoot() {
   661  			return fs.ErrorDirExists
   662  		}
   663  	}
   664  
   665  	// Find ID of dst parent, creating subdirs if necessary
   666  	var leaf, dstDirectoryID string
   667  	findPath := dstRemote
   668  	if dstRemote == "" {
   669  		findPath = f.root
   670  	}
   671  	leaf, dstDirectoryID, err = f.dirCache.FindPath(ctx, findPath, true)
   672  	if err != nil {
   673  		return err
   674  	}
   675  
   676  	// Check destination does not exist
   677  	if dstRemote != "" {
   678  		_, err = f.dirCache.FindDir(ctx, dstRemote, false)
   679  		if err == fs.ErrorDirNotFound {
   680  			// OK
   681  		} else if err != nil {
   682  			return err
   683  		} else {
   684  			return fs.ErrorDirExists
   685  		}
   686  	}
   687  
   688  	// Find ID of src
   689  	srcID, err := srcFs.dirCache.FindDir(ctx, srcRemote, false)
   690  	if err != nil {
   691  		return err
   692  	}
   693  
   694  	err = f.pacer.Call(func() (bool, error) {
   695  		params := url.Values{}
   696  		params.Set("file_id", srcID)
   697  		params.Set("parent_id", dstDirectoryID)
   698  		params.Set("name", f.opt.Enc.FromStandardName(leaf))
   699  		req, err := f.client.NewRequest(ctx, "POST", "/v2/files/move", strings.NewReader(params.Encode()))
   700  		if err != nil {
   701  			return false, err
   702  		}
   703  		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   704  		// fs.Debugf(f, "moving file (%s) to parent_id: %s", srcID, dstDirectoryID)
   705  		_, err = f.client.Do(req, nil)
   706  		return shouldRetry(err)
   707  	})
   708  	srcFs.dirCache.FlushDir(srcRemote)
   709  	return err
   710  }
   711  
   712  // About gets quota information
   713  func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
   714  	// defer log.Trace(f, "")("usage=%+v, err=%v", usage, &err)
   715  	var ai putio.AccountInfo
   716  	err = f.pacer.Call(func() (bool, error) {
   717  		// fs.Debugf(f, "getting account info")
   718  		ai, err = f.client.Account.Info(ctx)
   719  		return shouldRetry(err)
   720  	})
   721  	if err != nil {
   722  		return nil, errors.Wrap(err, "about failed")
   723  	}
   724  	return &fs.Usage{
   725  		Total: fs.NewUsageValue(ai.Disk.Size),  // quota of bytes that can be used
   726  		Used:  fs.NewUsageValue(ai.Disk.Used),  // bytes in use
   727  		Free:  fs.NewUsageValue(ai.Disk.Avail), // bytes which can be uploaded before reaching the quota
   728  	}, nil
   729  }
   730  
   731  // Hashes returns the supported hash sets.
   732  func (f *Fs) Hashes() hash.Set {
   733  	return hash.Set(hash.CRC32)
   734  }
   735  
   736  // DirCacheFlush resets the directory cache - used in testing as an
   737  // optional interface
   738  func (f *Fs) DirCacheFlush() {
   739  	// defer log.Trace(f, "")("")
   740  	f.dirCache.ResetRoot()
   741  }
   742  
   743  // CleanUp the trash in the Fs
   744  func (f *Fs) CleanUp(ctx context.Context) (err error) {
   745  	// defer log.Trace(f, "")("err=%v", &err)
   746  	return f.pacer.Call(func() (bool, error) {
   747  		req, err := f.client.NewRequest(ctx, "POST", "/v2/trash/empty", nil)
   748  		if err != nil {
   749  			return false, err
   750  		}
   751  		// fs.Debugf(f, "emptying trash")
   752  		_, err = f.client.Do(req, nil)
   753  		return shouldRetry(err)
   754  	})
   755  }