github.com/10XDev/rclone@v1.52.3-0.20200626220027-16af9ab76b2a/backend/jottacloud/jottacloud.go (about) 1 package jottacloud 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/md5" 7 "encoding/base64" 8 "encoding/hex" 9 "encoding/json" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "log" 14 "net/http" 15 "net/url" 16 "os" 17 "path" 18 "strconv" 19 "strings" 20 "time" 21 22 "github.com/pkg/errors" 23 "github.com/rclone/rclone/backend/jottacloud/api" 24 "github.com/rclone/rclone/fs" 25 "github.com/rclone/rclone/fs/accounting" 26 "github.com/rclone/rclone/fs/config" 27 "github.com/rclone/rclone/fs/config/configmap" 28 "github.com/rclone/rclone/fs/config/configstruct" 29 "github.com/rclone/rclone/fs/fserrors" 30 "github.com/rclone/rclone/fs/fshttp" 31 "github.com/rclone/rclone/fs/hash" 32 "github.com/rclone/rclone/fs/walk" 33 "github.com/rclone/rclone/lib/encoder" 34 "github.com/rclone/rclone/lib/oauthutil" 35 "github.com/rclone/rclone/lib/pacer" 36 "github.com/rclone/rclone/lib/rest" 37 "golang.org/x/oauth2" 38 ) 39 40 // Globals 41 const ( 42 minSleep = 10 * time.Millisecond 43 maxSleep = 2 * time.Second 44 decayConstant = 2 // bigger for slower decay, exponential 45 defaultDevice = "Jotta" 46 defaultMountpoint = "Archive" 47 rootURL = "https://www.jottacloud.com/jfs/" 48 apiURL = "https://api.jottacloud.com/" 49 baseURL = "https://www.jottacloud.com/" 50 defaultTokenURL = "https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/token" 51 cachePrefix = "rclone-jcmd5-" 52 configDevice = "device" 53 configMountpoint = "mountpoint" 54 configTokenURL = "tokenURL" 55 configVersion = 1 56 ) 57 58 var ( 59 // Description of how to auth for this app for a personal account 60 oauthConfig = &oauth2.Config{ 61 ClientID: "jottacli", 62 Endpoint: oauth2.Endpoint{ 63 AuthURL: defaultTokenURL, 64 TokenURL: defaultTokenURL, 65 }, 66 RedirectURL: oauthutil.RedirectLocalhostURL, 67 } 68 ) 69 70 // Register with Fs 71 func init() { 72 // needs to be done early so we can use oauth during config 73 fs.Register(&fs.RegInfo{ 74 Name: "jottacloud", 75 Description: "Jottacloud", 76 NewFs: NewFs, 77 Config: func(name string, m configmap.Mapper) { 78 ctx := context.TODO() 79 80 refresh := false 81 if version, ok := m.Get("configVersion"); ok { 82 ver, err := strconv.Atoi(version) 83 if err != nil { 84 log.Fatalf("Failed to parse config version - corrupted config") 85 } 86 refresh = ver != configVersion 87 } 88 89 if refresh { 90 fmt.Printf("Config outdated - refreshing\n") 91 } else { 92 tokenString, ok := m.Get("token") 93 if ok && tokenString != "" { 94 fmt.Printf("Already have a token - refresh?\n") 95 if !config.Confirm(false) { 96 return 97 } 98 } 99 } 100 101 clientConfig := *fs.Config 102 clientConfig.UserAgent = "JottaCli 0.6.18626 windows-amd64" 103 srv := rest.NewClient(fshttp.NewClient(&clientConfig)) 104 105 fmt.Printf("Generate a personal login token here: https://www.jottacloud.com/web/secure\n") 106 fmt.Printf("Login Token> ") 107 loginToken := config.ReadLine() 108 109 token, err := doAuth(ctx, srv, loginToken, m) 110 if err != nil { 111 log.Fatalf("Failed to get oauth token: %s", err) 112 } 113 err = oauthutil.PutToken(name, m, &token, true) 114 if err != nil { 115 log.Fatalf("Error while saving token: %s", err) 116 } 117 118 fmt.Printf("\nDo you want to use a non standard device/mountpoint e.g. for accessing files uploaded using the official Jottacloud client?\n\n") 119 if config.Confirm(false) { 120 oAuthClient, _, err := oauthutil.NewClient(name, m, oauthConfig) 121 if err != nil { 122 log.Fatalf("Failed to load oAuthClient: %s", err) 123 } 124 125 srv = rest.NewClient(oAuthClient).SetRoot(rootURL) 126 apiSrv := rest.NewClient(oAuthClient).SetRoot(apiURL) 127 128 device, mountpoint, err := setupMountpoint(ctx, srv, apiSrv) 129 if err != nil { 130 log.Fatalf("Failed to setup mountpoint: %s", err) 131 } 132 m.Set(configDevice, device) 133 m.Set(configMountpoint, mountpoint) 134 } 135 136 m.Set("configVersion", strconv.Itoa(configVersion)) 137 }, 138 Options: []fs.Option{{ 139 Name: "md5_memory_limit", 140 Help: "Files bigger than this will be cached on disk to calculate the MD5 if required.", 141 Default: fs.SizeSuffix(10 * 1024 * 1024), 142 Advanced: true, 143 }, { 144 Name: "trashed_only", 145 Help: "Only show files that are in the trash.\nThis will show trashed files in their original directory structure.", 146 Default: false, 147 Advanced: true, 148 }, { 149 Name: "hard_delete", 150 Help: "Delete files permanently rather than putting them into the trash.", 151 Default: false, 152 Advanced: true, 153 }, { 154 Name: "unlink", 155 Help: "Remove existing public link to file/folder with link command rather than creating.\nDefault is false, meaning link command will create or retrieve public link.", 156 Default: false, 157 Advanced: true, 158 }, { 159 Name: "upload_resume_limit", 160 Help: "Files bigger than this can be resumed if the upload fail's.", 161 Default: fs.SizeSuffix(10 * 1024 * 1024), 162 Advanced: true, 163 }, { 164 Name: config.ConfigEncoding, 165 Help: config.ConfigEncodingHelp, 166 Advanced: true, 167 // Encode invalid UTF-8 bytes as xml doesn't handle them properly. 168 // 169 // Also: '*', '/', ':', '<', '>', '?', '\"', '\x00', '|' 170 Default: (encoder.Display | 171 encoder.EncodeWin | // :?"*<>| 172 encoder.EncodeInvalidUtf8), 173 }}, 174 }) 175 } 176 177 // Options defines the configuration for this backend 178 type Options struct { 179 Device string `config:"device"` 180 Mountpoint string `config:"mountpoint"` 181 MD5MemoryThreshold fs.SizeSuffix `config:"md5_memory_limit"` 182 TrashedOnly bool `config:"trashed_only"` 183 HardDelete bool `config:"hard_delete"` 184 Unlink bool `config:"unlink"` 185 UploadThreshold fs.SizeSuffix `config:"upload_resume_limit"` 186 Enc encoder.MultiEncoder `config:"encoding"` 187 } 188 189 // Fs represents a remote jottacloud 190 type Fs struct { 191 name string 192 root string 193 user string 194 opt Options 195 features *fs.Features 196 endpointURL string 197 srv *rest.Client 198 apiSrv *rest.Client 199 pacer *fs.Pacer 200 tokenRenewer *oauthutil.Renew // renew the token on expiry 201 } 202 203 // Object describes a jottacloud object 204 // 205 // Will definitely have info but maybe not meta 206 type Object struct { 207 fs *Fs 208 remote string 209 hasMetaData bool 210 size int64 211 modTime time.Time 212 md5 string 213 mimeType string 214 } 215 216 // ------------------------------------------------------------ 217 218 // Name of the remote (as passed into NewFs) 219 func (f *Fs) Name() string { 220 return f.name 221 } 222 223 // Root of the remote (as passed into NewFs) 224 func (f *Fs) Root() string { 225 return f.root 226 } 227 228 // String converts this Fs to a string 229 func (f *Fs) String() string { 230 return fmt.Sprintf("jottacloud root '%s'", f.root) 231 } 232 233 // Features returns the optional features of this Fs 234 func (f *Fs) Features() *fs.Features { 235 return f.features 236 } 237 238 // parsePath parses a box 'url' 239 func parsePath(path string) (root string) { 240 root = strings.Trim(path, "/") 241 return 242 } 243 244 // retryErrorCodes is a slice of error codes that we will retry 245 var retryErrorCodes = []int{ 246 429, // Too Many Requests. 247 500, // Internal Server Error 248 502, // Bad Gateway 249 503, // Service Unavailable 250 504, // Gateway Timeout 251 509, // Bandwidth Limit Exceeded 252 } 253 254 // shouldRetry returns a boolean as to whether this resp and err 255 // deserve to be retried. It returns the err as a convenience 256 func shouldRetry(resp *http.Response, err error) (bool, error) { 257 return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err 258 } 259 260 // doAuth runs the actual token request 261 func doAuth(ctx context.Context, srv *rest.Client, loginTokenBase64 string, m configmap.Mapper) (token oauth2.Token, err error) { 262 loginTokenBytes, err := base64.RawURLEncoding.DecodeString(loginTokenBase64) 263 if err != nil { 264 return token, err 265 } 266 267 // decode login token 268 var loginToken api.LoginToken 269 decoder := json.NewDecoder(bytes.NewReader(loginTokenBytes)) 270 err = decoder.Decode(&loginToken) 271 if err != nil { 272 return token, err 273 } 274 275 // retrieve endpoint urls 276 opts := rest.Opts{ 277 Method: "GET", 278 RootURL: loginToken.WellKnownLink, 279 } 280 var wellKnown api.WellKnown 281 _, err = srv.CallJSON(ctx, &opts, nil, &wellKnown) 282 if err != nil { 283 return token, err 284 } 285 286 // save the tokenurl 287 oauthConfig.Endpoint.AuthURL = wellKnown.TokenEndpoint 288 oauthConfig.Endpoint.TokenURL = wellKnown.TokenEndpoint 289 m.Set(configTokenURL, wellKnown.TokenEndpoint) 290 291 // prepare out token request with username and password 292 values := url.Values{} 293 values.Set("client_id", "jottacli") 294 values.Set("grant_type", "password") 295 values.Set("password", loginToken.AuthToken) 296 values.Set("scope", "offline_access+openid") 297 values.Set("username", loginToken.Username) 298 values.Encode() 299 opts = rest.Opts{ 300 Method: "POST", 301 RootURL: oauthConfig.Endpoint.AuthURL, 302 ContentType: "application/x-www-form-urlencoded", 303 Body: strings.NewReader(values.Encode()), 304 } 305 306 // do the first request 307 var jsonToken api.TokenJSON 308 _, err = srv.CallJSON(ctx, &opts, nil, &jsonToken) 309 if err != nil { 310 return token, err 311 } 312 313 token.AccessToken = jsonToken.AccessToken 314 token.RefreshToken = jsonToken.RefreshToken 315 token.TokenType = jsonToken.TokenType 316 token.Expiry = time.Now().Add(time.Duration(jsonToken.ExpiresIn) * time.Second) 317 return token, err 318 } 319 320 // setupMountpoint sets up a custom device and mountpoint if desired by the user 321 func setupMountpoint(ctx context.Context, srv *rest.Client, apiSrv *rest.Client) (device, mountpoint string, err error) { 322 cust, err := getCustomerInfo(ctx, apiSrv) 323 if err != nil { 324 return "", "", err 325 } 326 327 acc, err := getDriveInfo(ctx, srv, cust.Username) 328 if err != nil { 329 return "", "", err 330 } 331 var deviceNames []string 332 for i := range acc.Devices { 333 deviceNames = append(deviceNames, acc.Devices[i].Name) 334 } 335 fmt.Printf("Please select the device to use. Normally this will be Jotta\n") 336 device = config.Choose("Devices", deviceNames, nil, false) 337 338 dev, err := getDeviceInfo(ctx, srv, path.Join(cust.Username, device)) 339 if err != nil { 340 return "", "", err 341 } 342 if len(dev.MountPoints) == 0 { 343 return "", "", errors.New("no mountpoints for selected device") 344 } 345 var mountpointNames []string 346 for i := range dev.MountPoints { 347 mountpointNames = append(mountpointNames, dev.MountPoints[i].Name) 348 } 349 fmt.Printf("Please select the mountpoint to user. Normally this will be Archive\n") 350 mountpoint = config.Choose("Mountpoints", mountpointNames, nil, false) 351 352 return device, mountpoint, err 353 } 354 355 // getCustomerInfo queries general information about the account 356 func getCustomerInfo(ctx context.Context, srv *rest.Client) (info *api.CustomerInfo, err error) { 357 opts := rest.Opts{ 358 Method: "GET", 359 Path: "account/v1/customer", 360 } 361 362 _, err = srv.CallJSON(ctx, &opts, nil, &info) 363 if err != nil { 364 return nil, errors.Wrap(err, "couldn't get customer info") 365 } 366 367 return info, nil 368 } 369 370 // getDriveInfo queries general information about the account and the available devices and mountpoints. 371 func getDriveInfo(ctx context.Context, srv *rest.Client, username string) (info *api.DriveInfo, err error) { 372 opts := rest.Opts{ 373 Method: "GET", 374 Path: username, 375 } 376 377 _, err = srv.CallXML(ctx, &opts, nil, &info) 378 if err != nil { 379 return nil, errors.Wrap(err, "couldn't get drive info") 380 } 381 382 return info, nil 383 } 384 385 // getDeviceInfo queries Information about a jottacloud device 386 func getDeviceInfo(ctx context.Context, srv *rest.Client, path string) (info *api.JottaDevice, err error) { 387 opts := rest.Opts{ 388 Method: "GET", 389 Path: urlPathEscape(path), 390 } 391 392 _, err = srv.CallXML(ctx, &opts, nil, &info) 393 if err != nil { 394 return nil, errors.Wrap(err, "couldn't get device info") 395 } 396 397 return info, nil 398 } 399 400 // setEndpointURL generates the API endpoint URL 401 func (f *Fs) setEndpointURL() { 402 if f.opt.Device == "" { 403 f.opt.Device = defaultDevice 404 } 405 if f.opt.Mountpoint == "" { 406 f.opt.Mountpoint = defaultMountpoint 407 } 408 f.endpointURL = urlPathEscape(path.Join(f.user, f.opt.Device, f.opt.Mountpoint)) 409 } 410 411 // readMetaDataForPath reads the metadata from the path 412 func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.JottaFile, err error) { 413 opts := rest.Opts{ 414 Method: "GET", 415 Path: f.filePath(path), 416 } 417 var result api.JottaFile 418 var resp *http.Response 419 err = f.pacer.Call(func() (bool, error) { 420 resp, err = f.srv.CallXML(ctx, &opts, nil, &result) 421 return shouldRetry(resp, err) 422 }) 423 424 if apiErr, ok := err.(*api.Error); ok { 425 // does not exist 426 if apiErr.StatusCode == http.StatusNotFound { 427 return nil, fs.ErrorObjectNotFound 428 } 429 } 430 431 if err != nil { 432 return nil, errors.Wrap(err, "read metadata failed") 433 } 434 if result.XMLName.Local != "file" { 435 return nil, fs.ErrorNotAFile 436 } 437 return &result, nil 438 } 439 440 // errorHandler parses a non 2xx error response into an error 441 func errorHandler(resp *http.Response) error { 442 // Decode error response 443 errResponse := new(api.Error) 444 err := rest.DecodeXML(resp, &errResponse) 445 if err != nil { 446 fs.Debugf(nil, "Couldn't decode error response: %v", err) 447 } 448 if errResponse.Message == "" { 449 errResponse.Message = resp.Status 450 } 451 if errResponse.StatusCode == 0 { 452 errResponse.StatusCode = resp.StatusCode 453 } 454 return errResponse 455 } 456 457 // Jottacloud wants '+' to be URL encoded even though the RFC states it's not reserved 458 func urlPathEscape(in string) string { 459 return strings.Replace(rest.URLPathEscape(in), "+", "%2B", -1) 460 } 461 462 // filePathRaw returns an unescaped file path (f.root, file) 463 func (f *Fs) filePathRaw(file string) string { 464 return path.Join(f.endpointURL, f.opt.Enc.FromStandardPath(path.Join(f.root, file))) 465 } 466 467 // filePath returns an escaped file path (f.root, file) 468 func (f *Fs) filePath(file string) string { 469 return urlPathEscape(f.filePathRaw(file)) 470 } 471 472 // NewFs constructs an Fs from the path, container:path 473 func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) { 474 ctx := context.TODO() 475 // Parse config into Options struct 476 opt := new(Options) 477 err := configstruct.Set(m, opt) 478 if err != nil { 479 return nil, err 480 } 481 482 // Check config version 483 var ok bool 484 var version string 485 if version, ok = m.Get("configVersion"); ok { 486 ver, err := strconv.Atoi(version) 487 if err != nil { 488 return nil, errors.New("Failed to parse config version") 489 } 490 ok = ver == configVersion 491 } 492 if !ok { 493 return nil, errors.New("Outdated config - please reconfigure this backend") 494 } 495 496 // if custom endpoints are set use them else stick with defaults 497 if tokenURL, ok := m.Get(configTokenURL); ok { 498 oauthConfig.Endpoint.TokenURL = tokenURL 499 // jottacloud is weird. we need to use the tokenURL as authURL 500 oauthConfig.Endpoint.AuthURL = tokenURL 501 } 502 503 // Create OAuth Client 504 baseClient := fshttp.NewClient(fs.Config) 505 oAuthClient, ts, err := oauthutil.NewClientWithBaseClient(name, m, oauthConfig, baseClient) 506 if err != nil { 507 return nil, errors.Wrap(err, "Failed to configure Jottacloud oauth client") 508 } 509 510 rootIsDir := strings.HasSuffix(root, "/") 511 root = parsePath(root) 512 513 f := &Fs{ 514 name: name, 515 root: root, 516 opt: *opt, 517 srv: rest.NewClient(oAuthClient).SetRoot(rootURL), 518 apiSrv: rest.NewClient(oAuthClient).SetRoot(apiURL), 519 pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))), 520 } 521 f.features = (&fs.Features{ 522 CaseInsensitive: true, 523 CanHaveEmptyDirectories: true, 524 ReadMimeType: true, 525 WriteMimeType: true, 526 }).Fill(f) 527 f.srv.SetErrorHandler(errorHandler) 528 if opt.TrashedOnly { // we cannot support showing Trashed Files when using ListR right now 529 f.features.ListR = nil 530 } 531 532 // Renew the token in the background 533 f.tokenRenewer = oauthutil.NewRenew(f.String(), ts, func() error { 534 _, err := f.readMetaDataForPath(ctx, "") 535 return err 536 }) 537 538 cust, err := getCustomerInfo(ctx, f.apiSrv) 539 if err != nil { 540 return nil, err 541 } 542 f.user = cust.Username 543 f.setEndpointURL() 544 545 if root != "" && !rootIsDir { 546 // Check to see if the root actually an existing file 547 remote := path.Base(root) 548 f.root = path.Dir(root) 549 if f.root == "." { 550 f.root = "" 551 } 552 _, err := f.NewObject(context.TODO(), remote) 553 if err != nil { 554 if errors.Cause(err) == fs.ErrorObjectNotFound || errors.Cause(err) == fs.ErrorNotAFile { 555 // File doesn't exist so return old f 556 f.root = root 557 return f, nil 558 } 559 return nil, err 560 } 561 // return an error with an fs which points to the parent 562 return f, fs.ErrorIsFile 563 } 564 return f, nil 565 } 566 567 // Return an Object from a path 568 // 569 // If it can't be found it returns the error fs.ErrorObjectNotFound. 570 func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.JottaFile) (fs.Object, error) { 571 o := &Object{ 572 fs: f, 573 remote: remote, 574 } 575 var err error 576 if info != nil { 577 // Set info 578 err = o.setMetaData(info) 579 } else { 580 err = o.readMetaData(ctx, false) // reads info and meta, returning an error 581 } 582 if err != nil { 583 return nil, err 584 } 585 return o, nil 586 } 587 588 // NewObject finds the Object at remote. If it can't be found 589 // it returns the error fs.ErrorObjectNotFound. 590 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 591 return f.newObjectWithInfo(ctx, remote, nil) 592 } 593 594 // CreateDir makes a directory 595 func (f *Fs) CreateDir(ctx context.Context, path string) (jf *api.JottaFolder, err error) { 596 // fs.Debugf(f, "CreateDir(%q, %q)\n", pathID, leaf) 597 var resp *http.Response 598 opts := rest.Opts{ 599 Method: "POST", 600 Path: f.filePath(path), 601 Parameters: url.Values{}, 602 } 603 604 opts.Parameters.Set("mkDir", "true") 605 606 err = f.pacer.Call(func() (bool, error) { 607 resp, err = f.srv.CallXML(ctx, &opts, nil, &jf) 608 return shouldRetry(resp, err) 609 }) 610 if err != nil { 611 //fmt.Printf("...Error %v\n", err) 612 return nil, err 613 } 614 // fmt.Printf("...Id %q\n", *info.Id) 615 return jf, nil 616 } 617 618 // List the objects and directories in dir into entries. The 619 // entries can be returned in any order but should be for a 620 // complete directory. 621 // 622 // dir should be "" to list the root, and should not have 623 // trailing slashes. 624 // 625 // This should return ErrDirNotFound if the directory isn't 626 // found. 627 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 628 opts := rest.Opts{ 629 Method: "GET", 630 Path: f.filePath(dir), 631 } 632 633 var resp *http.Response 634 var result api.JottaFolder 635 err = f.pacer.Call(func() (bool, error) { 636 resp, err = f.srv.CallXML(ctx, &opts, nil, &result) 637 return shouldRetry(resp, err) 638 }) 639 640 if err != nil { 641 if apiErr, ok := err.(*api.Error); ok { 642 // does not exist 643 if apiErr.StatusCode == http.StatusNotFound { 644 return nil, fs.ErrorDirNotFound 645 } 646 } 647 return nil, errors.Wrap(err, "couldn't list files") 648 } 649 650 if bool(result.Deleted) && !f.opt.TrashedOnly { 651 return nil, fs.ErrorDirNotFound 652 } 653 654 for i := range result.Folders { 655 item := &result.Folders[i] 656 if !f.opt.TrashedOnly && bool(item.Deleted) { 657 continue 658 } 659 remote := path.Join(dir, f.opt.Enc.ToStandardName(item.Name)) 660 d := fs.NewDir(remote, time.Time(item.ModifiedAt)) 661 entries = append(entries, d) 662 } 663 664 for i := range result.Files { 665 item := &result.Files[i] 666 if f.opt.TrashedOnly { 667 if !item.Deleted || item.State != "COMPLETED" { 668 continue 669 } 670 } else { 671 if item.Deleted || item.State != "COMPLETED" { 672 continue 673 } 674 } 675 remote := path.Join(dir, f.opt.Enc.ToStandardName(item.Name)) 676 o, err := f.newObjectWithInfo(ctx, remote, item) 677 if err != nil { 678 continue 679 } 680 entries = append(entries, o) 681 } 682 return entries, nil 683 } 684 685 // listFileDirFn is called from listFileDir to handle an object. 686 type listFileDirFn func(fs.DirEntry) error 687 688 // List the objects and directories into entries, from a 689 // special kind of JottaFolder representing a FileDirLis 690 func (f *Fs) listFileDir(ctx context.Context, remoteStartPath string, startFolder *api.JottaFolder, fn listFileDirFn) error { 691 pathPrefix := "/" + f.filePathRaw("") // Non-escaped prefix of API paths to be cut off, to be left with the remote path including the remoteStartPath 692 pathPrefixLength := len(pathPrefix) 693 startPath := path.Join(pathPrefix, remoteStartPath) // Non-escaped API path up to and including remoteStartPath, to decide if it should be created as a new dir object 694 startPathLength := len(startPath) 695 for i := range startFolder.Folders { 696 folder := &startFolder.Folders[i] 697 if folder.Deleted { 698 return nil 699 } 700 folderPath := f.opt.Enc.ToStandardPath(path.Join(folder.Path, folder.Name)) 701 folderPathLength := len(folderPath) 702 var remoteDir string 703 if folderPathLength > pathPrefixLength { 704 remoteDir = folderPath[pathPrefixLength+1:] 705 if folderPathLength > startPathLength { 706 d := fs.NewDir(remoteDir, time.Time(folder.ModifiedAt)) 707 err := fn(d) 708 if err != nil { 709 return err 710 } 711 } 712 } 713 for i := range folder.Files { 714 file := &folder.Files[i] 715 if file.Deleted || file.State != "COMPLETED" { 716 continue 717 } 718 remoteFile := path.Join(remoteDir, f.opt.Enc.ToStandardName(file.Name)) 719 o, err := f.newObjectWithInfo(ctx, remoteFile, file) 720 if err != nil { 721 return err 722 } 723 err = fn(o) 724 if err != nil { 725 return err 726 } 727 } 728 } 729 return nil 730 } 731 732 // ListR lists the objects and directories of the Fs starting 733 // from dir recursively into out. 734 // 735 // dir should be "" to start from the root, and should not 736 // have trailing slashes. 737 func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (err error) { 738 opts := rest.Opts{ 739 Method: "GET", 740 Path: f.filePath(dir), 741 Parameters: url.Values{}, 742 } 743 opts.Parameters.Set("mode", "list") 744 745 var resp *http.Response 746 var result api.JottaFolder // Could be JottaFileDirList, but JottaFolder is close enough 747 err = f.pacer.Call(func() (bool, error) { 748 resp, err = f.srv.CallXML(ctx, &opts, nil, &result) 749 return shouldRetry(resp, err) 750 }) 751 if err != nil { 752 if apiErr, ok := err.(*api.Error); ok { 753 // does not exist 754 if apiErr.StatusCode == http.StatusNotFound { 755 return fs.ErrorDirNotFound 756 } 757 } 758 return errors.Wrap(err, "couldn't list files") 759 } 760 list := walk.NewListRHelper(callback) 761 err = f.listFileDir(ctx, dir, &result, func(entry fs.DirEntry) error { 762 return list.Add(entry) 763 }) 764 if err != nil { 765 return err 766 } 767 return list.Flush() 768 } 769 770 // Creates from the parameters passed in a half finished Object which 771 // must have setMetaData called on it 772 // 773 // Used to create new objects 774 func (f *Fs) createObject(remote string, modTime time.Time, size int64) (o *Object) { 775 // Temporary Object under construction 776 o = &Object{ 777 fs: f, 778 remote: remote, 779 size: size, 780 modTime: modTime, 781 } 782 return o 783 } 784 785 // Put the object 786 // 787 // Copy the reader in to the new object which is returned 788 // 789 // The new object may have been created if an error is returned 790 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 791 if f.opt.Device != "Jotta" { 792 return nil, errors.New("upload not supported for devices other than Jotta") 793 } 794 o := f.createObject(src.Remote(), src.ModTime(ctx), src.Size()) 795 return o, o.Update(ctx, in, src, options...) 796 } 797 798 // mkParentDir makes the parent of the native path dirPath if 799 // necessary and any directories above that 800 func (f *Fs) mkParentDir(ctx context.Context, dirPath string) error { 801 // defer log.Trace(dirPath, "")("") 802 // chop off trailing / if it exists 803 if strings.HasSuffix(dirPath, "/") { 804 dirPath = dirPath[:len(dirPath)-1] 805 } 806 parent := path.Dir(dirPath) 807 if parent == "." { 808 parent = "" 809 } 810 return f.Mkdir(ctx, parent) 811 } 812 813 // Mkdir creates the container if it doesn't exist 814 func (f *Fs) Mkdir(ctx context.Context, dir string) error { 815 _, err := f.CreateDir(ctx, dir) 816 return err 817 } 818 819 // purgeCheck removes the root directory, if check is set then it 820 // refuses to do so if it has anything in 821 func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) (err error) { 822 root := path.Join(f.root, dir) 823 if root == "" { 824 return errors.New("can't purge root directory") 825 } 826 827 // check that the directory exists 828 entries, err := f.List(ctx, dir) 829 if err != nil { 830 return err 831 } 832 833 if check { 834 if len(entries) != 0 { 835 return fs.ErrorDirectoryNotEmpty 836 } 837 } 838 839 opts := rest.Opts{ 840 Method: "POST", 841 Path: f.filePath(dir), 842 Parameters: url.Values{}, 843 NoResponse: true, 844 } 845 846 if f.opt.HardDelete { 847 opts.Parameters.Set("rmDir", "true") 848 } else { 849 opts.Parameters.Set("dlDir", "true") 850 } 851 852 var resp *http.Response 853 err = f.pacer.Call(func() (bool, error) { 854 resp, err = f.srv.Call(ctx, &opts) 855 return shouldRetry(resp, err) 856 }) 857 if err != nil { 858 return errors.Wrap(err, "couldn't purge directory") 859 } 860 861 return nil 862 } 863 864 // Rmdir deletes the root folder 865 // 866 // Returns an error if it isn't empty 867 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 868 return f.purgeCheck(ctx, dir, true) 869 } 870 871 // Precision return the precision of this Fs 872 func (f *Fs) Precision() time.Duration { 873 return time.Second 874 } 875 876 // Purge deletes all the files and the container 877 func (f *Fs) Purge(ctx context.Context) error { 878 return f.purgeCheck(ctx, "", false) 879 } 880 881 // copyOrMoves copies or moves directories or files depending on the method parameter 882 func (f *Fs) copyOrMove(ctx context.Context, method, src, dest string) (info *api.JottaFile, err error) { 883 opts := rest.Opts{ 884 Method: "POST", 885 Path: src, 886 Parameters: url.Values{}, 887 } 888 889 opts.Parameters.Set(method, "/"+path.Join(f.endpointURL, f.opt.Enc.FromStandardPath(path.Join(f.root, dest)))) 890 891 var resp *http.Response 892 err = f.pacer.Call(func() (bool, error) { 893 resp, err = f.srv.CallXML(ctx, &opts, nil, &info) 894 return shouldRetry(resp, err) 895 }) 896 if err != nil { 897 return nil, err 898 } 899 return info, nil 900 } 901 902 // Copy src to this remote using server side copy operations. 903 // 904 // This is stored with the remote path given 905 // 906 // It returns the destination Object and a possible error 907 // 908 // Will only be called if src.Fs().Name() == f.Name() 909 // 910 // If it isn't possible then return fs.ErrorCantCopy 911 func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 912 srcObj, ok := src.(*Object) 913 if !ok { 914 fs.Debugf(src, "Can't copy - not same remote type") 915 return nil, fs.ErrorCantMove 916 } 917 918 err := f.mkParentDir(ctx, remote) 919 if err != nil { 920 return nil, err 921 } 922 info, err := f.copyOrMove(ctx, "cp", srcObj.filePath(), remote) 923 924 if err != nil { 925 return nil, errors.Wrap(err, "couldn't copy file") 926 } 927 928 return f.newObjectWithInfo(ctx, remote, info) 929 //return f.newObjectWithInfo(remote, &result) 930 } 931 932 // Move src to this remote using server side move operations. 933 // 934 // This is stored with the remote path given 935 // 936 // It returns the destination Object and a possible error 937 // 938 // Will only be called if src.Fs().Name() == f.Name() 939 // 940 // If it isn't possible then return fs.ErrorCantMove 941 func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 942 srcObj, ok := src.(*Object) 943 if !ok { 944 fs.Debugf(src, "Can't move - not same remote type") 945 return nil, fs.ErrorCantMove 946 } 947 948 err := f.mkParentDir(ctx, remote) 949 if err != nil { 950 return nil, err 951 } 952 info, err := f.copyOrMove(ctx, "mv", srcObj.filePath(), remote) 953 954 if err != nil { 955 return nil, errors.Wrap(err, "couldn't move file") 956 } 957 958 return f.newObjectWithInfo(ctx, remote, info) 959 //return f.newObjectWithInfo(remote, result) 960 } 961 962 // DirMove moves src, srcRemote to this remote at dstRemote 963 // using server side move operations. 964 // 965 // Will only be called if src.Fs().Name() == f.Name() 966 // 967 // If it isn't possible then return fs.ErrorCantDirMove 968 // 969 // If destination exists then return fs.ErrorDirExists 970 func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error { 971 srcFs, ok := src.(*Fs) 972 if !ok { 973 fs.Debugf(srcFs, "Can't move directory - not same remote type") 974 return fs.ErrorCantDirMove 975 } 976 srcPath := path.Join(srcFs.root, srcRemote) 977 dstPath := path.Join(f.root, dstRemote) 978 979 // Refuse to move to or from the root 980 if srcPath == "" || dstPath == "" { 981 fs.Debugf(src, "DirMove error: Can't move root") 982 return errors.New("can't move root directory") 983 } 984 //fmt.Printf("Move src: %s (FullPath %s), dst: %s (FullPath: %s)\n", srcRemote, srcPath, dstRemote, dstPath) 985 986 var err error 987 _, err = f.List(ctx, dstRemote) 988 if err == fs.ErrorDirNotFound { 989 // OK 990 } else if err != nil { 991 return err 992 } else { 993 return fs.ErrorDirExists 994 } 995 996 _, err = f.copyOrMove(ctx, "mvDir", path.Join(f.endpointURL, f.opt.Enc.FromStandardPath(srcPath))+"/", dstRemote) 997 998 if err != nil { 999 return errors.Wrap(err, "couldn't move directory") 1000 } 1001 return nil 1002 } 1003 1004 // PublicLink generates a public link to the remote path (usually readable by anyone) 1005 func (f *Fs) PublicLink(ctx context.Context, remote string) (link string, err error) { 1006 opts := rest.Opts{ 1007 Method: "GET", 1008 Path: f.filePath(remote), 1009 Parameters: url.Values{}, 1010 } 1011 1012 if f.opt.Unlink { 1013 opts.Parameters.Set("mode", "disableShare") 1014 } else { 1015 opts.Parameters.Set("mode", "enableShare") 1016 } 1017 1018 var resp *http.Response 1019 var result api.JottaFile 1020 err = f.pacer.Call(func() (bool, error) { 1021 resp, err = f.srv.CallXML(ctx, &opts, nil, &result) 1022 return shouldRetry(resp, err) 1023 }) 1024 1025 if apiErr, ok := err.(*api.Error); ok { 1026 // does not exist 1027 if apiErr.StatusCode == http.StatusNotFound { 1028 return "", fs.ErrorObjectNotFound 1029 } 1030 } 1031 if err != nil { 1032 if f.opt.Unlink { 1033 return "", errors.Wrap(err, "couldn't remove public link") 1034 } 1035 return "", errors.Wrap(err, "couldn't create public link") 1036 } 1037 if f.opt.Unlink { 1038 if result.PublicSharePath != "" { 1039 return "", errors.Errorf("couldn't remove public link - %q", result.PublicSharePath) 1040 } 1041 return "", nil 1042 } 1043 if result.PublicSharePath == "" { 1044 return "", errors.New("couldn't create public link - no link path received") 1045 } 1046 link = path.Join(baseURL, result.PublicSharePath) 1047 return link, nil 1048 } 1049 1050 // About gets quota information 1051 func (f *Fs) About(ctx context.Context) (*fs.Usage, error) { 1052 info, err := getDriveInfo(ctx, f.srv, f.user) 1053 if err != nil { 1054 return nil, err 1055 } 1056 1057 usage := &fs.Usage{ 1058 Used: fs.NewUsageValue(info.Usage), 1059 } 1060 if info.Capacity > 0 { 1061 usage.Total = fs.NewUsageValue(info.Capacity) 1062 usage.Free = fs.NewUsageValue(info.Capacity - info.Usage) 1063 } 1064 return usage, nil 1065 } 1066 1067 // CleanUp empties the trash 1068 func (f *Fs) CleanUp(ctx context.Context) error { 1069 opts := rest.Opts{ 1070 Method: "POST", 1071 Path: "files/v1/purge_trash", 1072 } 1073 1074 var info api.TrashResponse 1075 _, err := f.apiSrv.CallJSON(ctx, &opts, nil, &info) 1076 if err != nil { 1077 return errors.Wrap(err, "couldn't empty trash") 1078 } 1079 1080 return nil 1081 } 1082 1083 // Hashes returns the supported hash sets. 1084 func (f *Fs) Hashes() hash.Set { 1085 return hash.Set(hash.MD5) 1086 } 1087 1088 // --------------------------------------------- 1089 1090 // Fs returns the parent Fs 1091 func (o *Object) Fs() fs.Info { 1092 return o.fs 1093 } 1094 1095 // Return a string version 1096 func (o *Object) String() string { 1097 if o == nil { 1098 return "<nil>" 1099 } 1100 return o.remote 1101 } 1102 1103 // Remote returns the remote path 1104 func (o *Object) Remote() string { 1105 return o.remote 1106 } 1107 1108 // filePath returns an escaped file path (f.root, remote) 1109 func (o *Object) filePath() string { 1110 return o.fs.filePath(o.remote) 1111 } 1112 1113 // Hash returns the MD5 of an object returning a lowercase hex string 1114 func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) { 1115 if t != hash.MD5 { 1116 return "", hash.ErrUnsupported 1117 } 1118 return o.md5, nil 1119 } 1120 1121 // Size returns the size of an object in bytes 1122 func (o *Object) Size() int64 { 1123 ctx := context.TODO() 1124 err := o.readMetaData(ctx, false) 1125 if err != nil { 1126 fs.Logf(o, "Failed to read metadata: %v", err) 1127 return 0 1128 } 1129 return o.size 1130 } 1131 1132 // MimeType of an Object if known, "" otherwise 1133 func (o *Object) MimeType(ctx context.Context) string { 1134 return o.mimeType 1135 } 1136 1137 // setMetaData sets the metadata from info 1138 func (o *Object) setMetaData(info *api.JottaFile) (err error) { 1139 o.hasMetaData = true 1140 o.size = info.Size 1141 o.md5 = info.MD5 1142 o.mimeType = info.MimeType 1143 o.modTime = time.Time(info.ModifiedAt) 1144 return nil 1145 } 1146 1147 // readMetaData reads and updates the metadata for an object 1148 func (o *Object) readMetaData(ctx context.Context, force bool) (err error) { 1149 if o.hasMetaData && !force { 1150 return nil 1151 } 1152 info, err := o.fs.readMetaDataForPath(ctx, o.remote) 1153 if err != nil { 1154 return err 1155 } 1156 if bool(info.Deleted) && !o.fs.opt.TrashedOnly { 1157 return fs.ErrorObjectNotFound 1158 } 1159 return o.setMetaData(info) 1160 } 1161 1162 // ModTime returns the modification time of the object 1163 // 1164 // It attempts to read the objects mtime and if that isn't present the 1165 // LastModified returned in the http headers 1166 func (o *Object) ModTime(ctx context.Context) time.Time { 1167 err := o.readMetaData(ctx, false) 1168 if err != nil { 1169 fs.Logf(o, "Failed to read metadata: %v", err) 1170 return time.Now() 1171 } 1172 return o.modTime 1173 } 1174 1175 // SetModTime sets the modification time of the local fs object 1176 func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error { 1177 return fs.ErrorCantSetModTime 1178 } 1179 1180 // Storable returns a boolean showing whether this object storable 1181 func (o *Object) Storable() bool { 1182 return true 1183 } 1184 1185 // Open an object for read 1186 func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) { 1187 fs.FixRangeOption(options, o.size) 1188 var resp *http.Response 1189 opts := rest.Opts{ 1190 Method: "GET", 1191 Path: o.filePath(), 1192 Parameters: url.Values{}, 1193 Options: options, 1194 } 1195 1196 opts.Parameters.Set("mode", "bin") 1197 1198 err = o.fs.pacer.Call(func() (bool, error) { 1199 resp, err = o.fs.srv.Call(ctx, &opts) 1200 return shouldRetry(resp, err) 1201 }) 1202 if err != nil { 1203 return nil, err 1204 } 1205 return resp.Body, err 1206 } 1207 1208 // Read the md5 of in returning a reader which will read the same contents 1209 // 1210 // The cleanup function should be called when out is finished with 1211 // regardless of whether this function returned an error or not. 1212 func readMD5(in io.Reader, size, threshold int64) (md5sum string, out io.Reader, cleanup func(), err error) { 1213 // we need an MD5 1214 md5Hasher := md5.New() 1215 // use the teeReader to write to the local file AND calculate the MD5 while doing so 1216 teeReader := io.TeeReader(in, md5Hasher) 1217 1218 // nothing to clean up by default 1219 cleanup = func() {} 1220 1221 // don't cache small files on disk to reduce wear of the disk 1222 if size > threshold { 1223 var tempFile *os.File 1224 1225 // create the cache file 1226 tempFile, err = ioutil.TempFile("", cachePrefix) 1227 if err != nil { 1228 return 1229 } 1230 1231 _ = os.Remove(tempFile.Name()) // Delete the file - may not work on Windows 1232 1233 // clean up the file after we are done downloading 1234 cleanup = func() { 1235 // the file should normally already be close, but just to make sure 1236 _ = tempFile.Close() 1237 _ = os.Remove(tempFile.Name()) // delete the cache file after we are done - may be deleted already 1238 } 1239 1240 // copy the ENTIRE file to disc and calculate the MD5 in the process 1241 if _, err = io.Copy(tempFile, teeReader); err != nil { 1242 return 1243 } 1244 // jump to the start of the local file so we can pass it along 1245 if _, err = tempFile.Seek(0, 0); err != nil { 1246 return 1247 } 1248 1249 // replace the already read source with a reader of our cached file 1250 out = tempFile 1251 } else { 1252 // that's a small file, just read it into memory 1253 var inData []byte 1254 inData, err = ioutil.ReadAll(teeReader) 1255 if err != nil { 1256 return 1257 } 1258 1259 // set the reader to our read memory block 1260 out = bytes.NewReader(inData) 1261 } 1262 return hex.EncodeToString(md5Hasher.Sum(nil)), out, cleanup, nil 1263 } 1264 1265 // Update the object with the contents of the io.Reader, modTime and size 1266 // 1267 // If existing is set then it updates the object rather than creating a new one 1268 // 1269 // The new object may have been created if an error is returned 1270 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) { 1271 size := src.Size() 1272 md5String, err := src.Hash(ctx, hash.MD5) 1273 if err != nil || md5String == "" { 1274 // unwrap the accounting from the input, we use wrap to put it 1275 // back on after the buffering 1276 var wrap accounting.WrapFn 1277 in, wrap = accounting.UnWrap(in) 1278 var cleanup func() 1279 md5String, in, cleanup, err = readMD5(in, size, int64(o.fs.opt.MD5MemoryThreshold)) 1280 defer cleanup() 1281 if err != nil { 1282 return errors.Wrap(err, "failed to calculate MD5") 1283 } 1284 // Wrap the accounting back onto the stream 1285 in = wrap(in) 1286 } 1287 1288 // use the api to allocate the file first and get resume / deduplication info 1289 var resp *http.Response 1290 opts := rest.Opts{ 1291 Method: "POST", 1292 Path: "files/v1/allocate", 1293 Options: options, 1294 ExtraHeaders: make(map[string]string), 1295 } 1296 fileDate := api.Time(src.ModTime(ctx)).APIString() 1297 1298 // the allocate request 1299 var request = api.AllocateFileRequest{ 1300 Bytes: size, 1301 Created: fileDate, 1302 Modified: fileDate, 1303 Md5: md5String, 1304 Path: path.Join(o.fs.opt.Mountpoint, o.fs.opt.Enc.FromStandardPath(path.Join(o.fs.root, o.remote))), 1305 } 1306 1307 // send it 1308 var response api.AllocateFileResponse 1309 err = o.fs.pacer.CallNoRetry(func() (bool, error) { 1310 resp, err = o.fs.apiSrv.CallJSON(ctx, &opts, &request, &response) 1311 return shouldRetry(resp, err) 1312 }) 1313 if err != nil { 1314 return err 1315 } 1316 1317 // If the file state is INCOMPLETE and CORRPUT, try to upload a then 1318 if response.State != "COMPLETED" { 1319 // how much do we still have to upload? 1320 remainingBytes := size - response.ResumePos 1321 opts = rest.Opts{ 1322 Method: "POST", 1323 RootURL: response.UploadURL, 1324 ContentLength: &remainingBytes, 1325 ContentType: "application/octet-stream", 1326 Body: in, 1327 ExtraHeaders: make(map[string]string), 1328 } 1329 if response.ResumePos != 0 { 1330 opts.ExtraHeaders["Range"] = "bytes=" + strconv.FormatInt(response.ResumePos, 10) + "-" + strconv.FormatInt(size-1, 10) 1331 } 1332 1333 // copy the already uploaded bytes into the trash :) 1334 var result api.UploadResponse 1335 _, err = io.CopyN(ioutil.Discard, in, response.ResumePos) 1336 if err != nil { 1337 return err 1338 } 1339 1340 // send the remaining bytes 1341 resp, err = o.fs.apiSrv.CallJSON(ctx, &opts, nil, &result) 1342 if err != nil { 1343 return err 1344 } 1345 1346 // finally update the meta data 1347 o.hasMetaData = true 1348 o.size = result.Bytes 1349 o.md5 = result.Md5 1350 o.modTime = time.Unix(result.Modified/1000, 0) 1351 } else { 1352 // If the file state is COMPLETE we don't need to upload it because the file was already found but we still ned to update our metadata 1353 return o.readMetaData(ctx, true) 1354 } 1355 1356 return nil 1357 } 1358 1359 // Remove an object 1360 func (o *Object) Remove(ctx context.Context) error { 1361 opts := rest.Opts{ 1362 Method: "POST", 1363 Path: o.filePath(), 1364 Parameters: url.Values{}, 1365 NoResponse: true, 1366 } 1367 1368 if o.fs.opt.HardDelete { 1369 opts.Parameters.Set("rm", "true") 1370 } else { 1371 opts.Parameters.Set("dl", "true") 1372 } 1373 1374 return o.fs.pacer.Call(func() (bool, error) { 1375 resp, err := o.fs.srv.CallXML(ctx, &opts, nil, nil) 1376 return shouldRetry(resp, err) 1377 }) 1378 } 1379 1380 // Check the interfaces are satisfied 1381 var ( 1382 _ fs.Fs = (*Fs)(nil) 1383 _ fs.Purger = (*Fs)(nil) 1384 _ fs.Copier = (*Fs)(nil) 1385 _ fs.Mover = (*Fs)(nil) 1386 _ fs.DirMover = (*Fs)(nil) 1387 _ fs.ListRer = (*Fs)(nil) 1388 _ fs.PublicLinker = (*Fs)(nil) 1389 _ fs.Abouter = (*Fs)(nil) 1390 _ fs.CleanUpper = (*Fs)(nil) 1391 _ fs.Object = (*Object)(nil) 1392 _ fs.MimeTyper = (*Object)(nil) 1393 )