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

     1  // Package jottacloud provides an interface to the Jottacloud storage system.
     2  package jottacloud
     3  
     4  import (
     5  	"bytes"
     6  	"context"
     7  	"crypto/md5"
     8  	"encoding/base64"
     9  	"encoding/hex"
    10  	"encoding/json"
    11  	"encoding/xml"
    12  	"errors"
    13  	"fmt"
    14  	"io"
    15  	"math/rand"
    16  	"net/http"
    17  	"net/url"
    18  	"os"
    19  	"path"
    20  	"strconv"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/artpar/rclone/backend/jottacloud/api"
    25  	"github.com/artpar/rclone/fs"
    26  	"github.com/artpar/rclone/fs/accounting"
    27  	"github.com/artpar/rclone/fs/config"
    28  	"github.com/artpar/rclone/fs/config/configmap"
    29  	"github.com/artpar/rclone/fs/config/configstruct"
    30  	"github.com/artpar/rclone/fs/config/obscure"
    31  	"github.com/artpar/rclone/fs/fserrors"
    32  	"github.com/artpar/rclone/fs/fshttp"
    33  	"github.com/artpar/rclone/fs/hash"
    34  	"github.com/artpar/rclone/fs/walk"
    35  	"github.com/artpar/rclone/lib/encoder"
    36  	"github.com/artpar/rclone/lib/oauthutil"
    37  	"github.com/artpar/rclone/lib/pacer"
    38  	"github.com/artpar/rclone/lib/rest"
    39  	"golang.org/x/oauth2"
    40  )
    41  
    42  // Globals
    43  const (
    44  	minSleep           = 10 * time.Millisecond
    45  	maxSleep           = 2 * time.Second
    46  	decayConstant      = 2 // bigger for slower decay, exponential
    47  	defaultDevice      = "Jotta"
    48  	defaultMountpoint  = "Archive"
    49  	jfsURL             = "https://jfs.jottacloud.com/jfs/"
    50  	apiURL             = "https://api.jottacloud.com/"
    51  	wwwURL             = "https://www.jottacloud.com/"
    52  	cachePrefix        = "rclone-jcmd5-"
    53  	configDevice       = "device"
    54  	configMountpoint   = "mountpoint"
    55  	configTokenURL     = "tokenURL"
    56  	configClientID     = "client_id"
    57  	configClientSecret = "client_secret"
    58  	configUsername     = "username"
    59  	configVersion      = 1
    60  
    61  	defaultTokenURL = "https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/token"
    62  	defaultClientID = "jottacli"
    63  
    64  	legacyTokenURL              = "https://api.jottacloud.com/auth/v1/token"
    65  	legacyRegisterURL           = "https://api.jottacloud.com/auth/v1/register"
    66  	legacyClientID              = "nibfk8biu12ju7hpqomr8b1e40"
    67  	legacyEncryptedClientSecret = "Vp8eAv7eVElMnQwN-kgU9cbhgApNDaMqWdlDi5qFydlQoji4JBxrGMF2"
    68  	legacyConfigVersion         = 0
    69  
    70  	teliaseCloudTokenURL = "https://cloud-auth.telia.se/auth/realms/telia_se/protocol/openid-connect/token"
    71  	teliaseCloudAuthURL  = "https://cloud-auth.telia.se/auth/realms/telia_se/protocol/openid-connect/auth"
    72  	teliaseCloudClientID = "desktop"
    73  
    74  	telianoCloudTokenURL = "https://sky-auth.telia.no/auth/realms/get/protocol/openid-connect/token"
    75  	telianoCloudAuthURL  = "https://sky-auth.telia.no/auth/realms/get/protocol/openid-connect/auth"
    76  	telianoCloudClientID = "desktop"
    77  
    78  	tele2CloudTokenURL = "https://mittcloud-auth.tele2.se/auth/realms/comhem/protocol/openid-connect/token"
    79  	tele2CloudAuthURL  = "https://mittcloud-auth.tele2.se/auth/realms/comhem/protocol/openid-connect/auth"
    80  	tele2CloudClientID = "desktop"
    81  
    82  	onlimeCloudTokenURL = "https://cloud-auth.onlime.dk/auth/realms/onlime_wl/protocol/openid-connect/token"
    83  	onlimeCloudAuthURL  = "https://cloud-auth.onlime.dk/auth/realms/onlime_wl/protocol/openid-connect/auth"
    84  	onlimeCloudClientID = "desktop"
    85  )
    86  
    87  // Register with Fs
    88  func init() {
    89  	// needs to be done early so we can use oauth during config
    90  	fs.Register(&fs.RegInfo{
    91  		Name:        "jottacloud",
    92  		Description: "Jottacloud",
    93  		NewFs:       NewFs,
    94  		Config:      Config,
    95  		MetadataInfo: &fs.MetadataInfo{
    96  			Help: `Jottacloud has limited support for metadata, currently an extended set of timestamps.`,
    97  			System: map[string]fs.MetadataHelp{
    98  				"btime": {
    99  					Help:    "Time of file birth (creation), read from rclone metadata",
   100  					Type:    "RFC 3339",
   101  					Example: "2006-01-02T15:04:05.999999999Z07:00",
   102  				},
   103  				"mtime": {
   104  					Help:    "Time of last modification, read from rclone metadata",
   105  					Type:    "RFC 3339",
   106  					Example: "2006-01-02T15:04:05.999999999Z07:00",
   107  				},
   108  				"utime": {
   109  					Help:     "Time of last upload, when current revision was created, generated by backend",
   110  					Type:     "RFC 3339",
   111  					Example:  "2006-01-02T15:04:05.999999999Z07:00",
   112  					ReadOnly: true,
   113  				},
   114  				"content-type": {
   115  					Help:     "MIME type, also known as media type",
   116  					Type:     "string",
   117  					Example:  "text/plain",
   118  					ReadOnly: true,
   119  				},
   120  			},
   121  		},
   122  		Options: append(oauthutil.SharedOptions, []fs.Option{{
   123  			Name:     "md5_memory_limit",
   124  			Help:     "Files bigger than this will be cached on disk to calculate the MD5 if required.",
   125  			Default:  fs.SizeSuffix(10 * 1024 * 1024),
   126  			Advanced: true,
   127  		}, {
   128  			Name:     "trashed_only",
   129  			Help:     "Only show files that are in the trash.\n\nThis will show trashed files in their original directory structure.",
   130  			Default:  false,
   131  			Advanced: true,
   132  		}, {
   133  			Name:     "hard_delete",
   134  			Help:     "Delete files permanently rather than putting them into the trash.",
   135  			Default:  false,
   136  			Advanced: true,
   137  		}, {
   138  			Name:     "upload_resume_limit",
   139  			Help:     "Files bigger than this can be resumed if the upload fail's.",
   140  			Default:  fs.SizeSuffix(10 * 1024 * 1024),
   141  			Advanced: true,
   142  		}, {
   143  			Name:     "no_versions",
   144  			Help:     "Avoid server side versioning by deleting files and recreating files instead of overwriting them.",
   145  			Default:  false,
   146  			Advanced: true,
   147  		}, {
   148  			Name:     config.ConfigEncoding,
   149  			Help:     config.ConfigEncodingHelp,
   150  			Advanced: true,
   151  			// Encode invalid UTF-8 bytes as xml doesn't handle them properly.
   152  			//
   153  			// Also: '*', '/', ':', '<', '>', '?', '\"', '\x00', '|'
   154  			Default: (encoder.Display |
   155  				encoder.EncodeWin | // :?"*<>|
   156  				encoder.EncodeInvalidUtf8),
   157  		}}...),
   158  	})
   159  }
   160  
   161  // Config runs the backend configuration protocol
   162  func Config(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) {
   163  	switch config.State {
   164  	case "":
   165  		return fs.ConfigChooseExclusiveFixed("auth_type_done", "config_type", `Select authentication type.`, []fs.OptionExample{{
   166  			Value: "standard",
   167  			Help:  "Standard authentication.\nUse this if you're a normal Jottacloud user.",
   168  		}, {
   169  			Value: "legacy",
   170  			Help:  "Legacy authentication.\nThis is only required for certain whitelabel versions of Jottacloud and not recommended for normal users.",
   171  		}, {
   172  			Value: "telia_se",
   173  			Help:  "Telia Cloud authentication.\nUse this if you are using Telia Cloud (Sweden).",
   174  		}, {
   175  			Value: "telia_no",
   176  			Help:  "Telia Sky authentication.\nUse this if you are using Telia Sky (Norway).",
   177  		}, {
   178  			Value: "tele2",
   179  			Help:  "Tele2 Cloud authentication.\nUse this if you are using Tele2 Cloud.",
   180  		}, {
   181  			Value: "onlime",
   182  			Help:  "Onlime Cloud authentication.\nUse this if you are using Onlime Cloud.",
   183  		}})
   184  	case "auth_type_done":
   185  		// Jump to next state according to config chosen
   186  		return fs.ConfigGoto(config.Result)
   187  	case "standard": // configure a jottacloud backend using the modern JottaCli token based authentication
   188  		m.Set("configVersion", fmt.Sprint(configVersion))
   189  		return fs.ConfigInput("standard_token", "config_login_token", "Personal login token.\nGenerate here: https://www.jottacloud.com/web/secure")
   190  	case "standard_token":
   191  		loginToken := config.Result
   192  		m.Set(configClientID, defaultClientID)
   193  		m.Set(configClientSecret, "")
   194  
   195  		srv := rest.NewClient(fshttp.NewClient(ctx))
   196  		token, tokenEndpoint, err := doTokenAuth(ctx, srv, loginToken)
   197  		if err != nil {
   198  			return nil, fmt.Errorf("failed to get oauth token: %w", err)
   199  		}
   200  		m.Set(configTokenURL, tokenEndpoint)
   201  		err = oauthutil.PutToken(name, m, &token, true)
   202  		if err != nil {
   203  			return nil, fmt.Errorf("error while saving token: %w", err)
   204  		}
   205  		return fs.ConfigGoto("choose_device")
   206  	case "legacy": // configure a jottacloud backend using legacy authentication
   207  		m.Set("configVersion", fmt.Sprint(legacyConfigVersion))
   208  		return fs.ConfigConfirm("legacy_api", false, "config_machine_specific", `Do you want to create a machine specific API key?
   209  
   210  Rclone has it's own Jottacloud API KEY which works fine as long as one
   211  only uses rclone on a single machine. When you want to use rclone with
   212  this account on more than one machine it's recommended to create a
   213  machine specific API key. These keys can NOT be shared between
   214  machines.`)
   215  	case "legacy_api":
   216  		srv := rest.NewClient(fshttp.NewClient(ctx))
   217  		if config.Result == "true" {
   218  			deviceRegistration, err := registerDevice(ctx, srv)
   219  			if err != nil {
   220  				return nil, fmt.Errorf("failed to register device: %w", err)
   221  			}
   222  			m.Set(configClientID, deviceRegistration.ClientID)
   223  			m.Set(configClientSecret, obscure.MustObscure(deviceRegistration.ClientSecret))
   224  			fs.Debugf(nil, "Got clientID %q and clientSecret %q", deviceRegistration.ClientID, deviceRegistration.ClientSecret)
   225  		}
   226  		return fs.ConfigInput("legacy_username", "config_username", "Username (e-mail address)")
   227  	case "legacy_username":
   228  		m.Set(configUsername, config.Result)
   229  		return fs.ConfigPassword("legacy_password", "config_password", "Password (only used in setup, will not be stored)")
   230  	case "legacy_password":
   231  		m.Set("password", config.Result)
   232  		m.Set("auth_code", "")
   233  		return fs.ConfigGoto("legacy_do_auth")
   234  	case "legacy_auth_code":
   235  		authCode := strings.ReplaceAll(config.Result, "-", "") // remove any "-" contained in the code so we have a 6 digit number
   236  		m.Set("auth_code", authCode)
   237  		return fs.ConfigGoto("legacy_do_auth")
   238  	case "legacy_do_auth":
   239  		username, _ := m.Get(configUsername)
   240  		password, _ := m.Get("password")
   241  		password = obscure.MustReveal(password)
   242  		authCode, _ := m.Get("auth_code")
   243  
   244  		srv := rest.NewClient(fshttp.NewClient(ctx))
   245  		clientID, ok := m.Get(configClientID)
   246  		if !ok {
   247  			clientID = legacyClientID
   248  		}
   249  		clientSecret, ok := m.Get(configClientSecret)
   250  		if !ok {
   251  			clientSecret = legacyEncryptedClientSecret
   252  		}
   253  
   254  		oauthConfig := &oauth2.Config{
   255  			Endpoint: oauth2.Endpoint{
   256  				AuthURL: legacyTokenURL,
   257  			},
   258  			ClientID:     clientID,
   259  			ClientSecret: obscure.MustReveal(clientSecret),
   260  		}
   261  		token, err := doLegacyAuth(ctx, srv, oauthConfig, username, password, authCode)
   262  		if err == errAuthCodeRequired {
   263  			return fs.ConfigInput("legacy_auth_code", "config_auth_code", "Verification Code\nThis account uses 2 factor authentication you will receive a verification code via SMS.")
   264  		}
   265  		m.Set("password", "")
   266  		m.Set("auth_code", "")
   267  		if err != nil {
   268  			return nil, fmt.Errorf("failed to get oauth token: %w", err)
   269  		}
   270  		err = oauthutil.PutToken(name, m, &token, true)
   271  		if err != nil {
   272  			return nil, fmt.Errorf("error while saving token: %w", err)
   273  		}
   274  		return fs.ConfigGoto("choose_device")
   275  	case "telia_se": // telia_se cloud config
   276  		m.Set("configVersion", fmt.Sprint(configVersion))
   277  		m.Set(configClientID, teliaseCloudClientID)
   278  		m.Set(configTokenURL, teliaseCloudTokenURL)
   279  		return oauthutil.ConfigOut("choose_device", &oauthutil.Options{
   280  			OAuth2Config: &oauth2.Config{
   281  				Endpoint: oauth2.Endpoint{
   282  					AuthURL:  teliaseCloudAuthURL,
   283  					TokenURL: teliaseCloudTokenURL,
   284  				},
   285  				ClientID:    teliaseCloudClientID,
   286  				Scopes:      []string{"openid", "jotta-default", "offline_access"},
   287  				RedirectURL: oauthutil.RedirectLocalhostURL,
   288  			},
   289  		})
   290  	case "telia_no": // telia_no cloud config
   291  		m.Set("configVersion", fmt.Sprint(configVersion))
   292  		m.Set(configClientID, telianoCloudClientID)
   293  		m.Set(configTokenURL, telianoCloudTokenURL)
   294  		return oauthutil.ConfigOut("choose_device", &oauthutil.Options{
   295  			OAuth2Config: &oauth2.Config{
   296  				Endpoint: oauth2.Endpoint{
   297  					AuthURL:  telianoCloudAuthURL,
   298  					TokenURL: telianoCloudTokenURL,
   299  				},
   300  				ClientID:    telianoCloudClientID,
   301  				Scopes:      []string{"openid", "jotta-default", "offline_access"},
   302  				RedirectURL: oauthutil.RedirectLocalhostURL,
   303  			},
   304  		})
   305  	case "tele2": // tele2 cloud config
   306  		m.Set("configVersion", fmt.Sprint(configVersion))
   307  		m.Set(configClientID, tele2CloudClientID)
   308  		m.Set(configTokenURL, tele2CloudTokenURL)
   309  		return oauthutil.ConfigOut("choose_device", &oauthutil.Options{
   310  			OAuth2Config: &oauth2.Config{
   311  				Endpoint: oauth2.Endpoint{
   312  					AuthURL:  tele2CloudAuthURL,
   313  					TokenURL: tele2CloudTokenURL,
   314  				},
   315  				ClientID:    tele2CloudClientID,
   316  				Scopes:      []string{"openid", "jotta-default", "offline_access"},
   317  				RedirectURL: oauthutil.RedirectLocalhostURL,
   318  			},
   319  		})
   320  	case "onlime": // onlime cloud config
   321  		m.Set("configVersion", fmt.Sprint(configVersion))
   322  		m.Set(configClientID, onlimeCloudClientID)
   323  		m.Set(configTokenURL, onlimeCloudTokenURL)
   324  		return oauthutil.ConfigOut("choose_device", &oauthutil.Options{
   325  			OAuth2Config: &oauth2.Config{
   326  				Endpoint: oauth2.Endpoint{
   327  					AuthURL:  onlimeCloudAuthURL,
   328  					TokenURL: onlimeCloudTokenURL,
   329  				},
   330  				ClientID:    onlimeCloudClientID,
   331  				Scopes:      []string{"openid", "jotta-default", "offline_access"},
   332  				RedirectURL: oauthutil.RedirectLocalhostURL,
   333  			},
   334  		})
   335  	case "choose_device":
   336  		return fs.ConfigConfirm("choose_device_query", false, "config_non_standard", `Use a non-standard device/mountpoint?
   337  Choosing no, the default, will let you access the storage used for the archive
   338  section of the official Jottacloud client. If you instead want to access the
   339  sync or the backup section, for example, you must choose yes.`)
   340  
   341  	case "choose_device_query":
   342  		if config.Result != "true" {
   343  			m.Set(configDevice, "")
   344  			m.Set(configMountpoint, "")
   345  			return fs.ConfigGoto("end")
   346  		}
   347  		oAuthClient, _, err := getOAuthClient(ctx, name, m)
   348  		if err != nil {
   349  			return nil, err
   350  		}
   351  		jfsSrv := rest.NewClient(oAuthClient).SetRoot(jfsURL)
   352  		apiSrv := rest.NewClient(oAuthClient).SetRoot(apiURL)
   353  
   354  		cust, err := getCustomerInfo(ctx, apiSrv)
   355  		if err != nil {
   356  			return nil, err
   357  		}
   358  
   359  		acc, err := getDriveInfo(ctx, jfsSrv, cust.Username)
   360  		if err != nil {
   361  			return nil, err
   362  		}
   363  
   364  		deviceNames := make([]string, len(acc.Devices))
   365  		for i, dev := range acc.Devices {
   366  			if i > 0 && dev.Name == defaultDevice {
   367  				// Insert the special Jotta device as first entry, making it the default choice.
   368  				copy(deviceNames[1:i+1], deviceNames[0:i])
   369  				deviceNames[0] = dev.Name
   370  			} else {
   371  				deviceNames[i] = dev.Name
   372  			}
   373  		}
   374  
   375  		help := fmt.Sprintf(`The device to use. In standard setup the built-in %s device is used,
   376  which contains predefined mountpoints for archive, sync etc. All other devices
   377  are treated as backup devices by the official Jottacloud client. You may create
   378  a new by entering a unique name.`, defaultDevice)
   379  		return fs.ConfigChoose("choose_device_result", "config_device", help, len(deviceNames), func(i int) (string, string) {
   380  			return deviceNames[i], ""
   381  		})
   382  	case "choose_device_result":
   383  		device := config.Result
   384  
   385  		oAuthClient, _, err := getOAuthClient(ctx, name, m)
   386  		if err != nil {
   387  			return nil, err
   388  		}
   389  		jfsSrv := rest.NewClient(oAuthClient).SetRoot(jfsURL)
   390  		apiSrv := rest.NewClient(oAuthClient).SetRoot(apiURL)
   391  
   392  		cust, err := getCustomerInfo(ctx, apiSrv)
   393  		if err != nil {
   394  			return nil, err
   395  		}
   396  
   397  		acc, err := getDriveInfo(ctx, jfsSrv, cust.Username)
   398  		if err != nil {
   399  			return nil, err
   400  		}
   401  		isNew := true
   402  		for _, dev := range acc.Devices {
   403  			if strings.EqualFold(dev.Name, device) { // If device name exists with different casing we prefer the existing (not sure if and how the api handles the opposite)
   404  				device = dev.Name // Prefer same casing as existing, e.g. if user entered "jotta" we use the standard casing "Jotta" instead
   405  				isNew = false
   406  				break
   407  			}
   408  		}
   409  		var dev *api.JottaDevice
   410  		if isNew {
   411  			fs.Debugf(nil, "Creating new device: %s", device)
   412  			dev, err = createDevice(ctx, jfsSrv, path.Join(cust.Username, device))
   413  			if err != nil {
   414  				return nil, err
   415  			}
   416  		}
   417  		m.Set(configDevice, device)
   418  
   419  		if !isNew {
   420  			dev, err = getDeviceInfo(ctx, jfsSrv, path.Join(cust.Username, device))
   421  			if err != nil {
   422  				return nil, err
   423  			}
   424  		}
   425  
   426  		var help string
   427  		if device == defaultDevice {
   428  			// With built-in Jotta device the mountpoint choice is exclusive,
   429  			// we do not want to risk any problems by creating new mountpoints on it.
   430  			help = fmt.Sprintf(`The mountpoint to use on the built-in device %s.
   431  The standard setup is to use the %s mountpoint. Most other mountpoints
   432  have very limited support in rclone and should generally be avoided.`, defaultDevice, defaultMountpoint)
   433  			return fs.ConfigChooseExclusive("choose_device_mountpoint", "config_mountpoint", help, len(dev.MountPoints), func(i int) (string, string) {
   434  				return dev.MountPoints[i].Name, ""
   435  			})
   436  		}
   437  		help = fmt.Sprintf(`The mountpoint to use on the non-standard device %s.
   438  You may create a new by entering a unique name.`, device)
   439  		return fs.ConfigChoose("choose_device_mountpoint", "config_mountpoint", help, len(dev.MountPoints), func(i int) (string, string) {
   440  			return dev.MountPoints[i].Name, ""
   441  		})
   442  	case "choose_device_mountpoint":
   443  		mountpoint := config.Result
   444  
   445  		oAuthClient, _, err := getOAuthClient(ctx, name, m)
   446  		if err != nil {
   447  			return nil, err
   448  		}
   449  		jfsSrv := rest.NewClient(oAuthClient).SetRoot(jfsURL)
   450  		apiSrv := rest.NewClient(oAuthClient).SetRoot(apiURL)
   451  
   452  		cust, err := getCustomerInfo(ctx, apiSrv)
   453  		if err != nil {
   454  			return nil, err
   455  		}
   456  
   457  		device, _ := m.Get(configDevice)
   458  
   459  		dev, err := getDeviceInfo(ctx, jfsSrv, path.Join(cust.Username, device))
   460  		if err != nil {
   461  			return nil, err
   462  		}
   463  		isNew := true
   464  		for _, mnt := range dev.MountPoints {
   465  			if strings.EqualFold(mnt.Name, mountpoint) {
   466  				mountpoint = mnt.Name
   467  				isNew = false
   468  				break
   469  			}
   470  		}
   471  
   472  		if isNew {
   473  			if device == defaultDevice {
   474  				return nil, fmt.Errorf("custom mountpoints not supported on built-in %s device: %w", defaultDevice, err)
   475  			}
   476  			fs.Debugf(nil, "Creating new mountpoint: %s", mountpoint)
   477  			_, err := createMountPoint(ctx, jfsSrv, path.Join(cust.Username, device, mountpoint))
   478  			if err != nil {
   479  				return nil, err
   480  			}
   481  		}
   482  		m.Set(configMountpoint, mountpoint)
   483  
   484  		return fs.ConfigGoto("end")
   485  	case "end":
   486  		// All the config flows end up here in case we need to carry on with something
   487  		return nil, nil
   488  	}
   489  	return nil, fmt.Errorf("unknown state %q", config.State)
   490  }
   491  
   492  // Options defines the configuration for this backend
   493  type Options struct {
   494  	Device             string               `config:"device"`
   495  	Mountpoint         string               `config:"mountpoint"`
   496  	MD5MemoryThreshold fs.SizeSuffix        `config:"md5_memory_limit"`
   497  	TrashedOnly        bool                 `config:"trashed_only"`
   498  	HardDelete         bool                 `config:"hard_delete"`
   499  	NoVersions         bool                 `config:"no_versions"`
   500  	UploadThreshold    fs.SizeSuffix        `config:"upload_resume_limit"`
   501  	Enc                encoder.MultiEncoder `config:"encoding"`
   502  }
   503  
   504  // Fs represents a remote jottacloud
   505  type Fs struct {
   506  	name             string
   507  	root             string
   508  	user             string
   509  	opt              Options
   510  	features         *fs.Features
   511  	fileEndpoint     string
   512  	allocateEndpoint string
   513  	jfsSrv           *rest.Client
   514  	apiSrv           *rest.Client
   515  	pacer            *fs.Pacer
   516  	tokenRenewer     *oauthutil.Renew // renew the token on expiry
   517  }
   518  
   519  // Object describes a jottacloud object
   520  //
   521  // Will definitely have info but maybe not meta
   522  type Object struct {
   523  	fs          *Fs
   524  	remote      string
   525  	hasMetaData bool
   526  	size        int64
   527  	createTime  time.Time
   528  	modTime     time.Time
   529  	updateTime  time.Time
   530  	md5         string
   531  	mimeType    string
   532  }
   533  
   534  // ------------------------------------------------------------
   535  
   536  // Name of the remote (as passed into NewFs)
   537  func (f *Fs) Name() string {
   538  	return f.name
   539  }
   540  
   541  // Root of the remote (as passed into NewFs)
   542  func (f *Fs) Root() string {
   543  	return f.root
   544  }
   545  
   546  // String converts this Fs to a string
   547  func (f *Fs) String() string {
   548  	return fmt.Sprintf("jottacloud root '%s'", f.root)
   549  }
   550  
   551  // Features returns the optional features of this Fs
   552  func (f *Fs) Features() *fs.Features {
   553  	return f.features
   554  }
   555  
   556  // joinPath joins two path/url elements
   557  //
   558  // Does not perform clean on the result like path.Join does,
   559  // which breaks urls by changing prefix "https://" into "https:/".
   560  func joinPath(base string, rel string) string {
   561  	if rel == "" {
   562  		return base
   563  	}
   564  	if strings.HasSuffix(base, "/") {
   565  		return base + strings.TrimPrefix(rel, "/")
   566  	}
   567  	if strings.HasPrefix(rel, "/") {
   568  		return strings.TrimSuffix(base, "/") + rel
   569  	}
   570  	return base + "/" + rel
   571  }
   572  
   573  // retryErrorCodes is a slice of error codes that we will retry
   574  var retryErrorCodes = []int{
   575  	429, // Too Many Requests.
   576  	500, // Internal Server Error
   577  	502, // Bad Gateway
   578  	503, // Service Unavailable
   579  	504, // Gateway Timeout
   580  	509, // Bandwidth Limit Exceeded
   581  }
   582  
   583  // shouldRetry returns a boolean as to whether this resp and err
   584  // deserve to be retried.  It returns the err as a convenience
   585  func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
   586  	if fserrors.ContextError(ctx, &err) {
   587  		return false, err
   588  	}
   589  	return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
   590  }
   591  
   592  // registerDevice register a new device for use with the jottacloud API
   593  func registerDevice(ctx context.Context, srv *rest.Client) (reg *api.DeviceRegistrationResponse, err error) {
   594  	// random generator to generate random device names
   595  	seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))
   596  	randonDeviceNamePartLength := 21
   597  	randomDeviceNamePart := make([]byte, randonDeviceNamePartLength)
   598  	charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
   599  	for i := range randomDeviceNamePart {
   600  		randomDeviceNamePart[i] = charset[seededRand.Intn(len(charset))]
   601  	}
   602  	randomDeviceName := "rclone-" + string(randomDeviceNamePart)
   603  	fs.Debugf(nil, "Trying to register device '%s'", randomDeviceName)
   604  
   605  	values := url.Values{}
   606  	values.Set("device_id", randomDeviceName)
   607  
   608  	opts := rest.Opts{
   609  		Method:       "POST",
   610  		RootURL:      legacyRegisterURL,
   611  		ContentType:  "application/x-www-form-urlencoded",
   612  		ExtraHeaders: map[string]string{"Authorization": "Bearer c2xrZmpoYWRsZmFramhkc2xma2phaHNkbGZramhhc2xkZmtqaGFzZGxrZmpobGtq"},
   613  		Parameters:   values,
   614  	}
   615  
   616  	var deviceRegistration *api.DeviceRegistrationResponse
   617  	_, err = srv.CallJSON(ctx, &opts, nil, &deviceRegistration)
   618  	return deviceRegistration, err
   619  }
   620  
   621  var errAuthCodeRequired = errors.New("auth code required")
   622  
   623  // doLegacyAuth runs the actual token request for V1 authentication
   624  //
   625  // Call this first with blank authCode. If errAuthCodeRequired is
   626  // returned then call it again with an authCode
   627  func doLegacyAuth(ctx context.Context, srv *rest.Client, oauthConfig *oauth2.Config, username, password, authCode string) (token oauth2.Token, err error) {
   628  	// prepare out token request with username and password
   629  	values := url.Values{}
   630  	values.Set("grant_type", "PASSWORD")
   631  	values.Set("password", password)
   632  	values.Set("username", username)
   633  	values.Set("client_id", oauthConfig.ClientID)
   634  	values.Set("client_secret", oauthConfig.ClientSecret)
   635  	opts := rest.Opts{
   636  		Method:      "POST",
   637  		RootURL:     oauthConfig.Endpoint.AuthURL,
   638  		ContentType: "application/x-www-form-urlencoded",
   639  		Parameters:  values,
   640  	}
   641  	if authCode != "" {
   642  		opts.ExtraHeaders = make(map[string]string)
   643  		opts.ExtraHeaders["X-Jottacloud-Otp"] = authCode
   644  	}
   645  
   646  	// do the first request
   647  	var jsonToken api.TokenJSON
   648  	resp, err := srv.CallJSON(ctx, &opts, nil, &jsonToken)
   649  	if err != nil && authCode == "" {
   650  		// if 2fa is enabled the first request is expected to fail. We will do another request with the 2fa code as an additional http header
   651  		if resp != nil {
   652  			if resp.Header.Get("X-JottaCloud-OTP") == "required; SMS" {
   653  				return token, errAuthCodeRequired
   654  			}
   655  		}
   656  	}
   657  
   658  	token.AccessToken = jsonToken.AccessToken
   659  	token.RefreshToken = jsonToken.RefreshToken
   660  	token.TokenType = jsonToken.TokenType
   661  	token.Expiry = time.Now().Add(time.Duration(jsonToken.ExpiresIn) * time.Second)
   662  	return token, err
   663  }
   664  
   665  // doTokenAuth runs the actual token request for V2 authentication
   666  func doTokenAuth(ctx context.Context, apiSrv *rest.Client, loginTokenBase64 string) (token oauth2.Token, tokenEndpoint string, err error) {
   667  	loginTokenBytes, err := base64.RawURLEncoding.DecodeString(loginTokenBase64)
   668  	if err != nil {
   669  		return token, "", err
   670  	}
   671  
   672  	// decode login token
   673  	var loginToken api.LoginToken
   674  	decoder := json.NewDecoder(bytes.NewReader(loginTokenBytes))
   675  	err = decoder.Decode(&loginToken)
   676  	if err != nil {
   677  		return token, "", err
   678  	}
   679  
   680  	// retrieve endpoint urls
   681  	opts := rest.Opts{
   682  		Method:  "GET",
   683  		RootURL: loginToken.WellKnownLink,
   684  	}
   685  	var wellKnown api.WellKnown
   686  	_, err = apiSrv.CallJSON(ctx, &opts, nil, &wellKnown)
   687  	if err != nil {
   688  		return token, "", err
   689  	}
   690  
   691  	// prepare out token request with username and password
   692  	values := url.Values{}
   693  	values.Set("client_id", defaultClientID)
   694  	values.Set("grant_type", "password")
   695  	values.Set("password", loginToken.AuthToken)
   696  	values.Set("scope", "openid offline_access")
   697  	values.Set("username", loginToken.Username)
   698  	values.Encode()
   699  	opts = rest.Opts{
   700  		Method:      "POST",
   701  		RootURL:     wellKnown.TokenEndpoint,
   702  		ContentType: "application/x-www-form-urlencoded",
   703  		Body:        strings.NewReader(values.Encode()),
   704  	}
   705  
   706  	// do the first request
   707  	var jsonToken api.TokenJSON
   708  	_, err = apiSrv.CallJSON(ctx, &opts, nil, &jsonToken)
   709  	if err != nil {
   710  		return token, "", err
   711  	}
   712  
   713  	token.AccessToken = jsonToken.AccessToken
   714  	token.RefreshToken = jsonToken.RefreshToken
   715  	token.TokenType = jsonToken.TokenType
   716  	token.Expiry = time.Now().Add(time.Duration(jsonToken.ExpiresIn) * time.Second)
   717  	return token, wellKnown.TokenEndpoint, err
   718  }
   719  
   720  // getCustomerInfo queries general information about the account
   721  func getCustomerInfo(ctx context.Context, apiSrv *rest.Client) (info *api.CustomerInfo, err error) {
   722  	opts := rest.Opts{
   723  		Method: "GET",
   724  		Path:   "account/v1/customer",
   725  	}
   726  
   727  	_, err = apiSrv.CallJSON(ctx, &opts, nil, &info)
   728  	if err != nil {
   729  		return nil, fmt.Errorf("couldn't get customer info: %w", err)
   730  	}
   731  
   732  	return info, nil
   733  }
   734  
   735  // getDriveInfo queries general information about the account and the available devices and mountpoints.
   736  func getDriveInfo(ctx context.Context, srv *rest.Client, username string) (info *api.DriveInfo, err error) {
   737  	opts := rest.Opts{
   738  		Method: "GET",
   739  		Path:   username,
   740  	}
   741  
   742  	_, err = srv.CallXML(ctx, &opts, nil, &info)
   743  	if err != nil {
   744  		return nil, fmt.Errorf("couldn't get drive info: %w", err)
   745  	}
   746  
   747  	return info, nil
   748  }
   749  
   750  // getDeviceInfo queries Information about a jottacloud device
   751  func getDeviceInfo(ctx context.Context, srv *rest.Client, path string) (info *api.JottaDevice, err error) {
   752  	opts := rest.Opts{
   753  		Method: "GET",
   754  		Path:   urlPathEscape(path),
   755  	}
   756  
   757  	_, err = srv.CallXML(ctx, &opts, nil, &info)
   758  	if err != nil {
   759  		return nil, fmt.Errorf("couldn't get device info: %w", err)
   760  	}
   761  
   762  	return info, nil
   763  }
   764  
   765  // createDevice makes a device
   766  func createDevice(ctx context.Context, srv *rest.Client, path string) (info *api.JottaDevice, err error) {
   767  	opts := rest.Opts{
   768  		Method:     "POST",
   769  		Path:       urlPathEscape(path),
   770  		Parameters: url.Values{},
   771  	}
   772  
   773  	opts.Parameters.Set("type", "WORKSTATION")
   774  
   775  	_, err = srv.CallXML(ctx, &opts, nil, &info)
   776  	if err != nil {
   777  		return nil, fmt.Errorf("couldn't create device: %w", err)
   778  	}
   779  	return info, nil
   780  }
   781  
   782  // createMountPoint makes a mount point
   783  func createMountPoint(ctx context.Context, srv *rest.Client, path string) (info *api.JottaMountPoint, err error) {
   784  	opts := rest.Opts{
   785  		Method: "POST",
   786  		Path:   urlPathEscape(path),
   787  	}
   788  
   789  	_, err = srv.CallXML(ctx, &opts, nil, &info)
   790  	if err != nil {
   791  		return nil, fmt.Errorf("couldn't create mountpoint: %w", err)
   792  	}
   793  	return info, nil
   794  }
   795  
   796  // setEndpoints generates the API endpoints
   797  func (f *Fs) setEndpoints() {
   798  	if f.opt.Device == "" {
   799  		f.opt.Device = defaultDevice
   800  	}
   801  	if f.opt.Mountpoint == "" {
   802  		f.opt.Mountpoint = defaultMountpoint
   803  	}
   804  	f.fileEndpoint = path.Join(f.user, f.opt.Device, f.opt.Mountpoint)
   805  	f.allocateEndpoint = path.Join("/jfs", f.opt.Device, f.opt.Mountpoint)
   806  }
   807  
   808  // readMetaDataForPath reads the metadata from the path
   809  func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.JottaFile, err error) {
   810  	opts := rest.Opts{
   811  		Method: "GET",
   812  		Path:   f.filePath(path),
   813  	}
   814  	var result api.JottaFile
   815  	var resp *http.Response
   816  	err = f.pacer.Call(func() (bool, error) {
   817  		resp, err = f.jfsSrv.CallXML(ctx, &opts, nil, &result)
   818  		return shouldRetry(ctx, resp, err)
   819  	})
   820  
   821  	if apiErr, ok := err.(*api.Error); ok {
   822  		// does not exist
   823  		if apiErr.StatusCode == http.StatusNotFound {
   824  			return nil, fs.ErrorObjectNotFound
   825  		}
   826  	}
   827  
   828  	if err != nil {
   829  		return nil, fmt.Errorf("read metadata failed: %w", err)
   830  	}
   831  	if result.XMLName.Local == "folder" {
   832  		return nil, fs.ErrorIsDir
   833  	} else if result.XMLName.Local != "file" {
   834  		return nil, fs.ErrorNotAFile
   835  	}
   836  	return &result, nil
   837  }
   838  
   839  // errorHandler parses a non 2xx error response into an error
   840  func errorHandler(resp *http.Response) error {
   841  	// Decode error response
   842  	errResponse := new(api.Error)
   843  	err := rest.DecodeXML(resp, &errResponse)
   844  	if err != nil {
   845  		fs.Debugf(nil, "Couldn't decode error response: %v", err)
   846  	}
   847  	if errResponse.Message == "" {
   848  		errResponse.Message = resp.Status
   849  	}
   850  	if errResponse.StatusCode == 0 {
   851  		errResponse.StatusCode = resp.StatusCode
   852  	}
   853  	return errResponse
   854  }
   855  
   856  // Jottacloud wants '+' to be URL encoded even though the RFC states it's not reserved
   857  func urlPathEscape(in string) string {
   858  	return strings.ReplaceAll(rest.URLPathEscape(in), "+", "%2B")
   859  }
   860  
   861  // filePathRaw returns an unescaped file path (f.root, file)
   862  // Optionally made absolute by prefixing with "/", typically required when used
   863  // as request parameter instead of the path (which is relative to some root url).
   864  func (f *Fs) filePathRaw(file string, absolute bool) string {
   865  	prefix := ""
   866  	if absolute {
   867  		prefix = "/"
   868  	}
   869  	return path.Join(prefix, f.fileEndpoint, f.opt.Enc.FromStandardPath(path.Join(f.root, file)))
   870  }
   871  
   872  // filePath returns an escaped file path (f.root, file)
   873  func (f *Fs) filePath(file string) string {
   874  	return urlPathEscape(f.filePathRaw(file, false))
   875  }
   876  
   877  // allocatePathRaw returns an unescaped allocate file path (f.root, file)
   878  // Optionally made absolute by prefixing with "/", typically required when used
   879  // as request parameter instead of the path (which is relative to some root url).
   880  func (f *Fs) allocatePathRaw(file string, absolute bool) string {
   881  	prefix := ""
   882  	if absolute {
   883  		prefix = "/"
   884  	}
   885  	return path.Join(prefix, f.allocateEndpoint, f.opt.Enc.FromStandardPath(path.Join(f.root, file)))
   886  }
   887  
   888  // Jottacloud requires the grant_type 'refresh_token' string
   889  // to be uppercase and throws a 400 Bad Request if we use the
   890  // lower case used by the oauth2 module
   891  //
   892  // This filter catches all refresh requests, reads the body,
   893  // changes the case and then sends it on
   894  func grantTypeFilter(req *http.Request) {
   895  	if legacyTokenURL == req.URL.String() {
   896  		// read the entire body
   897  		refreshBody, err := io.ReadAll(req.Body)
   898  		if err != nil {
   899  			return
   900  		}
   901  		_ = req.Body.Close()
   902  
   903  		// make the refresh token upper case
   904  		refreshBody = []byte(strings.Replace(string(refreshBody), "grant_type=refresh_token", "grant_type=REFRESH_TOKEN", 1))
   905  
   906  		// set the new ReadCloser (with a dummy Close())
   907  		req.Body = io.NopCloser(bytes.NewReader(refreshBody))
   908  	}
   909  }
   910  
   911  func getOAuthClient(ctx context.Context, name string, m configmap.Mapper) (oAuthClient *http.Client, ts *oauthutil.TokenSource, err error) {
   912  	// Check config version
   913  	var ver int
   914  	version, ok := m.Get("configVersion")
   915  	if ok {
   916  		ver, err = strconv.Atoi(version)
   917  		if err != nil {
   918  			return nil, nil, errors.New("failed to parse config version")
   919  		}
   920  		ok = (ver == configVersion) || (ver == legacyConfigVersion)
   921  	}
   922  	if !ok {
   923  		return nil, nil, errors.New("outdated config - please reconfigure this backend")
   924  	}
   925  
   926  	baseClient := fshttp.NewClient(ctx)
   927  	oauthConfig := &oauth2.Config{
   928  		Endpoint: oauth2.Endpoint{
   929  			AuthURL:  defaultTokenURL,
   930  			TokenURL: defaultTokenURL,
   931  		},
   932  	}
   933  	if ver == configVersion {
   934  		oauthConfig.ClientID = defaultClientID
   935  		// if custom endpoints are set use them else stick with defaults
   936  		if tokenURL, ok := m.Get(configTokenURL); ok {
   937  			oauthConfig.Endpoint.TokenURL = tokenURL
   938  			// jottacloud is weird. we need to use the tokenURL as authURL
   939  			oauthConfig.Endpoint.AuthURL = tokenURL
   940  		}
   941  	} else if ver == legacyConfigVersion {
   942  		clientID, ok := m.Get(configClientID)
   943  		if !ok {
   944  			clientID = legacyClientID
   945  		}
   946  		clientSecret, ok := m.Get(configClientSecret)
   947  		if !ok {
   948  			clientSecret = legacyEncryptedClientSecret
   949  		}
   950  		oauthConfig.ClientID = clientID
   951  		oauthConfig.ClientSecret = obscure.MustReveal(clientSecret)
   952  
   953  		oauthConfig.Endpoint.TokenURL = legacyTokenURL
   954  		oauthConfig.Endpoint.AuthURL = legacyTokenURL
   955  
   956  		// add the request filter to fix token refresh
   957  		if do, ok := baseClient.Transport.(interface {
   958  			SetRequestFilter(f func(req *http.Request))
   959  		}); ok {
   960  			do.SetRequestFilter(grantTypeFilter)
   961  		} else {
   962  			fs.Debugf(name+":", "Couldn't add request filter - uploads will fail")
   963  		}
   964  	}
   965  
   966  	// Create OAuth Client
   967  	oAuthClient, ts, err = oauthutil.NewClientWithBaseClient(ctx, name, m, oauthConfig, baseClient)
   968  	if err != nil {
   969  		return nil, nil, fmt.Errorf("failed to configure Jottacloud oauth client: %w", err)
   970  	}
   971  	return oAuthClient, ts, nil
   972  }
   973  
   974  // NewFs constructs an Fs from the path, container:path
   975  func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
   976  	// Parse config into Options struct
   977  	opt := new(Options)
   978  	err := configstruct.Set(m, opt)
   979  	if err != nil {
   980  		return nil, err
   981  	}
   982  
   983  	oAuthClient, ts, err := getOAuthClient(ctx, name, m)
   984  	if err != nil {
   985  		return nil, err
   986  	}
   987  
   988  	rootIsDir := strings.HasSuffix(root, "/")
   989  	root = strings.Trim(root, "/")
   990  
   991  	f := &Fs{
   992  		name:   name,
   993  		root:   root,
   994  		opt:    *opt,
   995  		jfsSrv: rest.NewClient(oAuthClient).SetRoot(jfsURL),
   996  		apiSrv: rest.NewClient(oAuthClient).SetRoot(apiURL),
   997  		pacer:  fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
   998  	}
   999  	f.features = (&fs.Features{
  1000  		CaseInsensitive:         true,
  1001  		CanHaveEmptyDirectories: true,
  1002  		ReadMimeType:            true,
  1003  		WriteMimeType:           false,
  1004  		ReadMetadata:            true,
  1005  		WriteMetadata:           true,
  1006  		UserMetadata:            false,
  1007  	}).Fill(ctx, f)
  1008  	f.jfsSrv.SetErrorHandler(errorHandler)
  1009  	if opt.TrashedOnly { // we cannot support showing Trashed Files when using ListR right now
  1010  		f.features.ListR = nil
  1011  	}
  1012  
  1013  	// Renew the token in the background
  1014  	f.tokenRenewer = oauthutil.NewRenew(f.String(), ts, func() error {
  1015  		_, err := f.readMetaDataForPath(ctx, "")
  1016  		if err == fs.ErrorNotAFile || err == fs.ErrorIsDir {
  1017  			err = nil
  1018  		}
  1019  		return err
  1020  	})
  1021  
  1022  	cust, err := getCustomerInfo(ctx, f.apiSrv)
  1023  	if err != nil {
  1024  		return nil, err
  1025  	}
  1026  	f.user = cust.Username
  1027  	f.setEndpoints()
  1028  
  1029  	if root != "" && !rootIsDir {
  1030  		// Check to see if the root actually an existing file
  1031  		remote := path.Base(root)
  1032  		f.root = path.Dir(root)
  1033  		if f.root == "." {
  1034  			f.root = ""
  1035  		}
  1036  		_, err := f.NewObject(context.TODO(), remote)
  1037  		if err != nil {
  1038  			if errors.Is(err, fs.ErrorObjectNotFound) || errors.Is(err, fs.ErrorNotAFile) || errors.Is(err, fs.ErrorIsDir) {
  1039  				// File doesn't exist so return old f
  1040  				f.root = root
  1041  				return f, nil
  1042  			}
  1043  			return nil, err
  1044  		}
  1045  		// return an error with an fs which points to the parent
  1046  		return f, fs.ErrorIsFile
  1047  	}
  1048  	return f, nil
  1049  }
  1050  
  1051  // Return an Object from a path
  1052  //
  1053  // If it can't be found it returns the error fs.ErrorObjectNotFound.
  1054  func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.JottaFile) (fs.Object, error) {
  1055  	o := &Object{
  1056  		fs:     f,
  1057  		remote: remote,
  1058  	}
  1059  	var err error
  1060  	if info != nil {
  1061  		if !f.validFile(info) {
  1062  			return nil, fs.ErrorObjectNotFound
  1063  		}
  1064  		err = o.setMetaData(info) // sets the info
  1065  	} else {
  1066  		err = o.readMetaData(ctx, false) // reads info and meta, returning an error
  1067  	}
  1068  	if err != nil {
  1069  		return nil, err
  1070  	}
  1071  	return o, nil
  1072  }
  1073  
  1074  // NewObject finds the Object at remote.  If it can't be found
  1075  // it returns the error fs.ErrorObjectNotFound.
  1076  func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
  1077  	return f.newObjectWithInfo(ctx, remote, nil)
  1078  }
  1079  
  1080  // CreateDir makes a directory
  1081  func (f *Fs) CreateDir(ctx context.Context, path string) (jf *api.JottaFolder, err error) {
  1082  	// fs.Debugf(f, "CreateDir(%q, %q)\n", pathID, leaf)
  1083  	var resp *http.Response
  1084  	opts := rest.Opts{
  1085  		Method:     "POST",
  1086  		Path:       f.filePath(path),
  1087  		Parameters: url.Values{},
  1088  	}
  1089  
  1090  	opts.Parameters.Set("mkDir", "true")
  1091  
  1092  	err = f.pacer.Call(func() (bool, error) {
  1093  		resp, err = f.jfsSrv.CallXML(ctx, &opts, nil, &jf)
  1094  		return shouldRetry(ctx, resp, err)
  1095  	})
  1096  	if err != nil {
  1097  		//fmt.Printf("...Error %v\n", err)
  1098  		return nil, err
  1099  	}
  1100  	// fmt.Printf("...Id %q\n", *info.Id)
  1101  	return jf, nil
  1102  }
  1103  
  1104  // List the objects and directories in dir into entries.  The
  1105  // entries can be returned in any order but should be for a
  1106  // complete directory.
  1107  //
  1108  // dir should be "" to list the root, and should not have
  1109  // trailing slashes.
  1110  //
  1111  // This should return ErrDirNotFound if the directory isn't
  1112  // found.
  1113  func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
  1114  	opts := rest.Opts{
  1115  		Method: "GET",
  1116  		Path:   f.filePath(dir),
  1117  	}
  1118  
  1119  	var resp *http.Response
  1120  	var result api.JottaFolder
  1121  	err = f.pacer.Call(func() (bool, error) {
  1122  		resp, err = f.jfsSrv.CallXML(ctx, &opts, nil, &result)
  1123  		return shouldRetry(ctx, resp, err)
  1124  	})
  1125  
  1126  	if err != nil {
  1127  		if apiErr, ok := err.(*api.Error); ok {
  1128  			// does not exist
  1129  			if apiErr.StatusCode == http.StatusNotFound {
  1130  				return nil, fs.ErrorDirNotFound
  1131  			}
  1132  		}
  1133  		return nil, fmt.Errorf("couldn't list files: %w", err)
  1134  	}
  1135  
  1136  	if !f.validFolder(&result) {
  1137  		return nil, fs.ErrorDirNotFound
  1138  	}
  1139  
  1140  	for i := range result.Folders {
  1141  		item := &result.Folders[i]
  1142  		if f.validFolder(item) {
  1143  			remote := path.Join(dir, f.opt.Enc.ToStandardName(item.Name))
  1144  			d := fs.NewDir(remote, time.Time(item.ModifiedAt))
  1145  			entries = append(entries, d)
  1146  		}
  1147  	}
  1148  
  1149  	for i := range result.Files {
  1150  		item := &result.Files[i]
  1151  		if f.validFile(item) {
  1152  			remote := path.Join(dir, f.opt.Enc.ToStandardName(item.Name))
  1153  			if o, err := f.newObjectWithInfo(ctx, remote, item); err == nil {
  1154  				entries = append(entries, o)
  1155  			}
  1156  		}
  1157  	}
  1158  	return entries, nil
  1159  }
  1160  
  1161  func parseListRStream(ctx context.Context, r io.Reader, filesystem *Fs, callback func(fs.DirEntry) error) error {
  1162  
  1163  	type stats struct {
  1164  		Folders int `xml:"folders"`
  1165  		Files   int `xml:"files"`
  1166  	}
  1167  	var expected, actual stats
  1168  
  1169  	type xmlFile struct {
  1170  		Path     string          `xml:"path"`
  1171  		Name     string          `xml:"filename"`
  1172  		Checksum string          `xml:"md5"`
  1173  		Size     int64           `xml:"size"`
  1174  		Modified api.Rfc3339Time `xml:"modified"` // Note: Liststream response includes 3 decimal milliseconds, but we ignore them since there is second precision everywhere else
  1175  		Created  api.Rfc3339Time `xml:"created"`
  1176  	}
  1177  
  1178  	type xmlFolder struct {
  1179  		Path string `xml:"path"`
  1180  	}
  1181  
  1182  	addFolder := func(path string) error {
  1183  		return callback(fs.NewDir(filesystem.opt.Enc.ToStandardPath(path), time.Time{}))
  1184  	}
  1185  
  1186  	addFile := func(f *xmlFile) error {
  1187  		return callback(&Object{
  1188  			hasMetaData: true,
  1189  			fs:          filesystem,
  1190  			remote:      filesystem.opt.Enc.ToStandardPath(path.Join(f.Path, f.Name)),
  1191  			size:        f.Size,
  1192  			md5:         f.Checksum,
  1193  			createTime:  time.Time(f.Created),
  1194  			modTime:     time.Time(f.Modified),
  1195  		})
  1196  	}
  1197  
  1198  	// liststream paths are /mountpoint/root/path
  1199  	// so the returned paths should have /mountpoint/root/ trimmed
  1200  	// as the caller is expecting path.
  1201  	pathPrefix := filesystem.opt.Enc.FromStandardPath(path.Join("/", filesystem.opt.Mountpoint, filesystem.root))
  1202  	trimPathPrefix := func(p string) string {
  1203  		p = strings.TrimPrefix(p, pathPrefix)
  1204  		p = strings.TrimPrefix(p, "/")
  1205  		return p
  1206  	}
  1207  
  1208  	uniqueFolders := map[string]bool{}
  1209  	decoder := xml.NewDecoder(r)
  1210  
  1211  	for {
  1212  		t, err := decoder.Token()
  1213  		if err != nil {
  1214  			if err != io.EOF {
  1215  				return err
  1216  			}
  1217  			break
  1218  		}
  1219  		switch se := t.(type) {
  1220  		case xml.StartElement:
  1221  			switch se.Name.Local {
  1222  			case "file":
  1223  				var f xmlFile
  1224  				if err := decoder.DecodeElement(&f, &se); err != nil {
  1225  					return err
  1226  				}
  1227  				f.Path = trimPathPrefix(f.Path)
  1228  				actual.Files++
  1229  				if !uniqueFolders[f.Path] {
  1230  					uniqueFolders[f.Path] = true
  1231  					actual.Folders++
  1232  					if err := addFolder(f.Path); err != nil {
  1233  						return err
  1234  					}
  1235  				}
  1236  				if err := addFile(&f); err != nil {
  1237  					return err
  1238  				}
  1239  			case "folder":
  1240  				var f xmlFolder
  1241  				if err := decoder.DecodeElement(&f, &se); err != nil {
  1242  					return err
  1243  				}
  1244  				f.Path = trimPathPrefix(f.Path)
  1245  				uniqueFolders[f.Path] = true
  1246  				actual.Folders++
  1247  				if err := addFolder(f.Path); err != nil {
  1248  					return err
  1249  				}
  1250  			case "stats":
  1251  				if err := decoder.DecodeElement(&expected, &se); err != nil {
  1252  					return err
  1253  				}
  1254  			}
  1255  		}
  1256  	}
  1257  
  1258  	if expected.Folders != actual.Folders ||
  1259  		expected.Files != actual.Files {
  1260  		return fmt.Errorf("invalid result from listStream: expected[%#v] != actual[%#v]", expected, actual)
  1261  	}
  1262  	return nil
  1263  }
  1264  
  1265  // ListR lists the objects and directories of the Fs starting
  1266  // from dir recursively into out.
  1267  //
  1268  // dir should be "" to start from the root, and should not
  1269  // have trailing slashes.
  1270  func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (err error) {
  1271  	opts := rest.Opts{
  1272  		Method:     "GET",
  1273  		Path:       f.filePath(dir),
  1274  		Parameters: url.Values{},
  1275  	}
  1276  	opts.Parameters.Set("mode", "liststream")
  1277  	list := walk.NewListRHelper(callback)
  1278  
  1279  	var resp *http.Response
  1280  	err = f.pacer.Call(func() (bool, error) {
  1281  		resp, err = f.jfsSrv.Call(ctx, &opts)
  1282  		if err != nil {
  1283  			return shouldRetry(ctx, resp, err)
  1284  		}
  1285  
  1286  		err = parseListRStream(ctx, resp.Body, f, func(d fs.DirEntry) error {
  1287  			if d.Remote() == dir {
  1288  				return nil
  1289  			}
  1290  			return list.Add(d)
  1291  		})
  1292  		_ = resp.Body.Close()
  1293  		return shouldRetry(ctx, resp, err)
  1294  	})
  1295  	if err != nil {
  1296  		if apiErr, ok := err.(*api.Error); ok {
  1297  			// does not exist
  1298  			if apiErr.StatusCode == http.StatusNotFound {
  1299  				return fs.ErrorDirNotFound
  1300  			}
  1301  		}
  1302  		return fmt.Errorf("couldn't list files: %w", err)
  1303  	}
  1304  	if err != nil {
  1305  		return err
  1306  	}
  1307  	return list.Flush()
  1308  }
  1309  
  1310  // Creates from the parameters passed in a half finished Object which
  1311  // must have setMetaData called on it
  1312  //
  1313  // Used to create new objects
  1314  func (f *Fs) createObject(remote string, modTime time.Time, size int64) (o *Object) {
  1315  	// Temporary Object under construction
  1316  	o = &Object{
  1317  		fs:      f,
  1318  		remote:  remote,
  1319  		size:    size,
  1320  		modTime: modTime,
  1321  	}
  1322  	return o
  1323  }
  1324  
  1325  // Put the object
  1326  //
  1327  // Copy the reader in to the new object which is returned.
  1328  //
  1329  // The new object may have been created if an error is returned
  1330  func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
  1331  	o := f.createObject(src.Remote(), src.ModTime(ctx), src.Size())
  1332  	return o, o.Update(ctx, in, src, options...)
  1333  }
  1334  
  1335  // mkParentDir makes the parent of the native path dirPath if
  1336  // necessary and any directories above that
  1337  func (f *Fs) mkParentDir(ctx context.Context, dirPath string) error {
  1338  	// defer log.Trace(dirPath, "")("")
  1339  	// chop off trailing / if it exists
  1340  	parent := path.Dir(strings.TrimSuffix(dirPath, "/"))
  1341  	if parent == "." {
  1342  		parent = ""
  1343  	}
  1344  	return f.Mkdir(ctx, parent)
  1345  }
  1346  
  1347  // Mkdir creates the container if it doesn't exist
  1348  func (f *Fs) Mkdir(ctx context.Context, dir string) error {
  1349  	_, err := f.CreateDir(ctx, dir)
  1350  	return err
  1351  }
  1352  
  1353  // purgeCheck removes the root directory, if check is set then it
  1354  // refuses to do so if it has anything in
  1355  func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) (err error) {
  1356  	root := path.Join(f.root, dir)
  1357  	if root == "" {
  1358  		return errors.New("can't purge root directory")
  1359  	}
  1360  
  1361  	// check that the directory exists
  1362  	entries, err := f.List(ctx, dir)
  1363  	if err != nil {
  1364  		return err
  1365  	}
  1366  
  1367  	if check {
  1368  		if len(entries) != 0 {
  1369  			return fs.ErrorDirectoryNotEmpty
  1370  		}
  1371  	}
  1372  
  1373  	opts := rest.Opts{
  1374  		Method:     "POST",
  1375  		Path:       f.filePath(dir),
  1376  		Parameters: url.Values{},
  1377  		NoResponse: true,
  1378  	}
  1379  
  1380  	if f.opt.HardDelete {
  1381  		opts.Parameters.Set("rmDir", "true")
  1382  	} else {
  1383  		opts.Parameters.Set("dlDir", "true")
  1384  	}
  1385  
  1386  	var resp *http.Response
  1387  	err = f.pacer.Call(func() (bool, error) {
  1388  		resp, err = f.jfsSrv.Call(ctx, &opts)
  1389  		return shouldRetry(ctx, resp, err)
  1390  	})
  1391  	if err != nil {
  1392  		return fmt.Errorf("couldn't purge directory: %w", err)
  1393  	}
  1394  
  1395  	return nil
  1396  }
  1397  
  1398  // Rmdir deletes the root folder
  1399  //
  1400  // Returns an error if it isn't empty
  1401  func (f *Fs) Rmdir(ctx context.Context, dir string) error {
  1402  	return f.purgeCheck(ctx, dir, true)
  1403  }
  1404  
  1405  // Precision return the precision of this Fs
  1406  func (f *Fs) Precision() time.Duration {
  1407  	return time.Second
  1408  }
  1409  
  1410  // Purge deletes all the files and the container
  1411  func (f *Fs) Purge(ctx context.Context, dir string) error {
  1412  	return f.purgeCheck(ctx, dir, false)
  1413  }
  1414  
  1415  // createOrUpdate tries to make remote file match without uploading.
  1416  // If the remote file exists, and has matching size and md5, only
  1417  // timestamps are updated. If the file does not exist or does does
  1418  // not match size and md5, but matching content can be constructed
  1419  // from deduplication, the file will be updated/created. If the file
  1420  // is currently in trash, but can be made to match, it will be
  1421  // restored. Returns ErrorObjectNotFound if upload will be necessary
  1422  // to get a matching remote file.
  1423  func (f *Fs) createOrUpdate(ctx context.Context, file string, createTime time.Time, modTime time.Time, size int64, md5 string) (info *api.JottaFile, err error) {
  1424  	opts := rest.Opts{
  1425  		Method:       "POST",
  1426  		Path:         f.filePath(file),
  1427  		Parameters:   url.Values{},
  1428  		ExtraHeaders: make(map[string]string),
  1429  	}
  1430  
  1431  	opts.Parameters.Set("cphash", "true")
  1432  
  1433  	opts.ExtraHeaders["JSize"] = strconv.FormatInt(size, 10)
  1434  	opts.ExtraHeaders["JMd5"] = md5
  1435  	opts.ExtraHeaders["JCreated"] = api.JottaTime(createTime).String()
  1436  	opts.ExtraHeaders["JModified"] = api.JottaTime(modTime).String()
  1437  
  1438  	var resp *http.Response
  1439  	err = f.pacer.Call(func() (bool, error) {
  1440  		resp, err = f.jfsSrv.CallXML(ctx, &opts, nil, &info)
  1441  		return shouldRetry(ctx, resp, err)
  1442  	})
  1443  
  1444  	if apiErr, ok := err.(*api.Error); ok {
  1445  		// does not exist, i.e. not matching size and md5, and not possible to make it by deduplication
  1446  		if apiErr.StatusCode == http.StatusNotFound {
  1447  			return nil, fs.ErrorObjectNotFound
  1448  		}
  1449  	}
  1450  	return info, nil
  1451  }
  1452  
  1453  // copyOrMoves copies or moves directories or files depending on the method parameter
  1454  func (f *Fs) copyOrMove(ctx context.Context, method, src, dest string) (info *api.JottaFile, err error) {
  1455  	opts := rest.Opts{
  1456  		Method:     "POST",
  1457  		Path:       src,
  1458  		Parameters: url.Values{},
  1459  	}
  1460  
  1461  	opts.Parameters.Set(method, f.filePathRaw(dest, true))
  1462  
  1463  	var resp *http.Response
  1464  	err = f.pacer.Call(func() (bool, error) {
  1465  		resp, err = f.jfsSrv.CallXML(ctx, &opts, nil, &info)
  1466  		return shouldRetry(ctx, resp, err)
  1467  	})
  1468  	if err != nil {
  1469  		return nil, err
  1470  	}
  1471  	return info, nil
  1472  }
  1473  
  1474  // Copy src to this remote using server-side copy operations.
  1475  //
  1476  // This is stored with the remote path given.
  1477  //
  1478  // It returns the destination Object and a possible error.
  1479  //
  1480  // Will only be called if src.Fs().Name() == f.Name()
  1481  //
  1482  // If it isn't possible then return fs.ErrorCantCopy
  1483  func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
  1484  	srcObj, ok := src.(*Object)
  1485  	if !ok {
  1486  		fs.Debugf(src, "Can't copy - not same remote type")
  1487  		return nil, fs.ErrorCantMove
  1488  	}
  1489  
  1490  	err := f.mkParentDir(ctx, remote)
  1491  	if err != nil {
  1492  		return nil, err
  1493  	}
  1494  	info, err := f.copyOrMove(ctx, "cp", srcObj.filePath(), remote)
  1495  
  1496  	// if destination was a trashed file then after a successful copy the copied file is still in trash (bug in api?)
  1497  	if err == nil && bool(info.Deleted) && !f.opt.TrashedOnly && info.State == "COMPLETED" {
  1498  		fs.Debugf(src, "Server-side copied to trashed destination, restoring")
  1499  		info, err = f.createOrUpdate(ctx, remote, srcObj.createTime, srcObj.modTime, srcObj.size, srcObj.md5)
  1500  	}
  1501  
  1502  	if err != nil {
  1503  		return nil, fmt.Errorf("couldn't copy file: %w", err)
  1504  	}
  1505  
  1506  	return f.newObjectWithInfo(ctx, remote, info)
  1507  	//return f.newObjectWithInfo(remote, &result)
  1508  }
  1509  
  1510  // Move src to this remote using server-side move operations.
  1511  //
  1512  // This is stored with the remote path given.
  1513  //
  1514  // It returns the destination Object and a possible error.
  1515  //
  1516  // Will only be called if src.Fs().Name() == f.Name()
  1517  //
  1518  // If it isn't possible then return fs.ErrorCantMove
  1519  func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
  1520  	srcObj, ok := src.(*Object)
  1521  	if !ok {
  1522  		fs.Debugf(src, "Can't move - not same remote type")
  1523  		return nil, fs.ErrorCantMove
  1524  	}
  1525  
  1526  	err := f.mkParentDir(ctx, remote)
  1527  	if err != nil {
  1528  		return nil, err
  1529  	}
  1530  	info, err := f.copyOrMove(ctx, "mv", srcObj.filePath(), remote)
  1531  
  1532  	if err != nil {
  1533  		return nil, fmt.Errorf("couldn't move file: %w", err)
  1534  	}
  1535  
  1536  	return f.newObjectWithInfo(ctx, remote, info)
  1537  	//return f.newObjectWithInfo(remote, result)
  1538  }
  1539  
  1540  // DirMove moves src, srcRemote to this remote at dstRemote
  1541  // using server-side move operations.
  1542  //
  1543  // Will only be called if src.Fs().Name() == f.Name()
  1544  //
  1545  // If it isn't possible then return fs.ErrorCantDirMove
  1546  //
  1547  // If destination exists then return fs.ErrorDirExists
  1548  func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
  1549  	srcFs, ok := src.(*Fs)
  1550  	if !ok {
  1551  		fs.Debugf(srcFs, "Can't move directory - not same remote type")
  1552  		return fs.ErrorCantDirMove
  1553  	}
  1554  	srcPath := path.Join(srcFs.root, srcRemote)
  1555  	dstPath := path.Join(f.root, dstRemote)
  1556  
  1557  	// Refuse to move to or from the root
  1558  	if srcPath == "" || dstPath == "" {
  1559  		fs.Debugf(src, "DirMove error: Can't move root")
  1560  		return errors.New("can't move root directory")
  1561  	}
  1562  	//fmt.Printf("Move src: %s (FullPath %s), dst: %s (FullPath: %s)\n", srcRemote, srcPath, dstRemote, dstPath)
  1563  
  1564  	var err error
  1565  	_, err = f.List(ctx, dstRemote)
  1566  	if err == fs.ErrorDirNotFound {
  1567  		// OK
  1568  	} else if err != nil {
  1569  		return err
  1570  	} else {
  1571  		return fs.ErrorDirExists
  1572  	}
  1573  
  1574  	_, err = f.copyOrMove(ctx, "mvDir", path.Join(f.fileEndpoint, f.opt.Enc.FromStandardPath(srcPath))+"/", dstRemote)
  1575  
  1576  	if err != nil {
  1577  		return fmt.Errorf("couldn't move directory: %w", err)
  1578  	}
  1579  	return nil
  1580  }
  1581  
  1582  // PublicLink generates a public link to the remote path (usually readable by anyone)
  1583  func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
  1584  	opts := rest.Opts{
  1585  		Method:     "GET",
  1586  		Path:       f.filePath(remote),
  1587  		Parameters: url.Values{},
  1588  	}
  1589  
  1590  	if unlink {
  1591  		opts.Parameters.Set("mode", "disableShare")
  1592  	} else {
  1593  		opts.Parameters.Set("mode", "enableShare")
  1594  	}
  1595  
  1596  	var resp *http.Response
  1597  	var result api.JottaFile
  1598  	err = f.pacer.Call(func() (bool, error) {
  1599  		resp, err = f.jfsSrv.CallXML(ctx, &opts, nil, &result)
  1600  		return shouldRetry(ctx, resp, err)
  1601  	})
  1602  
  1603  	if apiErr, ok := err.(*api.Error); ok {
  1604  		// does not exist
  1605  		if apiErr.StatusCode == http.StatusNotFound {
  1606  			return "", fs.ErrorObjectNotFound
  1607  		}
  1608  	}
  1609  	if err != nil {
  1610  		if unlink {
  1611  			return "", fmt.Errorf("couldn't remove public link: %w", err)
  1612  		}
  1613  		return "", fmt.Errorf("couldn't create public link: %w", err)
  1614  	}
  1615  	if unlink {
  1616  		if result.PublicURI != "" {
  1617  			return "", fmt.Errorf("couldn't remove public link - %q", result.PublicURI)
  1618  		}
  1619  		return "", nil
  1620  	}
  1621  	if result.PublicURI == "" {
  1622  		return "", errors.New("couldn't create public link - no uri received")
  1623  	}
  1624  	if result.PublicSharePath != "" {
  1625  		webLink := joinPath(wwwURL, result.PublicSharePath)
  1626  		fs.Debugf(nil, "Web link: %s", webLink)
  1627  	} else {
  1628  		fs.Debugf(nil, "No web link received")
  1629  	}
  1630  	directLink := joinPath(wwwURL, fmt.Sprintf("opin/io/downloadPublic/%s/%s", f.user, result.PublicURI))
  1631  	fs.Debugf(nil, "Direct link: %s", directLink)
  1632  	return directLink, nil
  1633  }
  1634  
  1635  // About gets quota information
  1636  func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
  1637  	info, err := getDriveInfo(ctx, f.jfsSrv, f.user)
  1638  	if err != nil {
  1639  		return nil, err
  1640  	}
  1641  
  1642  	usage := &fs.Usage{
  1643  		Used: fs.NewUsageValue(info.Usage),
  1644  	}
  1645  	if info.Capacity > 0 {
  1646  		usage.Total = fs.NewUsageValue(info.Capacity)
  1647  		usage.Free = fs.NewUsageValue(info.Capacity - info.Usage)
  1648  	}
  1649  	return usage, nil
  1650  }
  1651  
  1652  // UserInfo fetches info about the current user
  1653  func (f *Fs) UserInfo(ctx context.Context) (userInfo map[string]string, err error) {
  1654  	cust, err := getCustomerInfo(ctx, f.apiSrv)
  1655  	if err != nil {
  1656  		return nil, err
  1657  	}
  1658  	return map[string]string{
  1659  		"Username":         cust.Username,
  1660  		"Email":            cust.Email,
  1661  		"Name":             cust.Name,
  1662  		"AccountType":      cust.AccountType,
  1663  		"SubscriptionType": cust.SubscriptionType,
  1664  	}, nil
  1665  }
  1666  
  1667  // CleanUp empties the trash
  1668  func (f *Fs) CleanUp(ctx context.Context) error {
  1669  	opts := rest.Opts{
  1670  		Method: "POST",
  1671  		Path:   "files/v1/purge_trash",
  1672  	}
  1673  
  1674  	var info api.TrashResponse
  1675  	_, err := f.apiSrv.CallJSON(ctx, &opts, nil, &info)
  1676  	if err != nil {
  1677  		return fmt.Errorf("couldn't empty trash: %w", err)
  1678  	}
  1679  
  1680  	return nil
  1681  }
  1682  
  1683  // Shutdown shutdown the fs
  1684  func (f *Fs) Shutdown(ctx context.Context) error {
  1685  	f.tokenRenewer.Shutdown()
  1686  	return nil
  1687  }
  1688  
  1689  // Hashes returns the supported hash sets.
  1690  func (f *Fs) Hashes() hash.Set {
  1691  	return hash.Set(hash.MD5)
  1692  }
  1693  
  1694  // ---------------------------------------------
  1695  
  1696  // Fs returns the parent Fs
  1697  func (o *Object) Fs() fs.Info {
  1698  	return o.fs
  1699  }
  1700  
  1701  // Return a string version
  1702  func (o *Object) String() string {
  1703  	if o == nil {
  1704  		return "<nil>"
  1705  	}
  1706  	return o.remote
  1707  }
  1708  
  1709  // Remote returns the remote path
  1710  func (o *Object) Remote() string {
  1711  	return o.remote
  1712  }
  1713  
  1714  // filePath returns an escaped file path (f.root, remote)
  1715  func (o *Object) filePath() string {
  1716  	return o.fs.filePath(o.remote)
  1717  }
  1718  
  1719  // Hash returns the MD5 of an object returning a lowercase hex string
  1720  func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) {
  1721  	if t != hash.MD5 {
  1722  		return "", hash.ErrUnsupported
  1723  	}
  1724  	return o.md5, nil
  1725  }
  1726  
  1727  // Size returns the size of an object in bytes
  1728  func (o *Object) Size() int64 {
  1729  	ctx := context.TODO()
  1730  	err := o.readMetaData(ctx, false)
  1731  	if err != nil {
  1732  		fs.Logf(o, "Failed to read metadata: %v", err)
  1733  		return 0
  1734  	}
  1735  	return o.size
  1736  }
  1737  
  1738  // MimeType of an Object if known, "" otherwise
  1739  func (o *Object) MimeType(ctx context.Context) string {
  1740  	return o.mimeType
  1741  }
  1742  
  1743  // validFile checks if info indicates file is valid
  1744  func (f *Fs) validFile(info *api.JottaFile) bool {
  1745  	if info.State != "COMPLETED" {
  1746  		return false // File is incomplete or corrupt
  1747  	}
  1748  	if !info.Deleted {
  1749  		return !f.opt.TrashedOnly // Regular file; return false if TrashedOnly, else true
  1750  	}
  1751  	return f.opt.TrashedOnly // Deleted file; return true if TrashedOnly, else false
  1752  }
  1753  
  1754  // validFolder checks if info indicates folder is valid
  1755  func (f *Fs) validFolder(info *api.JottaFolder) bool {
  1756  	// Returns true if folder is not deleted.
  1757  	// If TrashedOnly option then always returns true, because a folder not
  1758  	// in trash must be traversed to get to files/subfolders that are.
  1759  	return !bool(info.Deleted) || f.opt.TrashedOnly
  1760  }
  1761  
  1762  // setMetaData sets the metadata from info
  1763  func (o *Object) setMetaData(info *api.JottaFile) (err error) {
  1764  	o.hasMetaData = true
  1765  	o.size = info.Size
  1766  	o.md5 = info.MD5
  1767  	o.mimeType = info.MimeType
  1768  	o.createTime = time.Time(info.CreatedAt)
  1769  	o.modTime = time.Time(info.ModifiedAt)
  1770  	o.updateTime = time.Time(info.UpdatedAt)
  1771  	return nil
  1772  }
  1773  
  1774  // readMetaData reads and updates the metadata for an object
  1775  func (o *Object) readMetaData(ctx context.Context, force bool) (err error) {
  1776  	if o.hasMetaData && !force {
  1777  		return nil
  1778  	}
  1779  	info, err := o.fs.readMetaDataForPath(ctx, o.remote)
  1780  	if err != nil {
  1781  		return err
  1782  	}
  1783  	if !o.fs.validFile(info) {
  1784  		return fs.ErrorObjectNotFound
  1785  	}
  1786  	return o.setMetaData(info)
  1787  }
  1788  
  1789  // ModTime returns the modification time of the object
  1790  //
  1791  // It attempts to read the objects mtime and if that isn't present the
  1792  // LastModified returned in the http headers
  1793  func (o *Object) ModTime(ctx context.Context) time.Time {
  1794  	err := o.readMetaData(ctx, false)
  1795  	if err != nil {
  1796  		fs.Logf(o, "Failed to read metadata: %v", err)
  1797  		return time.Now()
  1798  	}
  1799  	return o.modTime
  1800  }
  1801  
  1802  // SetModTime sets the modification time of the local fs object
  1803  func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
  1804  	// make sure metadata is available, we need its current size and md5
  1805  	err := o.readMetaData(ctx, false)
  1806  	if err != nil {
  1807  		fs.Logf(o, "Failed to read metadata: %v", err)
  1808  		return err
  1809  	}
  1810  
  1811  	// request check/update with existing metadata and new modtime
  1812  	// (note that if size/md5 does not match, the file content will
  1813  	// also be modified if deduplication is possible, i.e. it is
  1814  	// important to use correct/latest values)
  1815  	_, err = o.fs.createOrUpdate(ctx, o.remote, o.createTime, modTime, o.size, o.md5)
  1816  	if err != nil {
  1817  		if err == fs.ErrorObjectNotFound {
  1818  			// file was modified (size/md5 changed) between readMetaData and createOrUpdate?
  1819  			return errors.New("metadata did not match")
  1820  		}
  1821  		return err
  1822  	}
  1823  
  1824  	// update local metadata
  1825  	o.modTime = modTime
  1826  	return nil
  1827  }
  1828  
  1829  // Storable returns a boolean showing whether this object storable
  1830  func (o *Object) Storable() bool {
  1831  	return true
  1832  }
  1833  
  1834  // Open an object for read
  1835  func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
  1836  	fs.FixRangeOption(options, o.size)
  1837  	var resp *http.Response
  1838  	opts := rest.Opts{
  1839  		Method:     "GET",
  1840  		Path:       o.filePath(),
  1841  		Parameters: url.Values{},
  1842  		Options:    options,
  1843  	}
  1844  
  1845  	opts.Parameters.Set("mode", "bin")
  1846  
  1847  	err = o.fs.pacer.Call(func() (bool, error) {
  1848  		resp, err = o.fs.jfsSrv.Call(ctx, &opts)
  1849  		return shouldRetry(ctx, resp, err)
  1850  	})
  1851  	if err != nil {
  1852  		return nil, err
  1853  	}
  1854  	return resp.Body, err
  1855  }
  1856  
  1857  // Read the md5 of in returning a reader which will read the same contents
  1858  //
  1859  // The cleanup function should be called when out is finished with
  1860  // regardless of whether this function returned an error or not.
  1861  func readMD5(in io.Reader, size, threshold int64) (md5sum string, out io.Reader, cleanup func(), err error) {
  1862  	// we need an MD5
  1863  	md5Hasher := md5.New()
  1864  	// use the teeReader to write to the local file AND calculate the MD5 while doing so
  1865  	teeReader := io.TeeReader(in, md5Hasher)
  1866  
  1867  	// nothing to clean up by default
  1868  	cleanup = func() {}
  1869  
  1870  	// don't cache small files on disk to reduce wear of the disk
  1871  	if size > threshold {
  1872  		var tempFile *os.File
  1873  
  1874  		// create the cache file
  1875  		tempFile, err = os.CreateTemp("", cachePrefix)
  1876  		if err != nil {
  1877  			return
  1878  		}
  1879  
  1880  		_ = os.Remove(tempFile.Name()) // Delete the file - may not work on Windows
  1881  
  1882  		// clean up the file after we are done downloading
  1883  		cleanup = func() {
  1884  			// the file should normally already be close, but just to make sure
  1885  			_ = tempFile.Close()
  1886  			_ = os.Remove(tempFile.Name()) // delete the cache file after we are done - may be deleted already
  1887  		}
  1888  
  1889  		// copy the ENTIRE file to disc and calculate the MD5 in the process
  1890  		if _, err = io.Copy(tempFile, teeReader); err != nil {
  1891  			return
  1892  		}
  1893  		// jump to the start of the local file so we can pass it along
  1894  		if _, err = tempFile.Seek(0, 0); err != nil {
  1895  			return
  1896  		}
  1897  
  1898  		// replace the already read source with a reader of our cached file
  1899  		out = tempFile
  1900  	} else {
  1901  		// that's a small file, just read it into memory
  1902  		var inData []byte
  1903  		inData, err = io.ReadAll(teeReader)
  1904  		if err != nil {
  1905  			return
  1906  		}
  1907  
  1908  		// set the reader to our read memory block
  1909  		out = bytes.NewReader(inData)
  1910  	}
  1911  	return hex.EncodeToString(md5Hasher.Sum(nil)), out, cleanup, nil
  1912  }
  1913  
  1914  // Update the object with the contents of the io.Reader, modTime and size
  1915  //
  1916  // If existing is set then it updates the object rather than creating a new one.
  1917  //
  1918  // The new object may have been created if an error is returned
  1919  func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) {
  1920  	if o.fs.opt.NoVersions {
  1921  		err := o.readMetaData(ctx, false)
  1922  		if err == nil {
  1923  			// if the object exists delete it
  1924  			err = o.remove(ctx, true)
  1925  			if err != nil && err != fs.ErrorObjectNotFound {
  1926  				// if delete failed then report that, unless it was because the file did not exist after all
  1927  				return fmt.Errorf("failed to remove old object: %w", err)
  1928  			}
  1929  		} else if err != fs.ErrorObjectNotFound {
  1930  			// if the object does not exist we can just continue but if the error is something different we should report that
  1931  			return err
  1932  		}
  1933  	}
  1934  	o.fs.tokenRenewer.Start()
  1935  	defer o.fs.tokenRenewer.Stop()
  1936  	size := src.Size()
  1937  	md5String, err := src.Hash(ctx, hash.MD5)
  1938  	if err != nil || md5String == "" {
  1939  		// unwrap the accounting from the input, we use wrap to put it
  1940  		// back on after the buffering
  1941  		var wrap accounting.WrapFn
  1942  		in, wrap = accounting.UnWrap(in)
  1943  		var cleanup func()
  1944  		md5String, in, cleanup, err = readMD5(in, size, int64(o.fs.opt.MD5MemoryThreshold))
  1945  		defer cleanup()
  1946  		if err != nil {
  1947  			return fmt.Errorf("failed to calculate MD5: %w", err)
  1948  		}
  1949  		// Wrap the accounting back onto the stream
  1950  		in = wrap(in)
  1951  	}
  1952  	// Fetch metadata if --metadata is in use
  1953  	meta, err := fs.GetMetadataOptions(ctx, o.fs, src, options)
  1954  	if err != nil {
  1955  		return fmt.Errorf("failed to read metadata from source object: %w", err)
  1956  	}
  1957  	var createdTime string
  1958  	var modTime string
  1959  	if meta != nil {
  1960  		if v, ok := meta["btime"]; ok {
  1961  			t, err := time.Parse(time.RFC3339Nano, v) // metadata stores RFC3339Nano timestamps
  1962  			if err != nil {
  1963  				fs.Debugf(o, "failed to parse metadata btime: %q: %v", v, err)
  1964  			} else {
  1965  				createdTime = api.Rfc3339Time(t).String() // jottacloud api wants RFC3339 timestamps
  1966  			}
  1967  		}
  1968  		if v, ok := meta["mtime"]; ok {
  1969  			t, err := time.Parse(time.RFC3339Nano, v)
  1970  			if err != nil {
  1971  				fs.Debugf(o, "failed to parse metadata mtime: %q: %v", v, err)
  1972  			} else {
  1973  				modTime = api.Rfc3339Time(t).String()
  1974  			}
  1975  		}
  1976  	}
  1977  	if modTime == "" { // prefer mtime in meta as Modified time, fallback to source ModTime
  1978  		modTime = api.Rfc3339Time(src.ModTime(ctx)).String()
  1979  	}
  1980  	if createdTime == "" { // if no Created time set same as Modified
  1981  		createdTime = modTime
  1982  	}
  1983  
  1984  	// use the api to allocate the file first and get resume / deduplication info
  1985  	var resp *http.Response
  1986  	opts := rest.Opts{
  1987  		Method:       "POST",
  1988  		Path:         "files/v1/allocate",
  1989  		Options:      options,
  1990  		ExtraHeaders: make(map[string]string),
  1991  	}
  1992  
  1993  	// the allocate request
  1994  	var request = api.AllocateFileRequest{
  1995  		Bytes:    size,
  1996  		Created:  createdTime,
  1997  		Modified: modTime,
  1998  		Md5:      md5String,
  1999  		Path:     o.fs.allocatePathRaw(o.remote, true),
  2000  	}
  2001  
  2002  	// send it
  2003  	var response api.AllocateFileResponse
  2004  	err = o.fs.pacer.CallNoRetry(func() (bool, error) {
  2005  		resp, err = o.fs.apiSrv.CallJSON(ctx, &opts, &request, &response)
  2006  		return shouldRetry(ctx, resp, err)
  2007  	})
  2008  	if err != nil {
  2009  		return err
  2010  	}
  2011  
  2012  	// If the file state is INCOMPLETE and CORRUPT, we must upload it.
  2013  	// Else, if the file state is COMPLETE, we don't need to upload it because
  2014  	// the content is already there, possibly it was created with deduplication,
  2015  	// and also any metadata changes are already performed by the allocate request.
  2016  	if response.State != "COMPLETED" {
  2017  		// how much do we still have to upload?
  2018  		remainingBytes := size - response.ResumePos
  2019  		opts = rest.Opts{
  2020  			Method:        "POST",
  2021  			RootURL:       response.UploadURL,
  2022  			ContentLength: &remainingBytes,
  2023  			ContentType:   "application/octet-stream",
  2024  			Body:          in,
  2025  			ExtraHeaders:  make(map[string]string),
  2026  		}
  2027  		if response.ResumePos != 0 {
  2028  			opts.ExtraHeaders["Range"] = "bytes=" + strconv.FormatInt(response.ResumePos, 10) + "-" + strconv.FormatInt(size-1, 10)
  2029  		}
  2030  
  2031  		// copy the already uploaded bytes into the trash :)
  2032  		var result api.UploadResponse
  2033  		_, err = io.CopyN(io.Discard, in, response.ResumePos)
  2034  		if err != nil {
  2035  			return err
  2036  		}
  2037  
  2038  		// send the remaining bytes
  2039  		_, err = o.fs.apiSrv.CallJSON(ctx, &opts, nil, &result)
  2040  		if err != nil {
  2041  			return err
  2042  		}
  2043  
  2044  		// Upload response contains main metadata properties (size, md5 and modTime)
  2045  		// which could be set back to the object, but it does not contain the
  2046  		// necessary information to set the createTime and updateTime properties,
  2047  		// so must therefore perform a read instead.
  2048  	}
  2049  	// in any case we must update the object meta data
  2050  	return o.readMetaData(ctx, true)
  2051  }
  2052  
  2053  func (o *Object) remove(ctx context.Context, hard bool) error {
  2054  	opts := rest.Opts{
  2055  		Method:     "POST",
  2056  		Path:       o.filePath(),
  2057  		Parameters: url.Values{},
  2058  		NoResponse: true,
  2059  	}
  2060  
  2061  	if hard {
  2062  		opts.Parameters.Set("rm", "true")
  2063  	} else {
  2064  		opts.Parameters.Set("dl", "true")
  2065  	}
  2066  
  2067  	err := o.fs.pacer.Call(func() (bool, error) {
  2068  		resp, err := o.fs.jfsSrv.CallXML(ctx, &opts, nil, nil)
  2069  		return shouldRetry(ctx, resp, err)
  2070  	})
  2071  	if apiErr, ok := err.(*api.Error); ok {
  2072  		// attempting to hard delete will fail if path does not exist, but standard delete will succeed
  2073  		if apiErr.StatusCode == http.StatusNotFound {
  2074  			return fs.ErrorObjectNotFound
  2075  		}
  2076  	}
  2077  	return err
  2078  }
  2079  
  2080  // Remove an object
  2081  func (o *Object) Remove(ctx context.Context) error {
  2082  	return o.remove(ctx, o.fs.opt.HardDelete)
  2083  }
  2084  
  2085  // Metadata returns metadata for an object
  2086  //
  2087  // It should return nil if there is no Metadata
  2088  func (o *Object) Metadata(ctx context.Context) (metadata fs.Metadata, err error) {
  2089  	err = o.readMetaData(ctx, false)
  2090  	if err != nil {
  2091  		fs.Logf(o, "Failed to read metadata: %v", err)
  2092  		return nil, err
  2093  	}
  2094  	metadata.Set("btime", o.createTime.Format(time.RFC3339Nano)) // metadata timestamps should be RFC3339Nano
  2095  	metadata.Set("mtime", o.modTime.Format(time.RFC3339Nano))
  2096  	metadata.Set("utime", o.updateTime.Format(time.RFC3339Nano))
  2097  	metadata.Set("content-type", o.mimeType)
  2098  	return metadata, nil
  2099  }
  2100  
  2101  // Check the interfaces are satisfied
  2102  var (
  2103  	_ fs.Fs           = (*Fs)(nil)
  2104  	_ fs.Purger       = (*Fs)(nil)
  2105  	_ fs.Copier       = (*Fs)(nil)
  2106  	_ fs.Mover        = (*Fs)(nil)
  2107  	_ fs.DirMover     = (*Fs)(nil)
  2108  	_ fs.ListRer      = (*Fs)(nil)
  2109  	_ fs.PublicLinker = (*Fs)(nil)
  2110  	_ fs.Abouter      = (*Fs)(nil)
  2111  	_ fs.UserInfoer   = (*Fs)(nil)
  2112  	_ fs.CleanUpper   = (*Fs)(nil)
  2113  	_ fs.Shutdowner   = (*Fs)(nil)
  2114  	_ fs.Object       = (*Object)(nil)
  2115  	_ fs.MimeTyper    = (*Object)(nil)
  2116  	_ fs.Metadataer   = (*Object)(nil)
  2117  )