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 )