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