github.com/10XDev/rclone@v1.52.3-0.20200626220027-16af9ab76b2a/backend/sugarsync/sugarsync.go (about) 1 // Package sugarsync provides an interface to the Sugarsync 2 // object storage system. 3 package sugarsync 4 5 /* FIXME 6 7 DirMove tests fails with: Can not move sync folder. 8 9 go test -v -short -run TestIntegration/FsMkdir/FsPutFiles/FsDirMove -verbose -dump-bodies 10 11 To work around this we use the remote "TestSugarSync:Test" to test with. 12 13 */ 14 15 import ( 16 "context" 17 "fmt" 18 "io" 19 "log" 20 "net/http" 21 "net/url" 22 "path" 23 "regexp" 24 "strconv" 25 "strings" 26 "sync" 27 "time" 28 29 "github.com/pkg/errors" 30 "github.com/rclone/rclone/backend/sugarsync/api" 31 "github.com/rclone/rclone/fs" 32 "github.com/rclone/rclone/fs/config" 33 "github.com/rclone/rclone/fs/config/configmap" 34 "github.com/rclone/rclone/fs/config/configstruct" 35 "github.com/rclone/rclone/fs/config/obscure" 36 "github.com/rclone/rclone/fs/fserrors" 37 "github.com/rclone/rclone/fs/fshttp" 38 "github.com/rclone/rclone/fs/hash" 39 "github.com/rclone/rclone/lib/dircache" 40 "github.com/rclone/rclone/lib/encoder" 41 "github.com/rclone/rclone/lib/pacer" 42 "github.com/rclone/rclone/lib/rest" 43 ) 44 45 /* 46 maxFileLength = 16383 47 canWriteUnnormalized = true 48 canReadUnnormalized = true 49 canReadRenormalized = false 50 canStream = true 51 */ 52 53 const ( 54 appID = "/sc/9068489/215_1736969337" 55 accessKeyID = "OTA2ODQ4OTE1NzEzNDAwNTI4Njc" 56 encryptedPrivateAccessKey = "JONdXuRLNSRI5ue2Cr-vn-5m_YxyMNq9yHRKUQevqo8uaZjH502Z-x1axhyqOa8cDyldGq08RfFxozo" 57 minSleep = 10 * time.Millisecond 58 maxSleep = 2 * time.Second 59 decayConstant = 2 // bigger for slower decay, exponential 60 rootURL = "https://api.sugarsync.com" 61 listChunks = 500 // chunk size to read directory listings 62 expiryLeeway = 5 * time.Minute // time before the token expires to renew 63 ) 64 65 // withDefault returns value but if value is "" then it returns defaultValue 66 func withDefault(key, defaultValue string) (value string) { 67 if value == "" { 68 value = defaultValue 69 } 70 return value 71 } 72 73 // Register with Fs 74 func init() { 75 fs.Register(&fs.RegInfo{ 76 Name: "sugarsync", 77 Description: "Sugarsync", 78 NewFs: NewFs, 79 Config: func(name string, m configmap.Mapper) { 80 opt := new(Options) 81 err := configstruct.Set(m, opt) 82 if err != nil { 83 log.Fatalf("Failed to read options: %v", err) 84 } 85 86 if opt.RefreshToken != "" { 87 fmt.Printf("Already have a token - refresh?\n") 88 if !config.ConfirmWithConfig(m, "config_refresh_token", true) { 89 return 90 } 91 } 92 fmt.Printf("Username (email address)> ") 93 username := config.ReadLine() 94 password := config.GetPassword("Your Sugarsync password is only required during setup and will not be stored.") 95 96 authRequest := api.AppAuthorization{ 97 Username: username, 98 Password: password, 99 Application: withDefault(opt.AppID, appID), 100 AccessKeyID: withDefault(opt.AccessKeyID, accessKeyID), 101 PrivateAccessKey: withDefault(opt.PrivateAccessKey, obscure.MustReveal(encryptedPrivateAccessKey)), 102 } 103 104 var resp *http.Response 105 opts := rest.Opts{ 106 Method: "POST", 107 Path: "/app-authorization", 108 } 109 srv := rest.NewClient(fshttp.NewClient(fs.Config)).SetRoot(rootURL) // FIXME 110 111 // FIXME 112 //err = f.pacer.Call(func() (bool, error) { 113 resp, err = srv.CallXML(context.Background(), &opts, &authRequest, nil) 114 // return shouldRetry(resp, err) 115 //}) 116 if err != nil { 117 log.Fatalf("Failed to get token: %v", err) 118 } 119 opt.RefreshToken = resp.Header.Get("Location") 120 m.Set("refresh_token", opt.RefreshToken) 121 }, 122 Options: []fs.Option{{ 123 Name: "app_id", 124 Help: "Sugarsync App ID.\n\nLeave blank to use rclone's.", 125 }, { 126 Name: "access_key_id", 127 Help: "Sugarsync Access Key ID.\n\nLeave blank to use rclone's.", 128 }, { 129 Name: "private_access_key", 130 Help: "Sugarsync Private Access Key\n\nLeave blank to use rclone's.", 131 }, { 132 Name: "hard_delete", 133 Help: "Permanently delete files if true\notherwise put them in the deleted files.", 134 Default: false, 135 }, { 136 Name: "refresh_token", 137 Help: "Sugarsync refresh token\n\nLeave blank normally, will be auto configured by rclone.", 138 Advanced: true, 139 }, { 140 Name: "authorization", 141 Help: "Sugarsync authorization\n\nLeave blank normally, will be auto configured by rclone.", 142 Advanced: true, 143 }, { 144 Name: "authorization_expiry", 145 Help: "Sugarsync authorization expiry\n\nLeave blank normally, will be auto configured by rclone.", 146 Advanced: true, 147 }, { 148 Name: "user", 149 Help: "Sugarsync user\n\nLeave blank normally, will be auto configured by rclone.", 150 Advanced: true, 151 }, { 152 Name: "root_id", 153 Help: "Sugarsync root id\n\nLeave blank normally, will be auto configured by rclone.", 154 Advanced: true, 155 }, { 156 Name: "deleted_id", 157 Help: "Sugarsync deleted folder id\n\nLeave blank normally, will be auto configured by rclone.", 158 Advanced: true, 159 }, { 160 Name: config.ConfigEncoding, 161 Help: config.ConfigEncodingHelp, 162 Advanced: true, 163 Default: (encoder.Base | 164 encoder.EncodeCtl | 165 encoder.EncodeInvalidUtf8), 166 }}, 167 }) 168 } 169 170 // Options defines the configuration for this backend 171 type Options struct { 172 AppID string `config:"app_id"` 173 AccessKeyID string `config:"access_key_id"` 174 PrivateAccessKey string `config:"private_access_key"` 175 HardDelete bool `config:"hard_delete"` 176 RefreshToken string `config:"refresh_token"` 177 Authorization string `config:"authorization"` 178 AuthorizationExpiry string `config:"authorization_expiry"` 179 User string `config:"user"` 180 RootID string `config:"root_id"` 181 DeletedID string `config:"deleted_id"` 182 Enc encoder.MultiEncoder `config:"encoding"` 183 } 184 185 // Fs represents a remote sugarsync 186 type Fs struct { 187 name string // name of this remote 188 root string // the path we are working on 189 opt Options // parsed options 190 features *fs.Features // optional features 191 srv *rest.Client // the connection to the one drive server 192 dirCache *dircache.DirCache // Map of directory path to directory id 193 pacer *fs.Pacer // pacer for API calls 194 m configmap.Mapper // config file access 195 authMu sync.Mutex // used when doing authorization 196 authExpiry time.Time // time the authorization expires 197 } 198 199 // Object describes a sugarsync object 200 // 201 // Will definitely have info but maybe not meta 202 type Object struct { 203 fs *Fs // what this object is part of 204 remote string // The remote path 205 hasMetaData bool // whether info below has been set 206 size int64 // size of the object 207 modTime time.Time // modification time of the object 208 id string // ID of the object 209 } 210 211 // ------------------------------------------------------------ 212 213 // Name of the remote (as passed into NewFs) 214 func (f *Fs) Name() string { 215 return f.name 216 } 217 218 // Root of the remote (as passed into NewFs) 219 func (f *Fs) Root() string { 220 return f.root 221 } 222 223 // String converts this Fs to a string 224 func (f *Fs) String() string { 225 return fmt.Sprintf("sugarsync root '%s'", f.root) 226 } 227 228 // Features returns the optional features of this Fs 229 func (f *Fs) Features() *fs.Features { 230 return f.features 231 } 232 233 // parsePath parses a sugarsync 'url' 234 func parsePath(path string) (root string) { 235 root = strings.Trim(path, "/") 236 return 237 } 238 239 // retryErrorCodes is a slice of error codes that we will retry 240 var retryErrorCodes = []int{ 241 429, // Too Many Requests. 242 500, // Internal Server Error 243 502, // Bad Gateway 244 503, // Service Unavailable 245 504, // Gateway Timeout 246 509, // Bandwidth Limit Exceeded 247 } 248 249 // shouldRetry returns a boolean as to whether this resp and err 250 // deserve to be retried. It returns the err as a convenience 251 func shouldRetry(resp *http.Response, err error) (bool, error) { 252 return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err 253 } 254 255 // readMetaDataForPath reads the metadata from the path 256 func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.File, err error) { 257 // defer fs.Trace(f, "path=%q", path)("info=%+v, err=%v", &info, &err) 258 leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, path, false) 259 if err != nil { 260 if err == fs.ErrorDirNotFound { 261 return nil, fs.ErrorObjectNotFound 262 } 263 return nil, err 264 } 265 266 found, err := f.listAll(ctx, directoryID, func(item *api.File) bool { 267 if item.Name == leaf { 268 info = item 269 return true 270 } 271 return false 272 }, nil) 273 if err != nil { 274 return nil, err 275 } 276 if !found { 277 return nil, fs.ErrorObjectNotFound 278 } 279 return info, nil 280 } 281 282 // readMetaDataForID reads the metadata for a file from the ID 283 func (f *Fs) readMetaDataForID(ctx context.Context, ID string) (info *api.File, err error) { 284 var resp *http.Response 285 opts := rest.Opts{ 286 Method: "GET", 287 RootURL: ID, 288 } 289 err = f.pacer.Call(func() (bool, error) { 290 resp, err = f.srv.CallXML(ctx, &opts, nil, &info) 291 return shouldRetry(resp, err) 292 }) 293 if err != nil { 294 if resp != nil && resp.StatusCode == http.StatusNotFound { 295 return nil, fs.ErrorObjectNotFound 296 } 297 return nil, errors.Wrap(err, "failed to get authorization") 298 } 299 return info, nil 300 } 301 302 // getAuthToken gets an Auth token from the refresh token 303 func (f *Fs) getAuthToken(ctx context.Context) error { 304 fs.Debugf(f, "Renewing token") 305 306 var authRequest = api.TokenAuthRequest{ 307 AccessKeyID: withDefault(f.opt.AccessKeyID, accessKeyID), 308 PrivateAccessKey: withDefault(f.opt.PrivateAccessKey, obscure.MustReveal(encryptedPrivateAccessKey)), 309 RefreshToken: f.opt.RefreshToken, 310 } 311 312 if authRequest.RefreshToken == "" { 313 return errors.New("no refresh token found - run `rclone config reconnect`") 314 } 315 316 var authResponse api.Authorization 317 var err error 318 var resp *http.Response 319 opts := rest.Opts{ 320 Method: "POST", 321 Path: "/authorization", 322 ExtraHeaders: map[string]string{ 323 "Authorization": "", // unset Authorization 324 }, 325 } 326 err = f.pacer.Call(func() (bool, error) { 327 resp, err = f.srv.CallXML(ctx, &opts, &authRequest, &authResponse) 328 return shouldRetry(resp, err) 329 }) 330 if err != nil { 331 return errors.Wrap(err, "failed to get authorization") 332 } 333 f.opt.Authorization = resp.Header.Get("Location") 334 f.authExpiry = authResponse.Expiration 335 f.opt.User = authResponse.User 336 337 // Cache the results 338 f.m.Set("authorization", f.opt.Authorization) 339 f.m.Set("authorization_expiry", f.authExpiry.Format(time.RFC3339)) 340 f.m.Set("user", f.opt.User) 341 return nil 342 } 343 344 // Read the auth from the config file and refresh it if it is expired, setting it in srv 345 func (f *Fs) getAuth(req *http.Request) (err error) { 346 f.authMu.Lock() 347 defer f.authMu.Unlock() 348 ctx := req.Context() 349 350 // if have auth, check it is in date 351 if f.opt.Authorization == "" || f.opt.User == "" || f.authExpiry.IsZero() || time.Until(f.authExpiry) < expiryLeeway { 352 // Get the auth token 353 f.srv.SetSigner(nil) // temporariliy remove the signer so we don't infinitely recurse 354 err = f.getAuthToken(ctx) 355 f.srv.SetSigner(f.getAuth) // replace signer 356 if err != nil { 357 return err 358 } 359 } 360 361 // Set Authorization header 362 req.Header.Set("Authorization", f.opt.Authorization) 363 364 return nil 365 } 366 367 // Read the user info into f 368 func (f *Fs) getUser(ctx context.Context) (user *api.User, err error) { 369 var resp *http.Response 370 opts := rest.Opts{ 371 Method: "GET", 372 Path: "/user", 373 } 374 err = f.pacer.Call(func() (bool, error) { 375 resp, err = f.srv.CallXML(ctx, &opts, nil, &user) 376 return shouldRetry(resp, err) 377 }) 378 if err != nil { 379 return nil, errors.Wrap(err, "failed to get user") 380 } 381 return user, nil 382 } 383 384 // Read the expiry time from a string 385 func parseExpiry(expiryString string) time.Time { 386 if expiryString == "" { 387 return time.Time{} 388 } 389 expiry, err := time.Parse(time.RFC3339, expiryString) 390 if err != nil { 391 fs.Debugf("sugarsync", "Invalid expiry time %q read from config", expiryString) 392 return time.Time{} 393 } 394 return expiry 395 } 396 397 // NewFs constructs an Fs from the path, container:path 398 func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) { 399 ctx := context.Background() 400 401 opt := new(Options) 402 err := configstruct.Set(m, opt) 403 if err != nil { 404 return nil, err 405 } 406 407 root = parsePath(root) 408 client := fshttp.NewClient(fs.Config) 409 f := &Fs{ 410 name: name, 411 root: root, 412 opt: *opt, 413 srv: rest.NewClient(client).SetRoot(rootURL), 414 pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))), 415 m: m, 416 authExpiry: parseExpiry(opt.AuthorizationExpiry), 417 } 418 f.features = (&fs.Features{ 419 CaseInsensitive: true, 420 CanHaveEmptyDirectories: true, 421 }).Fill(f) 422 f.srv.SetSigner(f.getAuth) // use signing hook to get the auth 423 f.srv.SetErrorHandler(errorHandler) 424 425 // Get rootID 426 if f.opt.RootID == "" { 427 user, err := f.getUser(ctx) 428 if err != nil { 429 return nil, err 430 } 431 f.opt.RootID = user.SyncFolders 432 if strings.HasSuffix(f.opt.RootID, "/contents") { 433 f.opt.RootID = f.opt.RootID[:len(f.opt.RootID)-9] 434 } else { 435 return nil, errors.Errorf("unexpected rootID %q", f.opt.RootID) 436 } 437 // Cache the results 438 f.m.Set("root_id", f.opt.RootID) 439 f.opt.DeletedID = user.Deleted 440 f.m.Set("deleted_id", f.opt.DeletedID) 441 } 442 f.dirCache = dircache.New(root, f.opt.RootID, f) 443 444 // Find the current root 445 err = f.dirCache.FindRoot(ctx, false) 446 if err != nil { 447 // Assume it is a file 448 newRoot, remote := dircache.SplitPath(root) 449 oldDirCache := f.dirCache 450 f.dirCache = dircache.New(newRoot, f.opt.RootID, f) 451 f.root = newRoot 452 resetF := func() { 453 f.dirCache = oldDirCache 454 f.root = root 455 } 456 // Make new Fs which is the parent 457 err = f.dirCache.FindRoot(ctx, false) 458 if err != nil { 459 // No root so return old f 460 resetF() 461 return f, nil 462 } 463 _, err := f.newObjectWithInfo(ctx, remote, nil) 464 if err != nil { 465 if err == fs.ErrorObjectNotFound { 466 // File doesn't exist so return old f 467 resetF() 468 return f, nil 469 } 470 return nil, err 471 } 472 // return an error with an fs which points to the parent 473 return f, fs.ErrorIsFile 474 } 475 return f, nil 476 } 477 478 var findError = regexp.MustCompile(`<h3>(.*?)</h3>`) 479 480 // errorHandler parses errors from the body 481 // 482 // Errors seem to be HTML with <h3> containing the error text 483 // <h3>Can not move sync folder.</h3> 484 func errorHandler(resp *http.Response) (err error) { 485 body, err := rest.ReadBody(resp) 486 if err != nil { 487 return errors.Wrap(err, "error reading error out of body") 488 } 489 match := findError.FindSubmatch(body) 490 if match == nil || len(match) < 2 || len(match[1]) == 0 { 491 return errors.Errorf("HTTP error %v (%v) returned body: %q", resp.StatusCode, resp.Status, body) 492 } 493 return errors.Errorf("HTTP error %v (%v): %s", resp.StatusCode, resp.Status, match[1]) 494 } 495 496 // rootSlash returns root with a slash on if it is empty, otherwise empty string 497 func (f *Fs) rootSlash() string { 498 if f.root == "" { 499 return f.root 500 } 501 return f.root + "/" 502 } 503 504 // Return an Object from a path 505 // 506 // If it can't be found it returns the error fs.ErrorObjectNotFound. 507 func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.File) (fs.Object, error) { 508 o := &Object{ 509 fs: f, 510 remote: remote, 511 } 512 var err error 513 if info != nil { 514 // Set info 515 err = o.setMetaData(info) 516 } else { 517 err = o.readMetaData(ctx) // reads info and meta, returning an error 518 } 519 if err != nil { 520 return nil, err 521 } 522 return o, nil 523 } 524 525 // NewObject finds the Object at remote. If it can't be found 526 // it returns the error fs.ErrorObjectNotFound. 527 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 528 return f.newObjectWithInfo(ctx, remote, nil) 529 } 530 531 // FindLeaf finds a directory of name leaf in the folder with ID pathID 532 func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) { 533 //fs.Debugf(f, "FindLeaf(%q, %q)", pathID, leaf) 534 // Find the leaf in pathID 535 found, err = f.listAll(ctx, pathID, nil, func(item *api.Collection) bool { 536 if item.Name == leaf { 537 pathIDOut = item.Ref 538 return true 539 } 540 return false 541 }) 542 // fs.Debugf(f, ">FindLeaf %q, %v, %v", pathIDOut, found, err) 543 return pathIDOut, found, err 544 } 545 546 // CreateDir makes a directory with pathID as parent and name leaf 547 func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) { 548 // fs.Debugf(f, "CreateDir(%q, %q)\n", pathID, leaf) 549 var resp *http.Response 550 opts := rest.Opts{ 551 Method: "POST", 552 RootURL: pathID, 553 NoResponse: true, 554 } 555 var mkdir interface{} 556 if pathID == f.opt.RootID { 557 // folders at the root are syncFolders 558 mkdir = &api.CreateSyncFolder{ 559 Name: f.opt.Enc.FromStandardName(leaf), 560 } 561 opts.ExtraHeaders = map[string]string{ 562 "*X-SugarSync-API-Version": "1.5", // non canonical header 563 } 564 565 } else { 566 mkdir = &api.CreateFolder{ 567 Name: f.opt.Enc.FromStandardName(leaf), 568 } 569 } 570 err = f.pacer.Call(func() (bool, error) { 571 resp, err = f.srv.CallXML(ctx, &opts, mkdir, nil) 572 return shouldRetry(resp, err) 573 }) 574 if err != nil { 575 return "", err 576 } 577 newID = resp.Header.Get("Location") 578 if newID == "" { 579 // look up ID if not returned (eg for syncFolder) 580 var found bool 581 newID, found, err = f.FindLeaf(ctx, pathID, leaf) 582 if err != nil { 583 return "", err 584 } 585 if !found { 586 return "", errors.Errorf("couldn't find ID for newly created directory %q", leaf) 587 } 588 589 } 590 return newID, nil 591 } 592 593 // list the objects into the function supplied 594 // 595 // Should return true to finish processing 596 type listAllFileFn func(*api.File) bool 597 598 // list the folders into the function supplied 599 // 600 // Should return true to finish processing 601 type listAllFolderFn func(*api.Collection) bool 602 603 // Lists the directory required calling the user function on each item found 604 // 605 // If the user fn ever returns true then it early exits with found = true 606 func (f *Fs) listAll(ctx context.Context, dirID string, fileFn listAllFileFn, folderFn listAllFolderFn) (found bool, err error) { 607 opts := rest.Opts{ 608 Method: "GET", 609 RootURL: dirID, 610 Path: "/contents", 611 Parameters: url.Values{}, 612 } 613 opts.Parameters.Set("max", strconv.Itoa(listChunks)) 614 start := 0 615 OUTER: 616 for { 617 opts.Parameters.Set("start", strconv.Itoa(start)) 618 619 var result api.CollectionContents 620 var resp *http.Response 621 err = f.pacer.Call(func() (bool, error) { 622 resp, err = f.srv.CallXML(ctx, &opts, nil, &result) 623 return shouldRetry(resp, err) 624 }) 625 if err != nil { 626 return found, errors.Wrap(err, "couldn't list files") 627 } 628 if fileFn != nil { 629 for i := range result.Files { 630 item := &result.Files[i] 631 item.Name = f.opt.Enc.ToStandardName(item.Name) 632 if fileFn(item) { 633 found = true 634 break OUTER 635 } 636 } 637 } 638 if folderFn != nil { 639 for i := range result.Collections { 640 item := &result.Collections[i] 641 item.Name = f.opt.Enc.ToStandardName(item.Name) 642 if folderFn(item) { 643 found = true 644 break OUTER 645 } 646 } 647 } 648 if !result.HasMore { 649 break 650 } 651 start = result.End + 1 652 } 653 return 654 } 655 656 // List the objects and directories in dir into entries. The 657 // entries can be returned in any order but should be for a 658 // complete directory. 659 // 660 // dir should be "" to list the root, and should not have 661 // trailing slashes. 662 // 663 // This should return ErrDirNotFound if the directory isn't 664 // found. 665 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 666 err = f.dirCache.FindRoot(ctx, false) 667 if err != nil { 668 return nil, err 669 } 670 directoryID, err := f.dirCache.FindDir(ctx, dir, false) 671 if err != nil { 672 return nil, err 673 } 674 var iErr error 675 _, err = f.listAll(ctx, directoryID, 676 func(info *api.File) bool { 677 remote := path.Join(dir, info.Name) 678 o, err := f.newObjectWithInfo(ctx, remote, info) 679 if err != nil { 680 iErr = err 681 return true 682 } 683 entries = append(entries, o) 684 return false 685 }, 686 func(info *api.Collection) bool { 687 remote := path.Join(dir, info.Name) 688 id := info.Ref 689 // cache the directory ID for later lookups 690 f.dirCache.Put(remote, id) 691 d := fs.NewDir(remote, info.TimeCreated).SetID(id) 692 entries = append(entries, d) 693 return false 694 }) 695 if err != nil { 696 return nil, err 697 } 698 if iErr != nil { 699 return nil, iErr 700 } 701 return entries, nil 702 } 703 704 // Creates from the parameters passed in a half finished Object which 705 // must have setMetaData called on it 706 // 707 // Returns the object, leaf, directoryID and error 708 // 709 // Used to create new objects 710 func (f *Fs) createObject(ctx context.Context, remote string, modTime time.Time, size int64) (o *Object, leaf string, directoryID string, err error) { 711 // Create the directory for the object if it doesn't exist 712 leaf, directoryID, err = f.dirCache.FindRootAndPath(ctx, remote, true) 713 if err != nil { 714 return 715 } 716 // Temporary Object under construction 717 o = &Object{ 718 fs: f, 719 remote: remote, 720 } 721 return o, leaf, directoryID, nil 722 } 723 724 // Put the object 725 // 726 // Copy the reader in to the new object which is returned 727 // 728 // The new object may have been created if an error is returned 729 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 730 existingObj, err := f.newObjectWithInfo(ctx, src.Remote(), nil) 731 switch err { 732 case nil: 733 return existingObj, existingObj.Update(ctx, in, src, options...) 734 case fs.ErrorObjectNotFound: 735 // Not found so create it 736 return f.PutUnchecked(ctx, in, src, options...) 737 default: 738 return nil, err 739 } 740 } 741 742 // PutStream uploads to the remote path with the modTime given of indeterminate size 743 func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 744 return f.Put(ctx, in, src, options...) 745 } 746 747 // PutUnchecked the object into the container 748 // 749 // This will produce an error if the object already exists 750 // 751 // Copy the reader in to the new object which is returned 752 // 753 // The new object may have been created if an error is returned 754 func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 755 remote := src.Remote() 756 size := src.Size() 757 modTime := src.ModTime(ctx) 758 759 o, _, _, err := f.createObject(ctx, remote, modTime, size) 760 if err != nil { 761 return nil, err 762 } 763 return o, o.Update(ctx, in, src, options...) 764 } 765 766 // Mkdir creates the container if it doesn't exist 767 func (f *Fs) Mkdir(ctx context.Context, dir string) error { 768 err := f.dirCache.FindRoot(ctx, true) 769 if err != nil { 770 return err 771 } 772 if dir != "" { 773 _, err = f.dirCache.FindDir(ctx, dir, true) 774 } 775 return err 776 } 777 778 // delete removes an object or directory by ID either putting it 779 // in the Deleted files or deleting it permanently 780 func (f *Fs) delete(ctx context.Context, isFile bool, id string, remote string, hardDelete bool) (err error) { 781 if hardDelete { 782 opts := rest.Opts{ 783 Method: "DELETE", 784 RootURL: id, 785 NoResponse: true, 786 } 787 return f.pacer.Call(func() (bool, error) { 788 resp, err := f.srv.Call(ctx, &opts) 789 return shouldRetry(resp, err) 790 }) 791 } 792 // Move file/dir to deleted files if not hard delete 793 leaf := path.Base(remote) 794 if isFile { 795 _, err = f.moveFile(ctx, id, leaf, f.opt.DeletedID) 796 } else { 797 err = f.moveDir(ctx, id, leaf, f.opt.DeletedID) 798 } 799 return err 800 } 801 802 // purgeCheck removes the root directory, if check is set then it 803 // refuses to do so if it has anything in 804 func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error { 805 root := path.Join(f.root, dir) 806 if root == "" { 807 return errors.New("can't purge root directory") 808 } 809 dc := f.dirCache 810 err := dc.FindRoot(ctx, false) 811 if err != nil { 812 return err 813 } 814 directoryID, err := dc.FindDir(ctx, dir, false) 815 if err != nil { 816 return err 817 } 818 819 if check { 820 found, err := f.listAll(ctx, directoryID, func(item *api.File) bool { 821 return true 822 }, func(item *api.Collection) bool { 823 return true 824 }) 825 if err != nil { 826 return err 827 } 828 if found { 829 return fs.ErrorDirectoryNotEmpty 830 } 831 } 832 833 err = f.delete(ctx, false, directoryID, root, f.opt.HardDelete || check) 834 if err != nil { 835 return err 836 } 837 838 f.dirCache.FlushDir(dir) 839 return nil 840 } 841 842 // Rmdir deletes the root folder 843 // 844 // Returns an error if it isn't empty 845 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 846 return f.purgeCheck(ctx, dir, true) 847 } 848 849 // Precision return the precision of this Fs 850 func (f *Fs) Precision() time.Duration { 851 return fs.ModTimeNotSupported 852 } 853 854 // Copy src to this remote using server side copy operations. 855 // 856 // This is stored with the remote path given 857 // 858 // It returns the destination Object and a possible error 859 // 860 // Will only be called if src.Fs().Name() == f.Name() 861 // 862 // If it isn't possible then return fs.ErrorCantCopy 863 func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 864 srcObj, ok := src.(*Object) 865 if !ok { 866 fs.Debugf(src, "Can't copy - not same remote type") 867 return nil, fs.ErrorCantCopy 868 } 869 err := srcObj.readMetaData(ctx) 870 if err != nil { 871 return nil, err 872 } 873 874 srcPath := srcObj.fs.rootSlash() + srcObj.remote 875 dstPath := f.rootSlash() + remote 876 if strings.ToLower(srcPath) == strings.ToLower(dstPath) { 877 return nil, errors.Errorf("can't copy %q -> %q as are same name when lowercase", srcPath, dstPath) 878 } 879 880 // Create temporary object 881 dstObj, leaf, directoryID, err := f.createObject(ctx, remote, srcObj.modTime, srcObj.size) 882 if err != nil { 883 return nil, err 884 } 885 886 // Copy the object 887 opts := rest.Opts{ 888 Method: "POST", 889 RootURL: directoryID, 890 NoResponse: true, 891 } 892 copyFile := api.CopyFile{ 893 Name: f.opt.Enc.FromStandardName(leaf), 894 Source: srcObj.id, 895 } 896 var resp *http.Response 897 err = f.pacer.Call(func() (bool, error) { 898 resp, err = f.srv.CallXML(ctx, &opts, ©File, nil) 899 return shouldRetry(resp, err) 900 }) 901 if err != nil { 902 return nil, err 903 } 904 dstObj.id = resp.Header.Get("Location") 905 err = dstObj.readMetaData(ctx) 906 if err != nil { 907 return nil, err 908 } 909 return dstObj, nil 910 } 911 912 // Purge deletes all the files and the container 913 // 914 // Optional interface: Only implement this if you have a way of 915 // deleting all the files quicker than just running Remove() on the 916 // result of List() 917 func (f *Fs) Purge(ctx context.Context) error { 918 // Caution: Deleting a folder may orphan objects. It's important 919 // to remove the contents of the folder before you delete the 920 // folder. That's because removing a folder using DELETE does not 921 // remove the objects contained within the folder. If you delete 922 // a folder without first deleting its contents, the contents may 923 // be rendered inaccessible. 924 // 925 // An alternative to permanently deleting a folder is moving it to the 926 // Deleted Files folder. A folder (and all its contents) in the 927 // Deleted Files folder can be recovered. Your app can retrieve the 928 // link to the user's Deleted Files folder from the <deleted> element 929 // in the user resource representation. Your application can then move 930 // a folder to the Deleted Files folder by issuing an HTTP PUT request 931 // to the URL that represents the file resource and provide as input, 932 // XML that specifies in the <parent> element the link to the Deleted 933 // Files folder. 934 if f.opt.HardDelete { 935 return fs.ErrorCantPurge 936 } 937 return f.purgeCheck(ctx, "", false) 938 } 939 940 // moveFile moves a file server side 941 func (f *Fs) moveFile(ctx context.Context, id, leaf, directoryID string) (info *api.File, err error) { 942 opts := rest.Opts{ 943 Method: "PUT", 944 RootURL: id, 945 } 946 move := api.MoveFile{ 947 Name: f.opt.Enc.FromStandardName(leaf), 948 Parent: directoryID, 949 } 950 var resp *http.Response 951 err = f.pacer.Call(func() (bool, error) { 952 resp, err = f.srv.CallXML(ctx, &opts, &move, &info) 953 return shouldRetry(resp, err) 954 }) 955 if err != nil { 956 return nil, err 957 } 958 // The docs say that there is nothing returned but apparently 959 // there is... however it doesn't have Ref 960 // 961 // If ref not set, assume it hasn't changed 962 if info.Ref == "" { 963 info.Ref = id 964 } 965 return info, nil 966 } 967 968 // moveDir moves a folder server side 969 func (f *Fs) moveDir(ctx context.Context, id, leaf, directoryID string) (err error) { 970 // Move the object 971 opts := rest.Opts{ 972 Method: "PUT", 973 RootURL: id, 974 NoResponse: true, 975 } 976 move := api.MoveFolder{ 977 Name: f.opt.Enc.FromStandardName(leaf), 978 Parent: directoryID, 979 } 980 var resp *http.Response 981 return f.pacer.Call(func() (bool, error) { 982 resp, err = f.srv.CallXML(ctx, &opts, &move, nil) 983 return shouldRetry(resp, err) 984 }) 985 } 986 987 // Move src to this remote using server side move operations. 988 // 989 // This is stored with the remote path given 990 // 991 // It returns the destination Object and a possible error 992 // 993 // Will only be called if src.Fs().Name() == f.Name() 994 // 995 // If it isn't possible then return fs.ErrorCantMove 996 func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 997 srcObj, ok := src.(*Object) 998 if !ok { 999 fs.Debugf(src, "Can't move - not same remote type") 1000 return nil, fs.ErrorCantMove 1001 } 1002 1003 // Create temporary object 1004 dstObj, leaf, directoryID, err := f.createObject(ctx, remote, srcObj.modTime, srcObj.size) 1005 if err != nil { 1006 return nil, err 1007 } 1008 1009 // Do the move 1010 info, err := f.moveFile(ctx, srcObj.id, leaf, directoryID) 1011 if err != nil { 1012 return nil, err 1013 } 1014 1015 err = dstObj.setMetaData(info) 1016 if err != nil { 1017 return nil, err 1018 } 1019 return dstObj, nil 1020 } 1021 1022 // DirMove moves src, srcRemote to this remote at dstRemote 1023 // using server side move operations. 1024 // 1025 // Will only be called if src.Fs().Name() == f.Name() 1026 // 1027 // If it isn't possible then return fs.ErrorCantDirMove 1028 // 1029 // If destination exists then return fs.ErrorDirExists 1030 func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error { 1031 srcFs, ok := src.(*Fs) 1032 if !ok { 1033 fs.Debugf(srcFs, "Can't move directory - not same remote type") 1034 return fs.ErrorCantDirMove 1035 } 1036 srcPath := path.Join(srcFs.root, srcRemote) 1037 dstPath := path.Join(f.root, dstRemote) 1038 1039 // Refuse to move to or from the root 1040 if srcPath == "" || dstPath == "" { 1041 fs.Debugf(src, "DirMove error: Can't move root") 1042 return errors.New("can't move root directory") 1043 } 1044 1045 // find the root src directory 1046 err := srcFs.dirCache.FindRoot(ctx, false) 1047 if err != nil { 1048 return err 1049 } 1050 1051 // find the root dst directory 1052 if dstRemote != "" { 1053 err = f.dirCache.FindRoot(ctx, true) 1054 if err != nil { 1055 return err 1056 } 1057 } else { 1058 if f.dirCache.FoundRoot() { 1059 return fs.ErrorDirExists 1060 } 1061 } 1062 1063 // Find ID of dst parent, creating subdirs if necessary 1064 var leaf, directoryID string 1065 findPath := dstRemote 1066 if dstRemote == "" { 1067 findPath = f.root 1068 } 1069 leaf, directoryID, err = f.dirCache.FindPath(ctx, findPath, true) 1070 if err != nil { 1071 return err 1072 } 1073 1074 // Check destination does not exist 1075 if dstRemote != "" { 1076 _, err = f.dirCache.FindDir(ctx, dstRemote, false) 1077 if err == fs.ErrorDirNotFound { 1078 // OK 1079 } else if err != nil { 1080 return err 1081 } else { 1082 return fs.ErrorDirExists 1083 } 1084 } 1085 1086 // Find ID of src 1087 srcID, err := srcFs.dirCache.FindDir(ctx, srcRemote, false) 1088 if err != nil { 1089 return err 1090 } 1091 1092 // Do the move 1093 err = f.moveDir(ctx, srcID, leaf, directoryID) 1094 if err != nil { 1095 return err 1096 } 1097 srcFs.dirCache.FlushDir(srcRemote) 1098 return nil 1099 } 1100 1101 // PublicLink adds a "readable by anyone with link" permission on the given file or folder. 1102 func (f *Fs) PublicLink(ctx context.Context, remote string) (string, error) { 1103 obj, err := f.NewObject(ctx, remote) 1104 if err != nil { 1105 return "", err 1106 } 1107 o, ok := obj.(*Object) 1108 if !ok { 1109 return "", errors.New("internal error: not an Object") 1110 } 1111 opts := rest.Opts{ 1112 Method: "PUT", 1113 RootURL: o.id, 1114 } 1115 linkFile := api.SetPublicLink{ 1116 PublicLink: api.PublicLink{Enabled: true}, 1117 } 1118 var resp *http.Response 1119 var info *api.File 1120 err = f.pacer.Call(func() (bool, error) { 1121 resp, err = f.srv.CallXML(ctx, &opts, &linkFile, &info) 1122 return shouldRetry(resp, err) 1123 }) 1124 if err != nil { 1125 return "", err 1126 } 1127 return info.PublicLink.URL, err 1128 } 1129 1130 // DirCacheFlush resets the directory cache - used in testing as an 1131 // optional interface 1132 func (f *Fs) DirCacheFlush() { 1133 f.dirCache.ResetRoot() 1134 } 1135 1136 // Hashes returns the supported hash sets. 1137 func (f *Fs) Hashes() hash.Set { 1138 return hash.Set(hash.None) 1139 } 1140 1141 // ------------------------------------------------------------ 1142 1143 // Fs returns the parent Fs 1144 func (o *Object) Fs() fs.Info { 1145 return o.fs 1146 } 1147 1148 // Return a string version 1149 func (o *Object) String() string { 1150 if o == nil { 1151 return "<nil>" 1152 } 1153 return o.remote 1154 } 1155 1156 // Remote returns the remote path 1157 func (o *Object) Remote() string { 1158 return o.remote 1159 } 1160 1161 // Hash returns the SHA-1 of an object returning a lowercase hex string 1162 func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) { 1163 return "", hash.ErrUnsupported 1164 } 1165 1166 // Size returns the size of an object in bytes 1167 func (o *Object) Size() int64 { 1168 err := o.readMetaData(context.TODO()) 1169 if err != nil { 1170 fs.Logf(o, "Failed to read metadata: %v", err) 1171 return 0 1172 } 1173 return o.size 1174 } 1175 1176 // setMetaData sets the metadata from info 1177 func (o *Object) setMetaData(info *api.File) (err error) { 1178 o.hasMetaData = true 1179 o.size = info.Size 1180 o.modTime = info.LastModified 1181 if info.Ref != "" { 1182 o.id = info.Ref 1183 } else if o.id == "" { 1184 return errors.New("no ID found in response") 1185 } 1186 return nil 1187 } 1188 1189 // readMetaData gets the metadata if it hasn't already been fetched 1190 // 1191 // it also sets the info 1192 func (o *Object) readMetaData(ctx context.Context) (err error) { 1193 if o.hasMetaData { 1194 return nil 1195 } 1196 var info *api.File 1197 if o.id != "" { 1198 info, err = o.fs.readMetaDataForID(ctx, o.id) 1199 } else { 1200 info, err = o.fs.readMetaDataForPath(ctx, o.remote) 1201 } 1202 if err != nil { 1203 return err 1204 } 1205 return o.setMetaData(info) 1206 } 1207 1208 // ModTime returns the modification time of the object 1209 // 1210 // 1211 // It attempts to read the objects mtime and if that isn't present the 1212 // LastModified returned in the http headers 1213 func (o *Object) ModTime(ctx context.Context) time.Time { 1214 err := o.readMetaData(ctx) 1215 if err != nil { 1216 fs.Logf(o, "Failed to read metadata: %v", err) 1217 return time.Now() 1218 } 1219 return o.modTime 1220 } 1221 1222 // SetModTime sets the modification time of the local fs object 1223 func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error { 1224 // Sugarsync doesn't support setting the mod time. 1225 // 1226 // In theory (but not in the docs) you could patch the object, 1227 // however it doesn't work. 1228 return fs.ErrorCantSetModTime 1229 } 1230 1231 // Storable returns a boolean showing whether this object storable 1232 func (o *Object) Storable() bool { 1233 return true 1234 } 1235 1236 // Open an object for read 1237 func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) { 1238 if o.id == "" { 1239 return nil, errors.New("can't download - no id") 1240 } 1241 fs.FixRangeOption(options, o.size) 1242 var resp *http.Response 1243 opts := rest.Opts{ 1244 Method: "GET", 1245 RootURL: o.id, 1246 Path: "/data", 1247 Options: options, 1248 } 1249 err = o.fs.pacer.Call(func() (bool, error) { 1250 resp, err = o.fs.srv.Call(ctx, &opts) 1251 return shouldRetry(resp, err) 1252 }) 1253 if err != nil { 1254 return nil, err 1255 } 1256 return resp.Body, err 1257 } 1258 1259 // createFile makes an (empty) file with pathID as parent and name leaf and returns the ID 1260 func (f *Fs) createFile(ctx context.Context, pathID, leaf, mimeType string) (newID string, err error) { 1261 var resp *http.Response 1262 opts := rest.Opts{ 1263 Method: "POST", 1264 RootURL: pathID, 1265 NoResponse: true, 1266 } 1267 mkdir := api.CreateFile{ 1268 Name: f.opt.Enc.FromStandardName(leaf), 1269 MediaType: mimeType, 1270 } 1271 err = f.pacer.Call(func() (bool, error) { 1272 resp, err = f.srv.CallXML(ctx, &opts, &mkdir, nil) 1273 return shouldRetry(resp, err) 1274 }) 1275 if err != nil { 1276 return "", err 1277 } 1278 return resp.Header.Get("Location"), nil 1279 } 1280 1281 // Update the object with the contents of the io.Reader, modTime and size 1282 // 1283 // If existing is set then it updates the object rather than creating a new one 1284 // 1285 // The new object may have been created if an error is returned 1286 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) { 1287 size := src.Size() 1288 // modTime := src.ModTime(ctx) 1289 remote := o.Remote() 1290 1291 // Create the directory for the object if it doesn't exist 1292 leaf, directoryID, err := o.fs.dirCache.FindRootAndPath(ctx, remote, true) 1293 if err != nil { 1294 return err 1295 } 1296 1297 // if file doesn't exist, create it 1298 if o.id == "" { 1299 o.id, err = o.fs.createFile(ctx, directoryID, leaf, fs.MimeType(ctx, src)) 1300 if err != nil { 1301 return errors.Wrap(err, "failed to create file") 1302 } 1303 if o.id == "" { 1304 return errors.New("failed to create file: no ID") 1305 } 1306 // if created the file and returning an error then delete the file 1307 defer func() { 1308 if err != nil { 1309 delErr := o.fs.delete(ctx, true, o.id, remote, o.fs.opt.HardDelete) 1310 if delErr != nil { 1311 fs.Errorf(o, "failed to remove failed upload: %v", delErr) 1312 } 1313 } 1314 }() 1315 } 1316 1317 var resp *http.Response 1318 opts := rest.Opts{ 1319 Method: "PUT", 1320 RootURL: o.id, 1321 Path: "/data", 1322 NoResponse: true, 1323 Options: options, 1324 Body: in, 1325 } 1326 if size >= 0 { 1327 opts.ContentLength = &size 1328 } 1329 err = o.fs.pacer.CallNoRetry(func() (bool, error) { 1330 resp, err = o.fs.srv.Call(ctx, &opts) 1331 return shouldRetry(resp, err) 1332 }) 1333 if err != nil { 1334 return errors.Wrap(err, "failed to upload file") 1335 } 1336 1337 o.hasMetaData = false 1338 return o.readMetaData(ctx) 1339 } 1340 1341 // Remove an object 1342 func (o *Object) Remove(ctx context.Context) error { 1343 return o.fs.delete(ctx, true, o.id, o.remote, o.fs.opt.HardDelete) 1344 } 1345 1346 // ID returns the ID of the Object if known, or "" if not 1347 func (o *Object) ID() string { 1348 return o.id 1349 } 1350 1351 // Check the interfaces are satisfied 1352 var ( 1353 _ fs.Fs = (*Fs)(nil) 1354 _ fs.Purger = (*Fs)(nil) 1355 _ fs.PutStreamer = (*Fs)(nil) 1356 _ fs.Copier = (*Fs)(nil) 1357 _ fs.Mover = (*Fs)(nil) 1358 _ fs.DirMover = (*Fs)(nil) 1359 _ fs.DirCacheFlusher = (*Fs)(nil) 1360 _ fs.PublicLinker = (*Fs)(nil) 1361 _ fs.Object = (*Object)(nil) 1362 _ fs.IDer = (*Object)(nil) 1363 )