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