github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/yandex/yandex.go (about)

     1  // Package yandex provides an interface to the Yandex storage system.
     2  package yandex
     3  
     4  import (
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"log"
    11  	"net/http"
    12  	"net/url"
    13  	"path"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/rclone/rclone/backend/yandex/api"
    19  	"github.com/rclone/rclone/fs"
    20  	"github.com/rclone/rclone/fs/config"
    21  	"github.com/rclone/rclone/fs/config/configmap"
    22  	"github.com/rclone/rclone/fs/config/configstruct"
    23  	"github.com/rclone/rclone/fs/config/obscure"
    24  	"github.com/rclone/rclone/fs/fserrors"
    25  	"github.com/rclone/rclone/fs/hash"
    26  	"github.com/rclone/rclone/lib/encoder"
    27  	"github.com/rclone/rclone/lib/oauthutil"
    28  	"github.com/rclone/rclone/lib/pacer"
    29  	"github.com/rclone/rclone/lib/readers"
    30  	"github.com/rclone/rclone/lib/rest"
    31  	"golang.org/x/oauth2"
    32  )
    33  
    34  // oAuth
    35  const (
    36  	rcloneClientID              = "ac39b43b9eba4cae8ffb788c06d816a8"
    37  	rcloneEncryptedClientSecret = "EfyyNZ3YUEwXM5yAhi72G9YwKn2mkFrYwJNS7cY0TJAhFlX9K-uJFbGlpO-RYjrJ"
    38  	rootURL                     = "https://cloud-api.yandex.com/v1/disk"
    39  	minSleep                    = 10 * time.Millisecond
    40  	maxSleep                    = 2 * time.Second // may needs to be increased, testing needed
    41  	decayConstant               = 2               // bigger for slower decay, exponential
    42  )
    43  
    44  // Globals
    45  var (
    46  	// Description of how to auth for this app
    47  	oauthConfig = &oauth2.Config{
    48  		Endpoint: oauth2.Endpoint{
    49  			AuthURL:  "https://oauth.yandex.com/authorize", //same as https://oauth.yandex.ru/authorize
    50  			TokenURL: "https://oauth.yandex.com/token",     //same as https://oauth.yandex.ru/token
    51  		},
    52  		ClientID:     rcloneClientID,
    53  		ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret),
    54  		RedirectURL:  oauthutil.RedirectURL,
    55  	}
    56  )
    57  
    58  // Register with Fs
    59  func init() {
    60  	fs.Register(&fs.RegInfo{
    61  		Name:        "yandex",
    62  		Description: "Yandex Disk",
    63  		NewFs:       NewFs,
    64  		Config: func(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) {
    65  			return oauthutil.ConfigOut("", &oauthutil.Options{
    66  				OAuth2Config: oauthConfig,
    67  			})
    68  		},
    69  		Options: append(oauthutil.SharedOptions, []fs.Option{{
    70  			Name:     "hard_delete",
    71  			Help:     "Delete files permanently rather than putting them into the trash.",
    72  			Default:  false,
    73  			Advanced: true,
    74  		}, {
    75  			Name:     config.ConfigEncoding,
    76  			Help:     config.ConfigEncodingHelp,
    77  			Advanced: true,
    78  			// Of the control characters \t \n \r are allowed
    79  			// it doesn't seem worth making an exception for this
    80  			Default: (encoder.Display |
    81  				encoder.EncodeInvalidUtf8),
    82  		}}...),
    83  	})
    84  }
    85  
    86  // Options defines the configuration for this backend
    87  type Options struct {
    88  	Token      string               `config:"token"`
    89  	HardDelete bool                 `config:"hard_delete"`
    90  	Enc        encoder.MultiEncoder `config:"encoding"`
    91  }
    92  
    93  // Fs represents a remote yandex
    94  type Fs struct {
    95  	name     string
    96  	root     string         // root path
    97  	opt      Options        // parsed options
    98  	ci       *fs.ConfigInfo // global config
    99  	features *fs.Features   // optional features
   100  	srv      *rest.Client   // the connection to the yandex server
   101  	pacer    *fs.Pacer      // pacer for API calls
   102  	diskRoot string         // root path with "disk:/" container name
   103  }
   104  
   105  // Object describes a swift object
   106  type Object struct {
   107  	fs          *Fs       // what this object is part of
   108  	remote      string    // The remote path
   109  	hasMetaData bool      // whether info below has been set
   110  	md5sum      string    // The MD5Sum of the object
   111  	size        int64     // Bytes in the object
   112  	modTime     time.Time // Modified time of the object
   113  	mimeType    string    // Content type according to the server
   114  
   115  }
   116  
   117  // ------------------------------------------------------------
   118  
   119  // Name of the remote (as passed into NewFs)
   120  func (f *Fs) Name() string {
   121  	return f.name
   122  }
   123  
   124  // Root of the remote (as passed into NewFs)
   125  func (f *Fs) Root() string {
   126  	return f.root
   127  }
   128  
   129  // String converts this Fs to a string
   130  func (f *Fs) String() string {
   131  	return fmt.Sprintf("Yandex %s", f.root)
   132  }
   133  
   134  // Precision return the precision of this Fs
   135  func (f *Fs) Precision() time.Duration {
   136  	return time.Nanosecond
   137  }
   138  
   139  // Hashes returns the supported hash sets.
   140  func (f *Fs) Hashes() hash.Set {
   141  	return hash.Set(hash.MD5)
   142  }
   143  
   144  // Features returns the optional features of this Fs
   145  func (f *Fs) Features() *fs.Features {
   146  	return f.features
   147  }
   148  
   149  // retryErrorCodes is a slice of error codes that we will retry
   150  var retryErrorCodes = []int{
   151  	429, // Too Many Requests.
   152  	500, // Internal Server Error
   153  	502, // Bad Gateway
   154  	503, // Service Unavailable
   155  	504, // Gateway Timeout
   156  	509, // Bandwidth Limit Exceeded
   157  }
   158  
   159  // shouldRetry returns a boolean as to whether this resp and err
   160  // deserve to be retried.  It returns the err as a convenience
   161  func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
   162  	if fserrors.ContextError(ctx, &err) {
   163  		return false, err
   164  	}
   165  	return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
   166  }
   167  
   168  // errorHandler parses a non 2xx error response into an error
   169  func errorHandler(resp *http.Response) error {
   170  	// Decode error response
   171  	errResponse := new(api.ErrorResponse)
   172  	err := rest.DecodeJSON(resp, &errResponse)
   173  	if err != nil {
   174  		fs.Debugf(nil, "Couldn't decode error response: %v", err)
   175  	}
   176  	if errResponse.Message == "" {
   177  		errResponse.Message = resp.Status
   178  	}
   179  	if errResponse.StatusCode == 0 {
   180  		errResponse.StatusCode = resp.StatusCode
   181  	}
   182  	return errResponse
   183  }
   184  
   185  // Sets root in f
   186  func (f *Fs) setRoot(root string) {
   187  	//Set root path
   188  	f.root = strings.Trim(root, "/")
   189  	//Set disk root path.
   190  	//Adding "disk:" to root path as all paths on disk start with it
   191  	var diskRoot string
   192  	if f.root == "" {
   193  		diskRoot = "disk:/"
   194  	} else {
   195  		diskRoot = "disk:/" + f.root + "/"
   196  	}
   197  	f.diskRoot = diskRoot
   198  }
   199  
   200  // filePath returns an escaped file path (f.root, file)
   201  func (f *Fs) filePath(file string) string {
   202  	return path.Join(f.diskRoot, file)
   203  }
   204  
   205  // dirPath returns an escaped file path (f.root, file) ending with '/'
   206  func (f *Fs) dirPath(file string) string {
   207  	return path.Join(f.diskRoot, file) + "/"
   208  }
   209  
   210  func (f *Fs) readMetaDataForPath(ctx context.Context, path string, options *api.ResourceInfoRequestOptions) (*api.ResourceInfoResponse, error) {
   211  	opts := rest.Opts{
   212  		Method:     "GET",
   213  		Path:       "/resources",
   214  		Parameters: url.Values{},
   215  	}
   216  
   217  	opts.Parameters.Set("path", f.opt.Enc.FromStandardPath(path))
   218  
   219  	if options.SortMode != nil {
   220  		opts.Parameters.Set("sort", options.SortMode.String())
   221  	}
   222  	if options.Limit != 0 {
   223  		opts.Parameters.Set("limit", strconv.FormatUint(options.Limit, 10))
   224  	}
   225  	if options.Offset != 0 {
   226  		opts.Parameters.Set("offset", strconv.FormatUint(options.Offset, 10))
   227  	}
   228  	if options.Fields != nil {
   229  		opts.Parameters.Set("fields", strings.Join(options.Fields, ","))
   230  	}
   231  
   232  	var err error
   233  	var info api.ResourceInfoResponse
   234  	var resp *http.Response
   235  	err = f.pacer.Call(func() (bool, error) {
   236  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &info)
   237  		return shouldRetry(ctx, resp, err)
   238  	})
   239  
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  
   244  	info.Name = f.opt.Enc.ToStandardName(info.Name)
   245  	return &info, nil
   246  }
   247  
   248  // NewFs constructs an Fs from the path, container:path
   249  func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
   250  	// Parse config into Options struct
   251  	opt := new(Options)
   252  	err := configstruct.Set(m, opt)
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  
   257  	token, err := oauthutil.GetToken(name, m)
   258  	if err != nil {
   259  		return nil, fmt.Errorf("couldn't read OAuth token: %w", err)
   260  	}
   261  	if token.RefreshToken == "" {
   262  		return nil, errors.New("unable to get RefreshToken. If you are upgrading from older versions of rclone, please run `rclone config` and re-configure this backend")
   263  	}
   264  	if token.TokenType != "OAuth" {
   265  		token.TokenType = "OAuth"
   266  		err = oauthutil.PutToken(name, m, token, false)
   267  		if err != nil {
   268  			return nil, fmt.Errorf("couldn't save OAuth token: %w", err)
   269  		}
   270  		log.Printf("Automatically upgraded OAuth config.")
   271  	}
   272  	oAuthClient, _, err := oauthutil.NewClient(ctx, name, m, oauthConfig)
   273  	if err != nil {
   274  		return nil, fmt.Errorf("failed to configure Yandex: %w", err)
   275  	}
   276  
   277  	ci := fs.GetConfig(ctx)
   278  	f := &Fs{
   279  		name:  name,
   280  		opt:   *opt,
   281  		ci:    ci,
   282  		srv:   rest.NewClient(oAuthClient).SetRoot(rootURL),
   283  		pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
   284  	}
   285  	f.setRoot(root)
   286  	f.features = (&fs.Features{
   287  		ReadMimeType:            true,
   288  		WriteMimeType:           false, // Yandex ignores the mime type we send
   289  		CanHaveEmptyDirectories: true,
   290  	}).Fill(ctx, f)
   291  	f.srv.SetErrorHandler(errorHandler)
   292  
   293  	// Check to see if the object exists and is a file
   294  	//request object meta info
   295  	// Check to see if the object exists and is a file
   296  	//request object meta info
   297  	if info, err := f.readMetaDataForPath(ctx, f.diskRoot, &api.ResourceInfoRequestOptions{}); err != nil {
   298  
   299  	} else {
   300  		if info.ResourceType == "file" {
   301  			rootDir := path.Dir(root)
   302  			if rootDir == "." {
   303  				rootDir = ""
   304  			}
   305  			f.setRoot(rootDir)
   306  			// return an error with an fs which points to the parent
   307  			return f, fs.ErrorIsFile
   308  		}
   309  	}
   310  	return f, nil
   311  }
   312  
   313  // Convert a list item into a DirEntry
   314  func (f *Fs) itemToDirEntry(ctx context.Context, remote string, object *api.ResourceInfoResponse) (fs.DirEntry, error) {
   315  	switch object.ResourceType {
   316  	case "dir":
   317  		t, err := time.Parse(time.RFC3339Nano, object.Modified)
   318  		if err != nil {
   319  			return nil, fmt.Errorf("error parsing time in directory item: %w", err)
   320  		}
   321  		d := fs.NewDir(remote, t).SetSize(object.Size)
   322  		return d, nil
   323  	case "file":
   324  		o, err := f.newObjectWithInfo(ctx, remote, object)
   325  		if err != nil {
   326  			return nil, err
   327  		}
   328  		return o, nil
   329  	default:
   330  		fs.Debugf(f, "Unknown resource type %q", object.ResourceType)
   331  	}
   332  	return nil, nil
   333  }
   334  
   335  // List the objects and directories in dir into entries.  The
   336  // entries can be returned in any order but should be for a
   337  // complete directory.
   338  //
   339  // dir should be "" to list the root, and should not have
   340  // trailing slashes.
   341  //
   342  // This should return ErrDirNotFound if the directory isn't
   343  // found.
   344  func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
   345  	root := f.dirPath(dir)
   346  
   347  	var limit uint64 = 1000 // max number of objects per request
   348  	var itemsCount uint64   // number of items per page in response
   349  	var offset uint64       // for the next page of requests
   350  
   351  	for {
   352  		opts := &api.ResourceInfoRequestOptions{
   353  			Limit:  limit,
   354  			Offset: offset,
   355  		}
   356  		info, err := f.readMetaDataForPath(ctx, root, opts)
   357  
   358  		if err != nil {
   359  			if apiErr, ok := err.(*api.ErrorResponse); ok {
   360  				// does not exist
   361  				if apiErr.ErrorName == "DiskNotFoundError" {
   362  					return nil, fs.ErrorDirNotFound
   363  				}
   364  			}
   365  			return nil, err
   366  		}
   367  		itemsCount = uint64(len(info.Embedded.Items))
   368  
   369  		if info.ResourceType == "dir" {
   370  			//list all subdirs
   371  			for _, element := range info.Embedded.Items {
   372  				element.Name = f.opt.Enc.ToStandardName(element.Name)
   373  				remote := path.Join(dir, element.Name)
   374  				entry, err := f.itemToDirEntry(ctx, remote, &element)
   375  				if err != nil {
   376  					return nil, err
   377  				}
   378  				if entry != nil {
   379  					entries = append(entries, entry)
   380  				}
   381  			}
   382  		} else if info.ResourceType == "file" {
   383  			return nil, fs.ErrorIsFile
   384  		}
   385  
   386  		//offset for the next page of items
   387  		offset += itemsCount
   388  		//check if we reached end of list
   389  		if itemsCount < limit {
   390  			break
   391  		}
   392  	}
   393  
   394  	return entries, nil
   395  }
   396  
   397  // Return an Object from a path
   398  //
   399  // If it can't be found it returns the error fs.ErrorObjectNotFound.
   400  func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.ResourceInfoResponse) (fs.Object, error) {
   401  	o := &Object{
   402  		fs:     f,
   403  		remote: remote,
   404  	}
   405  	var err error
   406  	if info != nil {
   407  		err = o.setMetaData(info)
   408  	} else {
   409  		err = o.readMetaData(ctx)
   410  		if apiErr, ok := err.(*api.ErrorResponse); ok {
   411  			// does not exist
   412  			if apiErr.ErrorName == "DiskNotFoundError" {
   413  				return nil, fs.ErrorObjectNotFound
   414  			}
   415  		}
   416  	}
   417  	if err != nil {
   418  		return nil, err
   419  	}
   420  	return o, nil
   421  }
   422  
   423  // NewObject finds the Object at remote.  If it can't be found it
   424  // returns the error fs.ErrorObjectNotFound.
   425  func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
   426  	return f.newObjectWithInfo(ctx, remote, nil)
   427  }
   428  
   429  // Creates from the parameters passed in a half finished Object which
   430  // must have setMetaData called on it
   431  //
   432  // Used to create new objects
   433  func (f *Fs) createObject(remote string, modTime time.Time, size int64) (o *Object) {
   434  	// Temporary Object under construction
   435  	o = &Object{
   436  		fs:      f,
   437  		remote:  remote,
   438  		size:    size,
   439  		modTime: modTime,
   440  	}
   441  	return o
   442  }
   443  
   444  // Put the object
   445  //
   446  // Copy the reader in to the new object which is returned.
   447  //
   448  // The new object may have been created if an error is returned
   449  func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   450  	o := f.createObject(src.Remote(), src.ModTime(ctx), src.Size())
   451  	return o, o.Update(ctx, in, src, options...)
   452  }
   453  
   454  // PutStream uploads to the remote path with the modTime given of indeterminate size
   455  func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   456  	return f.Put(ctx, in, src, options...)
   457  }
   458  
   459  // CreateDir makes a directory
   460  func (f *Fs) CreateDir(ctx context.Context, path string) (err error) {
   461  	//fmt.Printf("CreateDir: %s\n", path)
   462  
   463  	var resp *http.Response
   464  	opts := rest.Opts{
   465  		Method:     "PUT",
   466  		Path:       "/resources",
   467  		Parameters: url.Values{},
   468  		NoResponse: true,
   469  	}
   470  
   471  	// If creating a directory with a : use (undocumented) disk: prefix
   472  	if strings.ContainsRune(path, ':') {
   473  		path = "disk:" + path
   474  	}
   475  	opts.Parameters.Set("path", f.opt.Enc.FromStandardPath(path))
   476  
   477  	err = f.pacer.Call(func() (bool, error) {
   478  		resp, err = f.srv.Call(ctx, &opts)
   479  		return shouldRetry(ctx, resp, err)
   480  	})
   481  	if err != nil {
   482  		// fmt.Printf("CreateDir %q Error: %s\n", path, err.Error())
   483  		return err
   484  	}
   485  	// fmt.Printf("...Id %q\n", *info.Id)
   486  	return nil
   487  }
   488  
   489  // This really needs improvement and especially proper error checking
   490  // but Yandex does not publish a List of possible errors and when they're
   491  // expected to occur.
   492  func (f *Fs) mkDirs(ctx context.Context, path string) (err error) {
   493  	//trim filename from path
   494  	//dirString := strings.TrimSuffix(path, filepath.Base(path))
   495  	//trim "disk:" from path
   496  	dirString := strings.TrimPrefix(path, "disk:")
   497  	if dirString == "" {
   498  		return nil
   499  	}
   500  
   501  	if err = f.CreateDir(ctx, dirString); err != nil {
   502  		if apiErr, ok := err.(*api.ErrorResponse); ok {
   503  			// already exists
   504  			if apiErr.ErrorName != "DiskPathPointsToExistentDirectoryError" {
   505  				// 2 if it fails then create all directories in the path from root.
   506  				dirs := strings.Split(dirString, "/") //path separator
   507  				var mkdirpath = "/"                   //path separator /
   508  				for _, element := range dirs {
   509  					if element != "" {
   510  						mkdirpath += element + "/"      //path separator /
   511  						_ = f.CreateDir(ctx, mkdirpath) // ignore errors while creating dirs
   512  					}
   513  				}
   514  			}
   515  			return nil
   516  		}
   517  	}
   518  	return err
   519  }
   520  
   521  func (f *Fs) mkParentDirs(ctx context.Context, resPath string) error {
   522  	// defer log.Trace(dirPath, "")("")
   523  	// chop off trailing / if it exists
   524  	parent := path.Dir(strings.TrimSuffix(resPath, "/"))
   525  	if parent == "." {
   526  		parent = ""
   527  	}
   528  	return f.mkDirs(ctx, parent)
   529  }
   530  
   531  // Mkdir creates the container if it doesn't exist
   532  func (f *Fs) Mkdir(ctx context.Context, dir string) error {
   533  	path := f.filePath(dir)
   534  	return f.mkDirs(ctx, path)
   535  }
   536  
   537  // waitForJob waits for the job with status in url to complete
   538  func (f *Fs) waitForJob(ctx context.Context, location string) (err error) {
   539  	opts := rest.Opts{
   540  		RootURL: location,
   541  		Method:  "GET",
   542  	}
   543  	deadline := time.Now().Add(f.ci.TimeoutOrInfinite())
   544  	for time.Now().Before(deadline) {
   545  		var resp *http.Response
   546  		var body []byte
   547  		err = f.pacer.Call(func() (bool, error) {
   548  			resp, err = f.srv.Call(ctx, &opts)
   549  			if fserrors.ContextError(ctx, &err) {
   550  				return false, err
   551  			}
   552  			if err != nil {
   553  				return fserrors.ShouldRetry(err), err
   554  			}
   555  			body, err = rest.ReadBody(resp)
   556  			return fserrors.ShouldRetry(err), err
   557  		})
   558  		if err != nil {
   559  			return err
   560  		}
   561  		// Try to decode the body first as an api.AsyncOperationStatus
   562  		var status api.AsyncStatus
   563  		err = json.Unmarshal(body, &status)
   564  		if err != nil {
   565  			return fmt.Errorf("async status result not JSON: %q: %w", body, err)
   566  		}
   567  
   568  		switch status.Status {
   569  		case "failure":
   570  			return fmt.Errorf("async operation returned %q", status.Status)
   571  		case "success":
   572  			return nil
   573  		}
   574  
   575  		time.Sleep(1 * time.Second)
   576  	}
   577  	return fmt.Errorf("async operation didn't complete after %v", f.ci.TimeoutOrInfinite())
   578  }
   579  
   580  func (f *Fs) delete(ctx context.Context, path string, hardDelete bool) (err error) {
   581  	opts := rest.Opts{
   582  		Method:     "DELETE",
   583  		Path:       "/resources",
   584  		Parameters: url.Values{},
   585  	}
   586  
   587  	opts.Parameters.Set("path", f.opt.Enc.FromStandardPath(path))
   588  	opts.Parameters.Set("permanently", strconv.FormatBool(hardDelete))
   589  
   590  	var resp *http.Response
   591  	var body []byte
   592  	err = f.pacer.Call(func() (bool, error) {
   593  		resp, err = f.srv.Call(ctx, &opts)
   594  		if fserrors.ContextError(ctx, &err) {
   595  			return false, err
   596  		}
   597  		if err != nil {
   598  			return fserrors.ShouldRetry(err), err
   599  		}
   600  		body, err = rest.ReadBody(resp)
   601  		return fserrors.ShouldRetry(err), err
   602  	})
   603  	if err != nil {
   604  		return err
   605  	}
   606  
   607  	// if 202 Accepted it's an async operation we have to wait for it complete before retuning
   608  	if resp.StatusCode == 202 {
   609  		var info api.AsyncInfo
   610  		err = json.Unmarshal(body, &info)
   611  		if err != nil {
   612  			return fmt.Errorf("async info result not JSON: %q: %w", body, err)
   613  		}
   614  		return f.waitForJob(ctx, info.HRef)
   615  	}
   616  	return nil
   617  }
   618  
   619  // purgeCheck remotes the root directory, if check is set then it
   620  // refuses to do so if it has anything in
   621  func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
   622  	root := f.filePath(dir)
   623  	if check {
   624  		//to comply with rclone logic we check if the directory is empty before delete.
   625  		//send request to get list of objects in this directory.
   626  		info, err := f.readMetaDataForPath(ctx, root, &api.ResourceInfoRequestOptions{})
   627  		if err != nil {
   628  			return fmt.Errorf("rmdir failed: %w", err)
   629  		}
   630  		if len(info.Embedded.Items) != 0 {
   631  			return fs.ErrorDirectoryNotEmpty
   632  		}
   633  	}
   634  	//delete directory
   635  	return f.delete(ctx, root, f.opt.HardDelete)
   636  }
   637  
   638  // Rmdir deletes the container
   639  //
   640  // Returns an error if it isn't empty
   641  func (f *Fs) Rmdir(ctx context.Context, dir string) error {
   642  	return f.purgeCheck(ctx, dir, true)
   643  }
   644  
   645  // Purge deletes all the files in the directory
   646  //
   647  // Optional interface: Only implement this if you have a way of
   648  // deleting all the files quicker than just running Remove() on the
   649  // result of List()
   650  func (f *Fs) Purge(ctx context.Context, dir string) error {
   651  	return f.purgeCheck(ctx, dir, false)
   652  }
   653  
   654  // copyOrMoves copies or moves directories or files depending on the method parameter
   655  func (f *Fs) copyOrMove(ctx context.Context, method, src, dst string, overwrite bool) (err error) {
   656  	opts := rest.Opts{
   657  		Method:     "POST",
   658  		Path:       "/resources/" + method,
   659  		Parameters: url.Values{},
   660  	}
   661  
   662  	opts.Parameters.Set("from", f.opt.Enc.FromStandardPath(src))
   663  	opts.Parameters.Set("path", f.opt.Enc.FromStandardPath(dst))
   664  	opts.Parameters.Set("overwrite", strconv.FormatBool(overwrite))
   665  
   666  	var resp *http.Response
   667  	var body []byte
   668  	err = f.pacer.Call(func() (bool, error) {
   669  		resp, err = f.srv.Call(ctx, &opts)
   670  		if fserrors.ContextError(ctx, &err) {
   671  			return false, err
   672  		}
   673  		if err != nil {
   674  			return fserrors.ShouldRetry(err), err
   675  		}
   676  		body, err = rest.ReadBody(resp)
   677  		return fserrors.ShouldRetry(err), err
   678  	})
   679  	if err != nil {
   680  		return err
   681  	}
   682  
   683  	// if 202 Accepted it's an async operation we have to wait for it complete before retuning
   684  	if resp.StatusCode == 202 {
   685  		var info api.AsyncInfo
   686  		err = json.Unmarshal(body, &info)
   687  		if err != nil {
   688  			return fmt.Errorf("async info result not JSON: %q: %w", body, err)
   689  		}
   690  		return f.waitForJob(ctx, info.HRef)
   691  	}
   692  	return nil
   693  }
   694  
   695  // Copy src to this remote using server-side copy operations.
   696  //
   697  // This is stored with the remote path given.
   698  //
   699  // It returns the destination Object and a possible error.
   700  //
   701  // Will only be called if src.Fs().Name() == f.Name()
   702  //
   703  // If it isn't possible then return fs.ErrorCantCopy
   704  func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
   705  	srcObj, ok := src.(*Object)
   706  	if !ok {
   707  		fs.Debugf(src, "Can't copy - not same remote type")
   708  		return nil, fs.ErrorCantCopy
   709  	}
   710  
   711  	dstPath := f.filePath(remote)
   712  	err := f.mkParentDirs(ctx, dstPath)
   713  	if err != nil {
   714  		return nil, err
   715  	}
   716  	err = f.copyOrMove(ctx, "copy", srcObj.filePath(), dstPath, false)
   717  
   718  	if err != nil {
   719  		return nil, fmt.Errorf("couldn't copy file: %w", err)
   720  	}
   721  
   722  	return f.NewObject(ctx, remote)
   723  }
   724  
   725  // Move src to this remote using server-side move operations.
   726  //
   727  // This is stored with the remote path given.
   728  //
   729  // It returns the destination Object and a possible error.
   730  //
   731  // Will only be called if src.Fs().Name() == f.Name()
   732  //
   733  // If it isn't possible then return fs.ErrorCantMove
   734  func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
   735  	srcObj, ok := src.(*Object)
   736  	if !ok {
   737  		fs.Debugf(src, "Can't move - not same remote type")
   738  		return nil, fs.ErrorCantMove
   739  	}
   740  
   741  	dstPath := f.filePath(remote)
   742  	err := f.mkParentDirs(ctx, dstPath)
   743  	if err != nil {
   744  		return nil, err
   745  	}
   746  	err = f.copyOrMove(ctx, "move", srcObj.filePath(), dstPath, false)
   747  
   748  	if err != nil {
   749  		return nil, fmt.Errorf("couldn't move file: %w", err)
   750  	}
   751  
   752  	return f.NewObject(ctx, remote)
   753  }
   754  
   755  // DirMove moves src, srcRemote to this remote at dstRemote
   756  // using server-side move operations.
   757  //
   758  // Will only be called if src.Fs().Name() == f.Name()
   759  //
   760  // If it isn't possible then return fs.ErrorCantDirMove
   761  //
   762  // If destination exists then return fs.ErrorDirExists
   763  func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
   764  	srcFs, ok := src.(*Fs)
   765  	if !ok {
   766  		fs.Debugf(srcFs, "Can't move directory - not same remote type")
   767  		return fs.ErrorCantDirMove
   768  	}
   769  	srcPath := path.Join(srcFs.diskRoot, srcRemote)
   770  	dstPath := f.dirPath(dstRemote)
   771  
   772  	//fmt.Printf("Move src: %s (FullPath: %s), dst: %s (FullPath: %s)\n", srcRemote, srcPath, dstRemote, dstPath)
   773  
   774  	// Refuse to move to or from the root
   775  	if srcPath == "disk:/" || dstPath == "disk:/" {
   776  		fs.Debugf(src, "DirMove error: Can't move root")
   777  		return errors.New("can't move root directory")
   778  	}
   779  
   780  	err := f.mkParentDirs(ctx, dstPath)
   781  	if err != nil {
   782  		return err
   783  	}
   784  
   785  	_, err = f.readMetaDataForPath(ctx, dstPath, &api.ResourceInfoRequestOptions{})
   786  	if apiErr, ok := err.(*api.ErrorResponse); ok {
   787  		if apiErr.ErrorName != "DiskNotFoundError" {
   788  			return err
   789  		}
   790  	} else if err != nil {
   791  		return err
   792  	} else {
   793  		return fs.ErrorDirExists
   794  	}
   795  
   796  	err = f.copyOrMove(ctx, "move", srcPath, dstPath, false)
   797  
   798  	if err != nil {
   799  		return fmt.Errorf("couldn't move directory: %w", err)
   800  	}
   801  	return nil
   802  }
   803  
   804  // PublicLink generates a public link to the remote path (usually readable by anyone)
   805  func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
   806  	var path string
   807  	if unlink {
   808  		path = "/resources/unpublish"
   809  	} else {
   810  		path = "/resources/publish"
   811  	}
   812  	opts := rest.Opts{
   813  		Method:     "PUT",
   814  		Path:       f.opt.Enc.FromStandardPath(path),
   815  		Parameters: url.Values{},
   816  		NoResponse: true,
   817  	}
   818  
   819  	opts.Parameters.Set("path", f.opt.Enc.FromStandardPath(f.filePath(remote)))
   820  
   821  	var resp *http.Response
   822  	err = f.pacer.Call(func() (bool, error) {
   823  		resp, err = f.srv.Call(ctx, &opts)
   824  		return shouldRetry(ctx, resp, err)
   825  	})
   826  
   827  	if apiErr, ok := err.(*api.ErrorResponse); ok {
   828  		// does not exist
   829  		if apiErr.ErrorName == "DiskNotFoundError" {
   830  			return "", fs.ErrorObjectNotFound
   831  		}
   832  	}
   833  	if err != nil {
   834  		if unlink {
   835  			return "", fmt.Errorf("couldn't remove public link: %w", err)
   836  		}
   837  		return "", fmt.Errorf("couldn't create public link: %w", err)
   838  	}
   839  
   840  	info, err := f.readMetaDataForPath(ctx, f.filePath(remote), &api.ResourceInfoRequestOptions{})
   841  	if err != nil {
   842  		return "", err
   843  	}
   844  
   845  	if info.PublicURL == "" {
   846  		return "", errors.New("couldn't create public link - no link path received")
   847  	}
   848  	return info.PublicURL, nil
   849  }
   850  
   851  // CleanUp permanently deletes all trashed files/folders
   852  func (f *Fs) CleanUp(ctx context.Context) (err error) {
   853  	var resp *http.Response
   854  	opts := rest.Opts{
   855  		Method:     "DELETE",
   856  		Path:       "/trash/resources",
   857  		NoResponse: true,
   858  	}
   859  
   860  	err = f.pacer.Call(func() (bool, error) {
   861  		resp, err = f.srv.Call(ctx, &opts)
   862  		return shouldRetry(ctx, resp, err)
   863  	})
   864  	return err
   865  }
   866  
   867  // About gets quota information
   868  func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
   869  	opts := rest.Opts{
   870  		Method: "GET",
   871  		Path:   "/",
   872  	}
   873  
   874  	var resp *http.Response
   875  	var info api.DiskInfo
   876  	var err error
   877  	err = f.pacer.Call(func() (bool, error) {
   878  		resp, err = f.srv.CallJSON(ctx, &opts, nil, &info)
   879  		return shouldRetry(ctx, resp, err)
   880  	})
   881  
   882  	if err != nil {
   883  		return nil, err
   884  	}
   885  
   886  	usage := &fs.Usage{
   887  		Total: fs.NewUsageValue(info.TotalSpace),
   888  		Used:  fs.NewUsageValue(info.UsedSpace),
   889  		Free:  fs.NewUsageValue(info.TotalSpace - info.UsedSpace),
   890  	}
   891  	return usage, nil
   892  }
   893  
   894  // ------------------------------------------------------------
   895  
   896  // Fs returns the parent Fs
   897  func (o *Object) Fs() fs.Info {
   898  	return o.fs
   899  }
   900  
   901  // Return a string version
   902  func (o *Object) String() string {
   903  	if o == nil {
   904  		return "<nil>"
   905  	}
   906  	return o.remote
   907  }
   908  
   909  // Remote returns the remote path
   910  func (o *Object) Remote() string {
   911  	return o.remote
   912  }
   913  
   914  // Returns the full remote path for the object
   915  func (o *Object) filePath() string {
   916  	return o.fs.filePath(o.remote)
   917  }
   918  
   919  // setMetaData sets the fs data from a storage.Object
   920  func (o *Object) setMetaData(info *api.ResourceInfoResponse) (err error) {
   921  	o.hasMetaData = true
   922  	o.size = info.Size
   923  	o.md5sum = info.Md5
   924  	o.mimeType = info.MimeType
   925  
   926  	var modTimeString string
   927  	modTimeObj, ok := info.CustomProperties["rclone_modified"]
   928  	if ok {
   929  		// read modTime from rclone_modified custom_property of object
   930  		modTimeString, ok = modTimeObj.(string)
   931  	}
   932  	if !ok {
   933  		// read modTime from Modified property of object as a fallback
   934  		modTimeString = info.Modified
   935  	}
   936  	t, err := time.Parse(time.RFC3339Nano, modTimeString)
   937  	if err != nil {
   938  		return fmt.Errorf("failed to parse modtime from %q: %w", modTimeString, err)
   939  	}
   940  	o.modTime = t
   941  	return nil
   942  }
   943  
   944  // readMetaData reads ands sets the new metadata for a storage.Object
   945  func (o *Object) readMetaData(ctx context.Context) (err error) {
   946  	if o.hasMetaData {
   947  		return nil
   948  	}
   949  	info, err := o.fs.readMetaDataForPath(ctx, o.filePath(), &api.ResourceInfoRequestOptions{})
   950  	if err != nil {
   951  		return err
   952  	}
   953  	if info.ResourceType == "dir" {
   954  		return fs.ErrorIsDir
   955  	} else if info.ResourceType != "file" {
   956  		return fs.ErrorNotAFile
   957  	}
   958  	return o.setMetaData(info)
   959  }
   960  
   961  // ModTime returns the modification time of the object
   962  //
   963  // It attempts to read the objects mtime and if that isn't present the
   964  // LastModified returned in the http headers
   965  func (o *Object) ModTime(ctx context.Context) time.Time {
   966  	err := o.readMetaData(ctx)
   967  	if err != nil {
   968  		fs.Logf(o, "Failed to read metadata: %v", err)
   969  		return time.Now()
   970  	}
   971  	return o.modTime
   972  }
   973  
   974  // Size returns the size of an object in bytes
   975  func (o *Object) Size() int64 {
   976  	ctx := context.TODO()
   977  	err := o.readMetaData(ctx)
   978  	if err != nil {
   979  		fs.Logf(o, "Failed to read metadata: %v", err)
   980  		return 0
   981  	}
   982  	return o.size
   983  }
   984  
   985  // Hash returns the Md5sum of an object returning a lowercase hex string
   986  func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) {
   987  	if t != hash.MD5 {
   988  		return "", hash.ErrUnsupported
   989  	}
   990  	return o.md5sum, nil
   991  }
   992  
   993  // Storable returns whether this object is storable
   994  func (o *Object) Storable() bool {
   995  	return true
   996  }
   997  
   998  func (o *Object) setCustomProperty(ctx context.Context, property string, value string) (err error) {
   999  	var resp *http.Response
  1000  	opts := rest.Opts{
  1001  		Method:     "PATCH",
  1002  		Path:       "/resources",
  1003  		Parameters: url.Values{},
  1004  		NoResponse: true,
  1005  	}
  1006  
  1007  	opts.Parameters.Set("path", o.fs.opt.Enc.FromStandardPath(o.filePath()))
  1008  	rcm := map[string]interface{}{
  1009  		property: value,
  1010  	}
  1011  	cpr := api.CustomPropertyResponse{CustomProperties: rcm}
  1012  
  1013  	err = o.fs.pacer.Call(func() (bool, error) {
  1014  		resp, err = o.fs.srv.CallJSON(ctx, &opts, &cpr, nil)
  1015  		return shouldRetry(ctx, resp, err)
  1016  	})
  1017  	return err
  1018  }
  1019  
  1020  // SetModTime sets the modification time of the local fs object
  1021  //
  1022  // Commits the datastore
  1023  func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
  1024  	// set custom_property 'rclone_modified' of object to modTime
  1025  	err := o.setCustomProperty(ctx, "rclone_modified", modTime.Format(time.RFC3339Nano))
  1026  	if err != nil {
  1027  		return err
  1028  	}
  1029  	o.modTime = modTime
  1030  	return nil
  1031  }
  1032  
  1033  // Open an object for read
  1034  func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
  1035  	// prepare download
  1036  	var resp *http.Response
  1037  	var dl api.AsyncInfo
  1038  	opts := rest.Opts{
  1039  		Method:     "GET",
  1040  		Path:       "/resources/download",
  1041  		Parameters: url.Values{},
  1042  	}
  1043  
  1044  	opts.Parameters.Set("path", o.fs.opt.Enc.FromStandardPath(o.filePath()))
  1045  
  1046  	err = o.fs.pacer.Call(func() (bool, error) {
  1047  		resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &dl)
  1048  		return shouldRetry(ctx, resp, err)
  1049  	})
  1050  
  1051  	if err != nil {
  1052  		return nil, err
  1053  	}
  1054  
  1055  	// perform the download
  1056  	opts = rest.Opts{
  1057  		RootURL: dl.HRef,
  1058  		Method:  "GET",
  1059  		Options: options,
  1060  	}
  1061  	err = o.fs.pacer.Call(func() (bool, error) {
  1062  		resp, err = o.fs.srv.Call(ctx, &opts)
  1063  		return shouldRetry(ctx, resp, err)
  1064  	})
  1065  	if err != nil {
  1066  		return nil, err
  1067  	}
  1068  	return resp.Body, err
  1069  }
  1070  
  1071  func (o *Object) upload(ctx context.Context, in io.Reader, overwrite bool, mimeType string, options ...fs.OpenOption) (err error) {
  1072  	// prepare upload
  1073  	var resp *http.Response
  1074  	var ur api.AsyncInfo
  1075  	opts := rest.Opts{
  1076  		Method:     "GET",
  1077  		Path:       "/resources/upload",
  1078  		Parameters: url.Values{},
  1079  		Options:    options,
  1080  	}
  1081  
  1082  	opts.Parameters.Set("path", o.fs.opt.Enc.FromStandardPath(o.filePath()))
  1083  	opts.Parameters.Set("overwrite", strconv.FormatBool(overwrite))
  1084  
  1085  	err = o.fs.pacer.Call(func() (bool, error) {
  1086  		resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &ur)
  1087  		return shouldRetry(ctx, resp, err)
  1088  	})
  1089  
  1090  	if err != nil {
  1091  		return err
  1092  	}
  1093  
  1094  	// perform the actual upload
  1095  	opts = rest.Opts{
  1096  		RootURL:     ur.HRef,
  1097  		Method:      "PUT",
  1098  		ContentType: mimeType,
  1099  		Body:        in,
  1100  		NoResponse:  true,
  1101  	}
  1102  
  1103  	err = o.fs.pacer.CallNoRetry(func() (bool, error) {
  1104  		resp, err = o.fs.srv.Call(ctx, &opts)
  1105  		return shouldRetry(ctx, resp, err)
  1106  	})
  1107  
  1108  	return err
  1109  }
  1110  
  1111  // Update the already existing object
  1112  //
  1113  // Copy the reader into the object updating modTime and size.
  1114  //
  1115  // The new object may have been created if an error is returned
  1116  func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
  1117  	in1 := readers.NewCountingReader(in)
  1118  	modTime := src.ModTime(ctx)
  1119  	remote := o.filePath()
  1120  
  1121  	//create full path to file before upload.
  1122  	err := o.fs.mkParentDirs(ctx, remote)
  1123  	if err != nil {
  1124  		return err
  1125  	}
  1126  
  1127  	//upload file
  1128  	err = o.upload(ctx, in1, true, fs.MimeType(ctx, src), options...)
  1129  	if err != nil {
  1130  		return err
  1131  	}
  1132  
  1133  	//if file uploaded successfully then return metadata
  1134  	o.modTime = modTime
  1135  	o.md5sum = ""                   // according to unit tests after put the md5 is empty.
  1136  	o.size = int64(in1.BytesRead()) // better solution o.readMetaData() ?
  1137  	//and set modTime of uploaded file
  1138  	err = o.SetModTime(ctx, modTime)
  1139  
  1140  	return err
  1141  }
  1142  
  1143  // Remove an object
  1144  func (o *Object) Remove(ctx context.Context) error {
  1145  	return o.fs.delete(ctx, o.filePath(), o.fs.opt.HardDelete)
  1146  }
  1147  
  1148  // MimeType of an Object if known, "" otherwise
  1149  func (o *Object) MimeType(ctx context.Context) string {
  1150  	return o.mimeType
  1151  }
  1152  
  1153  // Check the interfaces are satisfied
  1154  var (
  1155  	_ fs.Fs           = (*Fs)(nil)
  1156  	_ fs.Purger       = (*Fs)(nil)
  1157  	_ fs.Copier       = (*Fs)(nil)
  1158  	_ fs.Mover        = (*Fs)(nil)
  1159  	_ fs.DirMover     = (*Fs)(nil)
  1160  	_ fs.PublicLinker = (*Fs)(nil)
  1161  	_ fs.CleanUpper   = (*Fs)(nil)
  1162  	_ fs.Abouter      = (*Fs)(nil)
  1163  	_ fs.Object       = (*Object)(nil)
  1164  	_ fs.MimeTyper    = (*Object)(nil)
  1165  )