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