github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/backend/pcloud/pcloud.go (about)

     1  // Package pcloud provides an interface to the Pcloud
     2  // object storage system.
     3  package pcloud
     4  
     5  // FIXME implement ListR? /listfolder can do recursive lists
     6  
     7  // FIXME cleanup returns login required?
     8  
     9  // FIXME mime type? Fix overview if implement.
    10  
    11  import (
    12  	"context"
    13  	"fmt"
    14  	"io"
    15  	"log"
    16  	"net/http"
    17  	"net/url"
    18  	"path"
    19  	"strings"
    20  	"time"
    21  
    22  	"github.com/pkg/errors"
    23  	"github.com/rclone/rclone/backend/pcloud/api"
    24  	"github.com/rclone/rclone/fs"
    25  	"github.com/rclone/rclone/fs/config"
    26  	"github.com/rclone/rclone/fs/config/configmap"
    27  	"github.com/rclone/rclone/fs/config/configstruct"
    28  	"github.com/rclone/rclone/fs/config/obscure"
    29  	"github.com/rclone/rclone/fs/fserrors"
    30  	"github.com/rclone/rclone/fs/hash"
    31  	"github.com/rclone/rclone/lib/dircache"
    32  	"github.com/rclone/rclone/lib/encoder"
    33  	"github.com/rclone/rclone/lib/oauthutil"
    34  	"github.com/rclone/rclone/lib/pacer"
    35  	"github.com/rclone/rclone/lib/rest"
    36  	"golang.org/x/oauth2"
    37  )
    38  
    39  const (
    40  	rcloneClientID              = "DnONSzyJXpm"
    41  	rcloneEncryptedClientSecret = "ej1OIF39VOQQ0PXaSdK9ztkLw3tdLNscW2157TKNQdQKkICR4uU7aFg4eFM"
    42  	minSleep                    = 10 * time.Millisecond
    43  	maxSleep                    = 2 * time.Second
    44  	decayConstant               = 2    // bigger for slower decay, exponential
    45  	rootID                      = "d0" // ID of root folder is always this
    46  	rootURL                     = "https://api.pcloud.com"
    47  )
    48  
    49  // Globals
    50  var (
    51  	// Description of how to auth for this app
    52  	oauthConfig = &oauth2.Config{
    53  		Scopes: nil,
    54  		Endpoint: oauth2.Endpoint{
    55  			AuthURL:  "https://my.pcloud.com/oauth2/authorize",
    56  			TokenURL: "https://api.pcloud.com/oauth2_token",
    57  		},
    58  		ClientID:     rcloneClientID,
    59  		ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
    60  		RedirectURL:  oauthutil.RedirectLocalhostURL,
    61  	}
    62  )
    63  
    64  // Register with Fs
    65  func init() {
    66  	fs.Register(&fs.RegInfo{
    67  		Name:        "pcloud",
    68  		Description: "Pcloud",
    69  		NewFs:       NewFs,
    70  		Config: func(name string, m configmap.Mapper) {
    71  			err := oauthutil.Config("pcloud", name, m, oauthConfig)
    72  			if err != nil {
    73  				log.Fatalf("Failed to configure token: %v", err)
    74  			}
    75  		},
    76  		Options: []fs.Option{{
    77  			Name: config.ConfigClientID,
    78  			Help: "Pcloud App Client Id\nLeave blank normally.",
    79  		}, {
    80  			Name: config.ConfigClientSecret,
    81  			Help: "Pcloud App Client Secret\nLeave blank normally.",
    82  		}, {
    83  			Name:     config.ConfigEncoding,
    84  			Help:     config.ConfigEncodingHelp,
    85  			Advanced: true,
    86  			// Encode invalid UTF-8 bytes as json doesn't handle them properly.
    87  			//
    88  			// TODO: Investigate Unicode simplification (\ gets converted to \ server-side)
    89  			Default: (encoder.Display |
    90  				encoder.EncodeBackSlash |
    91  				encoder.EncodeInvalidUtf8),
    92  		}},
    93  	})
    94  }
    95  
    96  // Options defines the configuration for this backend
    97  type Options struct {
    98  	Enc encoder.MultiEncoder `config:"encoding"`
    99  }
   100  
   101  // Fs represents a remote pcloud
   102  type Fs struct {
   103  	name         string             // name of this remote
   104  	root         string             // the path we are working on
   105  	opt          Options            // parsed options
   106  	features     *fs.Features       // optional features
   107  	srv          *rest.Client       // the connection to the server
   108  	dirCache     *dircache.DirCache // Map of directory path to directory id
   109  	pacer        *fs.Pacer          // pacer for API calls
   110  	tokenRenewer *oauthutil.Renew   // renew the token on expiry
   111  }
   112  
   113  // Object describes a pcloud object
   114  //
   115  // Will definitely have info but maybe not meta
   116  type Object struct {
   117  	fs          *Fs       // what this object is part of
   118  	remote      string    // The remote path
   119  	hasMetaData bool      // whether info below has been set
   120  	size        int64     // size of the object
   121  	modTime     time.Time // modification time of the object
   122  	id          string    // ID of the object
   123  	md5         string    // MD5 if known
   124  	sha1        string    // SHA1 if known
   125  	link        *api.GetFileLinkResult
   126  }
   127  
   128  // ------------------------------------------------------------
   129  
   130  // Name of the remote (as passed into NewFs)
   131  func (f *Fs) Name() string {
   132  	return f.name
   133  }
   134  
   135  // Root of the remote (as passed into NewFs)
   136  func (f *Fs) Root() string {
   137  	return f.root
   138  }
   139  
   140  // String converts this Fs to a string
   141  func (f *Fs) String() string {
   142  	return fmt.Sprintf("pcloud root '%s'", f.root)
   143  }
   144  
   145  // Features returns the optional features of this Fs
   146  func (f *Fs) Features() *fs.Features {
   147  	return f.features
   148  }
   149  
   150  // parsePath parses an pcloud 'url'
   151  func parsePath(path string) (root string) {
   152  	root = strings.Trim(path, "/")
   153  	return
   154  }
   155  
   156  // retryErrorCodes is a slice of error codes that we will retry
   157  var retryErrorCodes = []int{
   158  	429, // Too Many Requests.
   159  	500, // Internal Server Error
   160  	502, // Bad Gateway
   161  	503, // Service Unavailable
   162  	504, // Gateway Timeout
   163  	509, // Bandwidth Limit Exceeded
   164  }
   165  
   166  // shouldRetry returns a boolean as to whether this resp and err
   167  // deserve to be retried.  It returns the err as a convenience
   168  func shouldRetry(resp *http.Response, err error) (bool, error) {
   169  	doRetry := false
   170  
   171  	// Check if it is an api.Error
   172  	if apiErr, ok := err.(*api.Error); ok {
   173  		// See https://docs.pcloud.com/errors/ for error treatment
   174  		// Errors are classified as 1xxx, 2xxx etc
   175  		switch apiErr.Result / 1000 {
   176  		case 4: // 4xxx: rate limiting
   177  			doRetry = true
   178  		case 5: // 5xxx: internal errors
   179  			doRetry = true
   180  		}
   181  	}
   182  
   183  	if resp != nil && resp.StatusCode == 401 && len(resp.Header["Www-Authenticate"]) == 1 && strings.Index(resp.Header["Www-Authenticate"][0], "expired_token") >= 0 {
   184  		doRetry = true
   185  		fs.Debugf(nil, "Should retry: %v", err)
   186  	}
   187  	return doRetry || fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
   188  }
   189  
   190  // readMetaDataForPath reads the metadata from the path
   191  func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.Item, err error) {
   192  	// defer fs.Trace(f, "path=%q", path)("info=%+v, err=%v", &info, &err)
   193  	leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, path, false)
   194  	if err != nil {
   195  		if err == fs.ErrorDirNotFound {
   196  			return nil, fs.ErrorObjectNotFound
   197  		}
   198  		return nil, err
   199  	}
   200  
   201  	found, err := f.listAll(ctx, directoryID, false, true, func(item *api.Item) bool {
   202  		if item.Name == leaf {
   203  			info = item
   204  			return true
   205  		}
   206  		return false
   207  	})
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  	if !found {
   212  		return nil, fs.ErrorObjectNotFound
   213  	}
   214  	return info, nil
   215  }
   216  
   217  // errorHandler parses a non 2xx error response into an error
   218  func errorHandler(resp *http.Response) error {
   219  	// Decode error response
   220  	errResponse := new(api.Error)
   221  	err := rest.DecodeJSON(resp, &errResponse)
   222  	if err != nil {
   223  		fs.Debugf(nil, "Couldn't decode error response: %v", err)
   224  	}
   225  	if errResponse.ErrorString == "" {
   226  		errResponse.ErrorString = resp.Status
   227  	}
   228  	if errResponse.Result == 0 {
   229  		errResponse.Result = resp.StatusCode
   230  	}
   231  	return errResponse
   232  }
   233  
   234  // NewFs constructs an Fs from the path, container:path
   235  func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
   236  	ctx := context.Background()
   237  	// Parse config into Options struct
   238  	opt := new(Options)
   239  	err := configstruct.Set(m, opt)
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  	root = parsePath(root)
   244  	oAuthClient, ts, err := oauthutil.NewClient(name, m, oauthConfig)
   245  	if err != nil {
   246  		return nil, errors.Wrap(err, "failed to configure Pcloud")
   247  	}
   248  
   249  	f := &Fs{
   250  		name:  name,
   251  		root:  root,
   252  		opt:   *opt,
   253  		srv:   rest.NewClient(oAuthClient).SetRoot(rootURL),
   254  		pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
   255  	}
   256  	f.features = (&fs.Features{
   257  		CaseInsensitive:         false,
   258  		CanHaveEmptyDirectories: true,
   259  	}).Fill(f)
   260  	f.srv.SetErrorHandler(errorHandler)
   261  
   262  	// Renew the token in the background
   263  	f.tokenRenewer = oauthutil.NewRenew(f.String(), ts, func() error {
   264  		_, err := f.readMetaDataForPath(ctx, "")
   265  		return err
   266  	})
   267  
   268  	// Get rootID
   269  	f.dirCache = dircache.New(root, rootID, f)
   270  
   271  	// Find the current root
   272  	err = f.dirCache.FindRoot(ctx, false)
   273  	if err != nil {
   274  		// Assume it is a file
   275  		newRoot, remote := dircache.SplitPath(root)
   276  		tempF := *f
   277  		tempF.dirCache = dircache.New(newRoot, rootID, &tempF)
   278  		tempF.root = newRoot
   279  		// Make new Fs which is the parent
   280  		err = tempF.dirCache.FindRoot(ctx, false)
   281  		if err != nil {
   282  			// No root so return old f
   283  			return f, nil
   284  		}
   285  		_, err := tempF.newObjectWithInfo(ctx, remote, nil)
   286  		if err != nil {
   287  			if err == fs.ErrorObjectNotFound {
   288  				// File doesn't exist so return old f
   289  				return f, nil
   290  			}
   291  			return nil, err
   292  		}
   293  		// XXX: update the old f here instead of returning tempF, since
   294  		// `features` were already filled with functions having *f as a receiver.
   295  		// See https://github.com/rclone/rclone/issues/2182
   296  		f.dirCache = tempF.dirCache
   297  		f.root = tempF.root
   298  		// return an error with an fs which points to the parent
   299  		return f, fs.ErrorIsFile
   300  	}
   301  	return f, nil
   302  }
   303  
   304  // Return an Object from a path
   305  //
   306  // If it can't be found it returns the error fs.ErrorObjectNotFound.
   307  func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.Item) (fs.Object, error) {
   308  	o := &Object{
   309  		fs:     f,
   310  		remote: remote,
   311  	}
   312  	var err error
   313  	if info != nil {
   314  		// Set info
   315  		err = o.setMetaData(info)
   316  	} else {
   317  		err = o.readMetaData(ctx) // reads info and meta, returning an error
   318  	}
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  	return o, nil
   323  }
   324  
   325  // NewObject finds the Object at remote.  If it can't be found
   326  // it returns the error fs.ErrorObjectNotFound.
   327  func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
   328  	return f.newObjectWithInfo(ctx, remote, nil)
   329  }
   330  
   331  // FindLeaf finds a directory of name leaf in the folder with ID pathID
   332  func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) {
   333  	// Find the leaf in pathID
   334  	found, err = f.listAll(ctx, pathID, true, false, func(item *api.Item) bool {
   335  		if item.Name == leaf {
   336  			pathIDOut = item.ID
   337  			return true
   338  		}
   339  		return false
   340  	})
   341  	return pathIDOut, found, err
   342  }
   343  
   344  // CreateDir makes a directory with pathID as parent and name leaf
   345  func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) {
   346  	// fs.Debugf(f, "CreateDir(%q, %q)\n", pathID, leaf)
   347  	var resp *http.Response
   348  	var result api.ItemResult
   349  	opts := rest.Opts{
   350  		Method:     "POST",
   351  		Path:       "/createfolder",
   352  		Parameters: url.Values{},
   353  	}
   354  	opts.Parameters.Set("name", f.opt.Enc.FromStandardName(leaf))
   355  	opts.Parameters.Set("folderid", dirIDtoNumber(pathID))
   356  	err = f.pacer.Call(func() (bool, error) {
   357  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   358  		err = result.Error.Update(err)
   359  		return shouldRetry(resp, err)
   360  	})
   361  	if err != nil {
   362  		//fmt.Printf("...Error %v\n", err)
   363  		return "", err
   364  	}
   365  	// fmt.Printf("...Id %q\n", *info.Id)
   366  	return result.Metadata.ID, nil
   367  }
   368  
   369  // Converts a dirID which is usually 'd' followed by digits into just
   370  // the digits
   371  func dirIDtoNumber(dirID string) string {
   372  	if len(dirID) > 0 && dirID[0] == 'd' {
   373  		return dirID[1:]
   374  	}
   375  	fs.Debugf(nil, "Invalid directory id %q", dirID)
   376  	return dirID
   377  }
   378  
   379  // Converts a fileID which is usually 'f' followed by digits into just
   380  // the digits
   381  func fileIDtoNumber(fileID string) string {
   382  	if len(fileID) > 0 && fileID[0] == 'f' {
   383  		return fileID[1:]
   384  	}
   385  	fs.Debugf(nil, "Invalid file id %q", fileID)
   386  	return fileID
   387  }
   388  
   389  // list the objects into the function supplied
   390  //
   391  // If directories is set it only sends directories
   392  // User function to process a File item from listAll
   393  //
   394  // Should return true to finish processing
   395  type listAllFn func(*api.Item) bool
   396  
   397  // Lists the directory required calling the user function on each item found
   398  //
   399  // If the user fn ever returns true then it early exits with found = true
   400  func (f *Fs) listAll(ctx context.Context, dirID string, directoriesOnly bool, filesOnly bool, fn listAllFn) (found bool, err error) {
   401  	opts := rest.Opts{
   402  		Method:     "GET",
   403  		Path:       "/listfolder",
   404  		Parameters: url.Values{},
   405  	}
   406  	opts.Parameters.Set("folderid", dirIDtoNumber(dirID))
   407  	// FIXME can do recursive
   408  
   409  	var result api.ItemResult
   410  	var resp *http.Response
   411  	err = f.pacer.Call(func() (bool, error) {
   412  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   413  		err = result.Error.Update(err)
   414  		return shouldRetry(resp, err)
   415  	})
   416  	if err != nil {
   417  		return found, errors.Wrap(err, "couldn't list files")
   418  	}
   419  	for i := range result.Metadata.Contents {
   420  		item := &result.Metadata.Contents[i]
   421  		if item.IsFolder {
   422  			if filesOnly {
   423  				continue
   424  			}
   425  		} else {
   426  			if directoriesOnly {
   427  				continue
   428  			}
   429  		}
   430  		item.Name = f.opt.Enc.ToStandardName(item.Name)
   431  		if fn(item) {
   432  			found = true
   433  			break
   434  		}
   435  	}
   436  	return
   437  }
   438  
   439  // List the objects and directories in dir into entries.  The
   440  // entries can be returned in any order but should be for a
   441  // complete directory.
   442  //
   443  // dir should be "" to list the root, and should not have
   444  // trailing slashes.
   445  //
   446  // This should return ErrDirNotFound if the directory isn't
   447  // found.
   448  func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
   449  	err = f.dirCache.FindRoot(ctx, false)
   450  	if err != nil {
   451  		return nil, err
   452  	}
   453  	directoryID, err := f.dirCache.FindDir(ctx, dir, false)
   454  	if err != nil {
   455  		return nil, err
   456  	}
   457  	var iErr error
   458  	_, err = f.listAll(ctx, directoryID, false, false, func(info *api.Item) bool {
   459  		remote := path.Join(dir, info.Name)
   460  		if info.IsFolder {
   461  			// cache the directory ID for later lookups
   462  			f.dirCache.Put(remote, info.ID)
   463  			d := fs.NewDir(remote, info.ModTime()).SetID(info.ID)
   464  			// FIXME more info from dir?
   465  			entries = append(entries, d)
   466  		} else {
   467  			o, err := f.newObjectWithInfo(ctx, remote, info)
   468  			if err != nil {
   469  				iErr = err
   470  				return true
   471  			}
   472  			entries = append(entries, o)
   473  		}
   474  		return false
   475  	})
   476  	if err != nil {
   477  		return nil, err
   478  	}
   479  	if iErr != nil {
   480  		return nil, iErr
   481  	}
   482  	return entries, nil
   483  }
   484  
   485  // Creates from the parameters passed in a half finished Object which
   486  // must have setMetaData called on it
   487  //
   488  // Returns the object, leaf, directoryID and error
   489  //
   490  // Used to create new objects
   491  func (f *Fs) createObject(ctx context.Context, remote string, modTime time.Time, size int64) (o *Object, leaf string, directoryID string, err error) {
   492  	// Create the directory for the object if it doesn't exist
   493  	leaf, directoryID, err = f.dirCache.FindRootAndPath(ctx, remote, true)
   494  	if err != nil {
   495  		return
   496  	}
   497  	// Temporary Object under construction
   498  	o = &Object{
   499  		fs:     f,
   500  		remote: remote,
   501  	}
   502  	return o, leaf, directoryID, nil
   503  }
   504  
   505  // Put the object into the container
   506  //
   507  // Copy the reader in to the new object which is returned
   508  //
   509  // The new object may have been created if an error is returned
   510  func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   511  	remote := src.Remote()
   512  	size := src.Size()
   513  	modTime := src.ModTime(ctx)
   514  
   515  	o, _, _, err := f.createObject(ctx, remote, modTime, size)
   516  	if err != nil {
   517  		return nil, err
   518  	}
   519  	return o, o.Update(ctx, in, src, options...)
   520  }
   521  
   522  // Mkdir creates the container if it doesn't exist
   523  func (f *Fs) Mkdir(ctx context.Context, dir string) error {
   524  	err := f.dirCache.FindRoot(ctx, true)
   525  	if err != nil {
   526  		return err
   527  	}
   528  	if dir != "" {
   529  		_, err = f.dirCache.FindDir(ctx, dir, true)
   530  	}
   531  	return err
   532  }
   533  
   534  // purgeCheck removes the root directory, if check is set then it
   535  // refuses to do so if it has anything in
   536  func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
   537  	root := path.Join(f.root, dir)
   538  	if root == "" {
   539  		return errors.New("can't purge root directory")
   540  	}
   541  	dc := f.dirCache
   542  	err := dc.FindRoot(ctx, false)
   543  	if err != nil {
   544  		return err
   545  	}
   546  	rootID, err := dc.FindDir(ctx, dir, false)
   547  	if err != nil {
   548  		return err
   549  	}
   550  
   551  	opts := rest.Opts{
   552  		Method:     "POST",
   553  		Path:       "/deletefolder",
   554  		Parameters: url.Values{},
   555  	}
   556  	opts.Parameters.Set("folderid", dirIDtoNumber(rootID))
   557  	if !check {
   558  		opts.Path = "/deletefolderrecursive"
   559  	}
   560  	var resp *http.Response
   561  	var result api.ItemResult
   562  	err = f.pacer.Call(func() (bool, error) {
   563  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   564  		err = result.Error.Update(err)
   565  		return shouldRetry(resp, err)
   566  	})
   567  	if err != nil {
   568  		return errors.Wrap(err, "rmdir failed")
   569  	}
   570  	f.dirCache.FlushDir(dir)
   571  	if err != nil {
   572  		return err
   573  	}
   574  	return nil
   575  }
   576  
   577  // Rmdir deletes the root folder
   578  //
   579  // Returns an error if it isn't empty
   580  func (f *Fs) Rmdir(ctx context.Context, dir string) error {
   581  	return f.purgeCheck(ctx, dir, true)
   582  }
   583  
   584  // Precision return the precision of this Fs
   585  func (f *Fs) Precision() time.Duration {
   586  	return time.Second
   587  }
   588  
   589  // Copy src to this remote using server side copy operations.
   590  //
   591  // This is stored with the remote path given
   592  //
   593  // It returns the destination Object and a possible error
   594  //
   595  // Will only be called if src.Fs().Name() == f.Name()
   596  //
   597  // If it isn't possible then return fs.ErrorCantCopy
   598  func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
   599  	srcObj, ok := src.(*Object)
   600  	if !ok {
   601  		fs.Debugf(src, "Can't copy - not same remote type")
   602  		return nil, fs.ErrorCantCopy
   603  	}
   604  	err := srcObj.readMetaData(ctx)
   605  	if err != nil {
   606  		return nil, err
   607  	}
   608  
   609  	// Create temporary object
   610  	dstObj, leaf, directoryID, err := f.createObject(ctx, remote, srcObj.modTime, srcObj.size)
   611  	if err != nil {
   612  		return nil, err
   613  	}
   614  
   615  	// Copy the object
   616  	opts := rest.Opts{
   617  		Method:     "POST",
   618  		Path:       "/copyfile",
   619  		Parameters: url.Values{},
   620  	}
   621  	opts.Parameters.Set("fileid", fileIDtoNumber(srcObj.id))
   622  	opts.Parameters.Set("toname", f.opt.Enc.FromStandardName(leaf))
   623  	opts.Parameters.Set("tofolderid", dirIDtoNumber(directoryID))
   624  	opts.Parameters.Set("mtime", fmt.Sprintf("%d", srcObj.modTime.Unix()))
   625  	var resp *http.Response
   626  	var result api.ItemResult
   627  	err = f.pacer.Call(func() (bool, error) {
   628  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   629  		err = result.Error.Update(err)
   630  		return shouldRetry(resp, err)
   631  	})
   632  	if err != nil {
   633  		return nil, err
   634  	}
   635  	err = dstObj.setMetaData(&result.Metadata)
   636  	if err != nil {
   637  		return nil, err
   638  	}
   639  	return dstObj, nil
   640  }
   641  
   642  // Purge deletes all the files and the container
   643  //
   644  // Optional interface: Only implement this if you have a way of
   645  // deleting all the files quicker than just running Remove() on the
   646  // result of List()
   647  func (f *Fs) Purge(ctx context.Context) error {
   648  	return f.purgeCheck(ctx, "", false)
   649  }
   650  
   651  // CleanUp empties the trash
   652  func (f *Fs) CleanUp(ctx context.Context) error {
   653  	err := f.dirCache.FindRoot(ctx, false)
   654  	if err != nil {
   655  		return err
   656  	}
   657  	opts := rest.Opts{
   658  		Method:     "POST",
   659  		Path:       "/trash_clear",
   660  		Parameters: url.Values{},
   661  	}
   662  	opts.Parameters.Set("folderid", dirIDtoNumber(f.dirCache.RootID()))
   663  	var resp *http.Response
   664  	var result api.Error
   665  	return f.pacer.Call(func() (bool, error) {
   666  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   667  		err = result.Update(err)
   668  		return shouldRetry(resp, err)
   669  	})
   670  }
   671  
   672  // Move src to this remote using server side move operations.
   673  //
   674  // This is stored with the remote path given
   675  //
   676  // It returns the destination Object and a possible error
   677  //
   678  // Will only be called if src.Fs().Name() == f.Name()
   679  //
   680  // If it isn't possible then return fs.ErrorCantMove
   681  func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
   682  	srcObj, ok := src.(*Object)
   683  	if !ok {
   684  		fs.Debugf(src, "Can't move - not same remote type")
   685  		return nil, fs.ErrorCantMove
   686  	}
   687  
   688  	// Create temporary object
   689  	dstObj, leaf, directoryID, err := f.createObject(ctx, remote, srcObj.modTime, srcObj.size)
   690  	if err != nil {
   691  		return nil, err
   692  	}
   693  
   694  	// Do the move
   695  	opts := rest.Opts{
   696  		Method:     "POST",
   697  		Path:       "/renamefile",
   698  		Parameters: url.Values{},
   699  	}
   700  	opts.Parameters.Set("fileid", fileIDtoNumber(srcObj.id))
   701  	opts.Parameters.Set("toname", f.opt.Enc.FromStandardName(leaf))
   702  	opts.Parameters.Set("tofolderid", dirIDtoNumber(directoryID))
   703  	var resp *http.Response
   704  	var result api.ItemResult
   705  	err = f.pacer.Call(func() (bool, error) {
   706  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   707  		err = result.Error.Update(err)
   708  		return shouldRetry(resp, err)
   709  	})
   710  	if err != nil {
   711  		return nil, err
   712  	}
   713  
   714  	err = dstObj.setMetaData(&result.Metadata)
   715  	if err != nil {
   716  		return nil, err
   717  	}
   718  	return dstObj, nil
   719  }
   720  
   721  // DirMove moves src, srcRemote to this remote at dstRemote
   722  // using server side move operations.
   723  //
   724  // Will only be called if src.Fs().Name() == f.Name()
   725  //
   726  // If it isn't possible then return fs.ErrorCantDirMove
   727  //
   728  // If destination exists then return fs.ErrorDirExists
   729  func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
   730  	srcFs, ok := src.(*Fs)
   731  	if !ok {
   732  		fs.Debugf(srcFs, "Can't move directory - not same remote type")
   733  		return fs.ErrorCantDirMove
   734  	}
   735  	srcPath := path.Join(srcFs.root, srcRemote)
   736  	dstPath := path.Join(f.root, dstRemote)
   737  
   738  	// Refuse to move to or from the root
   739  	if srcPath == "" || dstPath == "" {
   740  		fs.Debugf(src, "DirMove error: Can't move root")
   741  		return errors.New("can't move root directory")
   742  	}
   743  
   744  	// find the root src directory
   745  	err := srcFs.dirCache.FindRoot(ctx, false)
   746  	if err != nil {
   747  		return err
   748  	}
   749  
   750  	// find the root dst directory
   751  	if dstRemote != "" {
   752  		err = f.dirCache.FindRoot(ctx, true)
   753  		if err != nil {
   754  			return err
   755  		}
   756  	} else {
   757  		if f.dirCache.FoundRoot() {
   758  			return fs.ErrorDirExists
   759  		}
   760  	}
   761  
   762  	// Find ID of dst parent, creating subdirs if necessary
   763  	var leaf, directoryID string
   764  	findPath := dstRemote
   765  	if dstRemote == "" {
   766  		findPath = f.root
   767  	}
   768  	leaf, directoryID, err = f.dirCache.FindPath(ctx, findPath, true)
   769  	if err != nil {
   770  		return err
   771  	}
   772  
   773  	// Check destination does not exist
   774  	if dstRemote != "" {
   775  		_, err = f.dirCache.FindDir(ctx, dstRemote, false)
   776  		if err == fs.ErrorDirNotFound {
   777  			// OK
   778  		} else if err != nil {
   779  			return err
   780  		} else {
   781  			return fs.ErrorDirExists
   782  		}
   783  	}
   784  
   785  	// Find ID of src
   786  	srcID, err := srcFs.dirCache.FindDir(ctx, srcRemote, false)
   787  	if err != nil {
   788  		return err
   789  	}
   790  
   791  	// Do the move
   792  	opts := rest.Opts{
   793  		Method:     "POST",
   794  		Path:       "/renamefolder",
   795  		Parameters: url.Values{},
   796  	}
   797  	opts.Parameters.Set("folderid", dirIDtoNumber(srcID))
   798  	opts.Parameters.Set("toname", f.opt.Enc.FromStandardName(leaf))
   799  	opts.Parameters.Set("tofolderid", dirIDtoNumber(directoryID))
   800  	var resp *http.Response
   801  	var result api.ItemResult
   802  	err = f.pacer.Call(func() (bool, error) {
   803  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
   804  		err = result.Error.Update(err)
   805  		return shouldRetry(resp, err)
   806  	})
   807  	if err != nil {
   808  		return err
   809  	}
   810  
   811  	srcFs.dirCache.FlushDir(srcRemote)
   812  	return nil
   813  }
   814  
   815  // DirCacheFlush resets the directory cache - used in testing as an
   816  // optional interface
   817  func (f *Fs) DirCacheFlush() {
   818  	f.dirCache.ResetRoot()
   819  }
   820  
   821  // About gets quota information
   822  func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
   823  	opts := rest.Opts{
   824  		Method: "POST",
   825  		Path:   "/userinfo",
   826  	}
   827  	var resp *http.Response
   828  	var q api.UserInfo
   829  	err = f.pacer.Call(func() (bool, error) {
   830  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &q)
   831  		err = q.Error.Update(err)
   832  		return shouldRetry(resp, err)
   833  	})
   834  	if err != nil {
   835  		return nil, errors.Wrap(err, "about failed")
   836  	}
   837  	usage = &fs.Usage{
   838  		Total: fs.NewUsageValue(q.Quota),               // quota of bytes that can be used
   839  		Used:  fs.NewUsageValue(q.UsedQuota),           // bytes in use
   840  		Free:  fs.NewUsageValue(q.Quota - q.UsedQuota), // bytes which can be uploaded before reaching the quota
   841  	}
   842  	return usage, nil
   843  }
   844  
   845  // Hashes returns the supported hash sets.
   846  func (f *Fs) Hashes() hash.Set {
   847  	return hash.Set(hash.MD5 | hash.SHA1)
   848  }
   849  
   850  // ------------------------------------------------------------
   851  
   852  // Fs returns the parent Fs
   853  func (o *Object) Fs() fs.Info {
   854  	return o.fs
   855  }
   856  
   857  // Return a string version
   858  func (o *Object) String() string {
   859  	if o == nil {
   860  		return "<nil>"
   861  	}
   862  	return o.remote
   863  }
   864  
   865  // Remote returns the remote path
   866  func (o *Object) Remote() string {
   867  	return o.remote
   868  }
   869  
   870  // getHashes fetches the hashes into the object
   871  func (o *Object) getHashes(ctx context.Context) (err error) {
   872  	var resp *http.Response
   873  	var result api.ChecksumFileResult
   874  	opts := rest.Opts{
   875  		Method:     "GET",
   876  		Path:       "/checksumfile",
   877  		Parameters: url.Values{},
   878  	}
   879  	opts.Parameters.Set("fileid", fileIDtoNumber(o.id))
   880  	err = o.fs.pacer.Call(func() (bool, error) {
   881  		resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &result)
   882  		err = result.Error.Update(err)
   883  		return shouldRetry(resp, err)
   884  	})
   885  	if err != nil {
   886  		return err
   887  	}
   888  	o.setHashes(&result.Hashes)
   889  	return o.setMetaData(&result.Metadata)
   890  }
   891  
   892  // Hash returns the SHA-1 of an object returning a lowercase hex string
   893  func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) {
   894  	if t != hash.MD5 && t != hash.SHA1 {
   895  		return "", hash.ErrUnsupported
   896  	}
   897  	if o.md5 == "" && o.sha1 == "" {
   898  		err := o.getHashes(ctx)
   899  		if err != nil {
   900  			return "", errors.Wrap(err, "failed to get hash")
   901  		}
   902  	}
   903  	if t == hash.MD5 {
   904  		return o.md5, nil
   905  	}
   906  	return o.sha1, nil
   907  }
   908  
   909  // Size returns the size of an object in bytes
   910  func (o *Object) Size() int64 {
   911  	err := o.readMetaData(context.TODO())
   912  	if err != nil {
   913  		fs.Logf(o, "Failed to read metadata: %v", err)
   914  		return 0
   915  	}
   916  	return o.size
   917  }
   918  
   919  // setMetaData sets the metadata from info
   920  func (o *Object) setMetaData(info *api.Item) (err error) {
   921  	if info.IsFolder {
   922  		return errors.Wrapf(fs.ErrorNotAFile, "%q is a folder", o.remote)
   923  	}
   924  	o.hasMetaData = true
   925  	o.size = info.Size
   926  	o.modTime = info.ModTime()
   927  	o.id = info.ID
   928  	return nil
   929  }
   930  
   931  // setHashes sets the hashes from that passed in
   932  func (o *Object) setHashes(hashes *api.Hashes) {
   933  	o.sha1 = hashes.SHA1
   934  	o.md5 = hashes.MD5
   935  }
   936  
   937  // readMetaData gets the metadata if it hasn't already been fetched
   938  //
   939  // it also sets the info
   940  func (o *Object) readMetaData(ctx context.Context) (err error) {
   941  	if o.hasMetaData {
   942  		return nil
   943  	}
   944  	info, err := o.fs.readMetaDataForPath(ctx, o.remote)
   945  	if err != nil {
   946  		//if apiErr, ok := err.(*api.Error); ok {
   947  		// FIXME
   948  		// if apiErr.Code == "not_found" || apiErr.Code == "trashed" {
   949  		// 	return fs.ErrorObjectNotFound
   950  		// }
   951  		//}
   952  		return err
   953  	}
   954  	return o.setMetaData(info)
   955  }
   956  
   957  // ModTime returns the modification time of the object
   958  //
   959  //
   960  // It attempts to read the objects mtime and if that isn't present the
   961  // LastModified returned in the http headers
   962  func (o *Object) ModTime(ctx context.Context) time.Time {
   963  	err := o.readMetaData(ctx)
   964  	if err != nil {
   965  		fs.Logf(o, "Failed to read metadata: %v", err)
   966  		return time.Now()
   967  	}
   968  	return o.modTime
   969  }
   970  
   971  // SetModTime sets the modification time of the local fs object
   972  func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
   973  	// Pcloud doesn't have a way of doing this so returning this
   974  	// error will cause the file to be re-uploaded to set the time.
   975  	return fs.ErrorCantSetModTime
   976  }
   977  
   978  // Storable returns a boolean showing whether this object storable
   979  func (o *Object) Storable() bool {
   980  	return true
   981  }
   982  
   983  // downloadURL fetches the download link
   984  func (o *Object) downloadURL(ctx context.Context) (URL string, err error) {
   985  	if o.id == "" {
   986  		return "", errors.New("can't download - no id")
   987  	}
   988  	if o.link.IsValid() {
   989  		return o.link.URL(), nil
   990  	}
   991  	var resp *http.Response
   992  	var result api.GetFileLinkResult
   993  	opts := rest.Opts{
   994  		Method:     "GET",
   995  		Path:       "/getfilelink",
   996  		Parameters: url.Values{},
   997  	}
   998  	opts.Parameters.Set("fileid", fileIDtoNumber(o.id))
   999  	err = o.fs.pacer.Call(func() (bool, error) {
  1000  		resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &result)
  1001  		err = result.Error.Update(err)
  1002  		return shouldRetry(resp, err)
  1003  	})
  1004  	if err != nil {
  1005  		return "", err
  1006  	}
  1007  	if !result.IsValid() {
  1008  		return "", errors.Errorf("fetched invalid link %+v", result)
  1009  	}
  1010  	o.link = &result
  1011  	return o.link.URL(), nil
  1012  }
  1013  
  1014  // Open an object for read
  1015  func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
  1016  	url, err := o.downloadURL(ctx)
  1017  	if err != nil {
  1018  		return nil, err
  1019  	}
  1020  	var resp *http.Response
  1021  	opts := rest.Opts{
  1022  		Method:  "GET",
  1023  		RootURL: url,
  1024  		Options: options,
  1025  	}
  1026  	err = o.fs.pacer.Call(func() (bool, error) {
  1027  		resp, err = o.fs.srv.Call(ctx, &opts)
  1028  		return shouldRetry(resp, err)
  1029  	})
  1030  	if err != nil {
  1031  		return nil, err
  1032  	}
  1033  	return resp.Body, err
  1034  }
  1035  
  1036  // Update the object with the contents of the io.Reader, modTime and size
  1037  //
  1038  // If existing is set then it updates the object rather than creating a new one
  1039  //
  1040  // The new object may have been created if an error is returned
  1041  func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) {
  1042  	o.fs.tokenRenewer.Start()
  1043  	defer o.fs.tokenRenewer.Stop()
  1044  
  1045  	size := src.Size() // NB can upload without size
  1046  	modTime := src.ModTime(ctx)
  1047  	remote := o.Remote()
  1048  
  1049  	// Create the directory for the object if it doesn't exist
  1050  	leaf, directoryID, err := o.fs.dirCache.FindRootAndPath(ctx, remote, true)
  1051  	if err != nil {
  1052  		return err
  1053  	}
  1054  
  1055  	// Experiments with pcloud indicate that it doesn't like any
  1056  	// form of request which doesn't have a Content-Length.
  1057  	// According to the docs if you close the connection at the
  1058  	// end then it should work without Content-Length, but I
  1059  	// couldn't get this to work using opts.Close (which sets
  1060  	// http.Request.Close).
  1061  	//
  1062  	// This means that chunked transfer encoding needs to be
  1063  	// disabled and a Content-Length needs to be supplied.  This
  1064  	// also rules out streaming.
  1065  	//
  1066  	// Docs: https://docs.pcloud.com/methods/file/uploadfile.html
  1067  	var resp *http.Response
  1068  	var result api.UploadFileResponse
  1069  	opts := rest.Opts{
  1070  		Method:           "PUT",
  1071  		Path:             "/uploadfile",
  1072  		Body:             in,
  1073  		ContentType:      fs.MimeType(ctx, o),
  1074  		ContentLength:    &size,
  1075  		Parameters:       url.Values{},
  1076  		TransferEncoding: []string{"identity"}, // pcloud doesn't like chunked encoding
  1077  	}
  1078  	leaf = o.fs.opt.Enc.FromStandardName(leaf)
  1079  	opts.Parameters.Set("filename", leaf)
  1080  	opts.Parameters.Set("folderid", dirIDtoNumber(directoryID))
  1081  	opts.Parameters.Set("nopartial", "1")
  1082  	opts.Parameters.Set("mtime", fmt.Sprintf("%d", modTime.Unix()))
  1083  
  1084  	// Special treatment for a 0 length upload.  This doesn't work
  1085  	// with PUT even with Content-Length set (by setting
  1086  	// opts.Body=0), so upload it as a multpart form POST with
  1087  	// Content-Length set.
  1088  	if size == 0 {
  1089  		formReader, contentType, overhead, err := rest.MultipartUpload(in, opts.Parameters, "content", leaf)
  1090  		if err != nil {
  1091  			return errors.Wrap(err, "failed to make multipart upload for 0 length file")
  1092  		}
  1093  
  1094  		contentLength := overhead + size
  1095  
  1096  		opts.ContentType = contentType
  1097  		opts.Body = formReader
  1098  		opts.Method = "POST"
  1099  		opts.Parameters = nil
  1100  		opts.ContentLength = &contentLength
  1101  	}
  1102  
  1103  	err = o.fs.pacer.CallNoRetry(func() (bool, error) {
  1104  		resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &result)
  1105  		err = result.Error.Update(err)
  1106  		return shouldRetry(resp, err)
  1107  	})
  1108  	if err != nil {
  1109  		// sometimes pcloud leaves a half complete file on
  1110  		// error, so delete it if it exists
  1111  		delObj, delErr := o.fs.NewObject(ctx, o.remote)
  1112  		if delErr == nil && delObj != nil {
  1113  			_ = delObj.Remove(ctx)
  1114  		}
  1115  		return err
  1116  	}
  1117  	if len(result.Items) != 1 {
  1118  		return errors.Errorf("failed to upload %v - not sure why", o)
  1119  	}
  1120  	o.setHashes(&result.Checksums[0])
  1121  	return o.setMetaData(&result.Items[0])
  1122  }
  1123  
  1124  // Remove an object
  1125  func (o *Object) Remove(ctx context.Context) error {
  1126  	opts := rest.Opts{
  1127  		Method:     "POST",
  1128  		Path:       "/deletefile",
  1129  		Parameters: url.Values{},
  1130  	}
  1131  	var result api.ItemResult
  1132  	opts.Parameters.Set("fileid", fileIDtoNumber(o.id))
  1133  	return o.fs.pacer.Call(func() (bool, error) {
  1134  		resp, err := o.fs.srv.CallJSON(ctx, &opts, nil, &result)
  1135  		err = result.Error.Update(err)
  1136  		return shouldRetry(resp, err)
  1137  	})
  1138  }
  1139  
  1140  // ID returns the ID of the Object if known, or "" if not
  1141  func (o *Object) ID() string {
  1142  	return o.id
  1143  }
  1144  
  1145  // Check the interfaces are satisfied
  1146  var (
  1147  	_ fs.Fs              = (*Fs)(nil)
  1148  	_ fs.Purger          = (*Fs)(nil)
  1149  	_ fs.CleanUpper      = (*Fs)(nil)
  1150  	_ fs.Copier          = (*Fs)(nil)
  1151  	_ fs.Mover           = (*Fs)(nil)
  1152  	_ fs.DirMover        = (*Fs)(nil)
  1153  	_ fs.DirCacheFlusher = (*Fs)(nil)
  1154  	_ fs.Abouter         = (*Fs)(nil)
  1155  	_ fs.Object          = (*Object)(nil)
  1156  	_ fs.IDer            = (*Object)(nil)
  1157  )