github.com/artpar/rclone@v1.67.3/backend/pcloud/pcloud.go (about)

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