github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/backend/pcloud/pcloud.go (about) 1 // Package pcloud provides an interface to the Pcloud 2 // object storage system. 3 package pcloud 4 5 // FIXME implement ListR? /listfolder can do recursive lists 6 7 // FIXME cleanup returns login required? 8 9 // FIXME mime type? Fix overview if implement. 10 11 import ( 12 "context" 13 "fmt" 14 "io" 15 "log" 16 "net/http" 17 "net/url" 18 "path" 19 "strings" 20 "time" 21 22 "github.com/pkg/errors" 23 "github.com/rclone/rclone/backend/pcloud/api" 24 "github.com/rclone/rclone/fs" 25 "github.com/rclone/rclone/fs/config" 26 "github.com/rclone/rclone/fs/config/configmap" 27 "github.com/rclone/rclone/fs/config/configstruct" 28 "github.com/rclone/rclone/fs/config/obscure" 29 "github.com/rclone/rclone/fs/fserrors" 30 "github.com/rclone/rclone/fs/hash" 31 "github.com/rclone/rclone/lib/dircache" 32 "github.com/rclone/rclone/lib/encoder" 33 "github.com/rclone/rclone/lib/oauthutil" 34 "github.com/rclone/rclone/lib/pacer" 35 "github.com/rclone/rclone/lib/rest" 36 "golang.org/x/oauth2" 37 ) 38 39 const ( 40 rcloneClientID = "DnONSzyJXpm" 41 rcloneEncryptedClientSecret = "ej1OIF39VOQQ0PXaSdK9ztkLw3tdLNscW2157TKNQdQKkICR4uU7aFg4eFM" 42 minSleep = 10 * time.Millisecond 43 maxSleep = 2 * time.Second 44 decayConstant = 2 // bigger for slower decay, exponential 45 rootID = "d0" // ID of root folder is always this 46 rootURL = "https://api.pcloud.com" 47 ) 48 49 // Globals 50 var ( 51 // Description of how to auth for this app 52 oauthConfig = &oauth2.Config{ 53 Scopes: nil, 54 Endpoint: oauth2.Endpoint{ 55 AuthURL: "https://my.pcloud.com/oauth2/authorize", 56 TokenURL: "https://api.pcloud.com/oauth2_token", 57 }, 58 ClientID: rcloneClientID, 59 ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), 60 RedirectURL: oauthutil.RedirectLocalhostURL, 61 } 62 ) 63 64 // Register with Fs 65 func init() { 66 fs.Register(&fs.RegInfo{ 67 Name: "pcloud", 68 Description: "Pcloud", 69 NewFs: NewFs, 70 Config: func(name string, m configmap.Mapper) { 71 err := oauthutil.Config("pcloud", name, m, oauthConfig) 72 if err != nil { 73 log.Fatalf("Failed to configure token: %v", err) 74 } 75 }, 76 Options: []fs.Option{{ 77 Name: config.ConfigClientID, 78 Help: "Pcloud App Client Id\nLeave blank normally.", 79 }, { 80 Name: config.ConfigClientSecret, 81 Help: "Pcloud App Client Secret\nLeave blank normally.", 82 }, { 83 Name: config.ConfigEncoding, 84 Help: config.ConfigEncodingHelp, 85 Advanced: true, 86 // Encode invalid UTF-8 bytes as json doesn't handle them properly. 87 // 88 // TODO: Investigate Unicode simplification (\ gets converted to \ server-side) 89 Default: (encoder.Display | 90 encoder.EncodeBackSlash | 91 encoder.EncodeInvalidUtf8), 92 }}, 93 }) 94 } 95 96 // Options defines the configuration for this backend 97 type Options struct { 98 Enc encoder.MultiEncoder `config:"encoding"` 99 } 100 101 // Fs represents a remote pcloud 102 type Fs struct { 103 name string // name of this remote 104 root string // the path we are working on 105 opt Options // parsed options 106 features *fs.Features // optional features 107 srv *rest.Client // the connection to the server 108 dirCache *dircache.DirCache // Map of directory path to directory id 109 pacer *fs.Pacer // pacer for API calls 110 tokenRenewer *oauthutil.Renew // renew the token on expiry 111 } 112 113 // Object describes a pcloud object 114 // 115 // Will definitely have info but maybe not meta 116 type Object struct { 117 fs *Fs // what this object is part of 118 remote string // The remote path 119 hasMetaData bool // whether info below has been set 120 size int64 // size of the object 121 modTime time.Time // modification time of the object 122 id string // ID of the object 123 md5 string // MD5 if known 124 sha1 string // SHA1 if known 125 link *api.GetFileLinkResult 126 } 127 128 // ------------------------------------------------------------ 129 130 // Name of the remote (as passed into NewFs) 131 func (f *Fs) Name() string { 132 return f.name 133 } 134 135 // Root of the remote (as passed into NewFs) 136 func (f *Fs) Root() string { 137 return f.root 138 } 139 140 // String converts this Fs to a string 141 func (f *Fs) String() string { 142 return fmt.Sprintf("pcloud root '%s'", f.root) 143 } 144 145 // Features returns the optional features of this Fs 146 func (f *Fs) Features() *fs.Features { 147 return f.features 148 } 149 150 // parsePath parses an pcloud 'url' 151 func parsePath(path string) (root string) { 152 root = strings.Trim(path, "/") 153 return 154 } 155 156 // retryErrorCodes is a slice of error codes that we will retry 157 var retryErrorCodes = []int{ 158 429, // Too Many Requests. 159 500, // Internal Server Error 160 502, // Bad Gateway 161 503, // Service Unavailable 162 504, // Gateway Timeout 163 509, // Bandwidth Limit Exceeded 164 } 165 166 // shouldRetry returns a boolean as to whether this resp and err 167 // deserve to be retried. It returns the err as a convenience 168 func shouldRetry(resp *http.Response, err error) (bool, error) { 169 doRetry := false 170 171 // Check if it is an api.Error 172 if apiErr, ok := err.(*api.Error); ok { 173 // See https://docs.pcloud.com/errors/ for error treatment 174 // Errors are classified as 1xxx, 2xxx etc 175 switch apiErr.Result / 1000 { 176 case 4: // 4xxx: rate limiting 177 doRetry = true 178 case 5: // 5xxx: internal errors 179 doRetry = true 180 } 181 } 182 183 if resp != nil && resp.StatusCode == 401 && len(resp.Header["Www-Authenticate"]) == 1 && strings.Index(resp.Header["Www-Authenticate"][0], "expired_token") >= 0 { 184 doRetry = true 185 fs.Debugf(nil, "Should retry: %v", err) 186 } 187 return doRetry || fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err 188 } 189 190 // readMetaDataForPath reads the metadata from the path 191 func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.Item, err error) { 192 // defer fs.Trace(f, "path=%q", path)("info=%+v, err=%v", &info, &err) 193 leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, path, false) 194 if err != nil { 195 if err == fs.ErrorDirNotFound { 196 return nil, fs.ErrorObjectNotFound 197 } 198 return nil, err 199 } 200 201 found, err := f.listAll(ctx, directoryID, false, true, func(item *api.Item) bool { 202 if item.Name == leaf { 203 info = item 204 return true 205 } 206 return false 207 }) 208 if err != nil { 209 return nil, err 210 } 211 if !found { 212 return nil, fs.ErrorObjectNotFound 213 } 214 return info, nil 215 } 216 217 // errorHandler parses a non 2xx error response into an error 218 func errorHandler(resp *http.Response) error { 219 // Decode error response 220 errResponse := new(api.Error) 221 err := rest.DecodeJSON(resp, &errResponse) 222 if err != nil { 223 fs.Debugf(nil, "Couldn't decode error response: %v", err) 224 } 225 if errResponse.ErrorString == "" { 226 errResponse.ErrorString = resp.Status 227 } 228 if errResponse.Result == 0 { 229 errResponse.Result = resp.StatusCode 230 } 231 return errResponse 232 } 233 234 // NewFs constructs an Fs from the path, container:path 235 func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) { 236 ctx := context.Background() 237 // Parse config into Options struct 238 opt := new(Options) 239 err := configstruct.Set(m, opt) 240 if err != nil { 241 return nil, err 242 } 243 root = parsePath(root) 244 oAuthClient, ts, err := oauthutil.NewClient(name, m, oauthConfig) 245 if err != nil { 246 return nil, errors.Wrap(err, "failed to configure Pcloud") 247 } 248 249 f := &Fs{ 250 name: name, 251 root: root, 252 opt: *opt, 253 srv: rest.NewClient(oAuthClient).SetRoot(rootURL), 254 pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))), 255 } 256 f.features = (&fs.Features{ 257 CaseInsensitive: false, 258 CanHaveEmptyDirectories: true, 259 }).Fill(f) 260 f.srv.SetErrorHandler(errorHandler) 261 262 // Renew the token in the background 263 f.tokenRenewer = oauthutil.NewRenew(f.String(), ts, func() error { 264 _, err := f.readMetaDataForPath(ctx, "") 265 return err 266 }) 267 268 // Get rootID 269 f.dirCache = dircache.New(root, rootID, f) 270 271 // Find the current root 272 err = f.dirCache.FindRoot(ctx, false) 273 if err != nil { 274 // Assume it is a file 275 newRoot, remote := dircache.SplitPath(root) 276 tempF := *f 277 tempF.dirCache = dircache.New(newRoot, rootID, &tempF) 278 tempF.root = newRoot 279 // Make new Fs which is the parent 280 err = tempF.dirCache.FindRoot(ctx, false) 281 if err != nil { 282 // No root so return old f 283 return f, nil 284 } 285 _, err := tempF.newObjectWithInfo(ctx, remote, nil) 286 if err != nil { 287 if err == fs.ErrorObjectNotFound { 288 // File doesn't exist so return old f 289 return f, nil 290 } 291 return nil, err 292 } 293 // XXX: update the old f here instead of returning tempF, since 294 // `features` were already filled with functions having *f as a receiver. 295 // See https://github.com/rclone/rclone/issues/2182 296 f.dirCache = tempF.dirCache 297 f.root = tempF.root 298 // return an error with an fs which points to the parent 299 return f, fs.ErrorIsFile 300 } 301 return f, nil 302 } 303 304 // Return an Object from a path 305 // 306 // If it can't be found it returns the error fs.ErrorObjectNotFound. 307 func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.Item) (fs.Object, error) { 308 o := &Object{ 309 fs: f, 310 remote: remote, 311 } 312 var err error 313 if info != nil { 314 // Set info 315 err = o.setMetaData(info) 316 } else { 317 err = o.readMetaData(ctx) // reads info and meta, returning an error 318 } 319 if err != nil { 320 return nil, err 321 } 322 return o, nil 323 } 324 325 // NewObject finds the Object at remote. If it can't be found 326 // it returns the error fs.ErrorObjectNotFound. 327 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 328 return f.newObjectWithInfo(ctx, remote, nil) 329 } 330 331 // FindLeaf finds a directory of name leaf in the folder with ID pathID 332 func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) { 333 // Find the leaf in pathID 334 found, err = f.listAll(ctx, pathID, true, false, func(item *api.Item) bool { 335 if item.Name == leaf { 336 pathIDOut = item.ID 337 return true 338 } 339 return false 340 }) 341 return pathIDOut, found, err 342 } 343 344 // CreateDir makes a directory with pathID as parent and name leaf 345 func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) { 346 // fs.Debugf(f, "CreateDir(%q, %q)\n", pathID, leaf) 347 var resp *http.Response 348 var result api.ItemResult 349 opts := rest.Opts{ 350 Method: "POST", 351 Path: "/createfolder", 352 Parameters: url.Values{}, 353 } 354 opts.Parameters.Set("name", f.opt.Enc.FromStandardName(leaf)) 355 opts.Parameters.Set("folderid", dirIDtoNumber(pathID)) 356 err = f.pacer.Call(func() (bool, error) { 357 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 358 err = result.Error.Update(err) 359 return shouldRetry(resp, err) 360 }) 361 if err != nil { 362 //fmt.Printf("...Error %v\n", err) 363 return "", err 364 } 365 // fmt.Printf("...Id %q\n", *info.Id) 366 return result.Metadata.ID, nil 367 } 368 369 // Converts a dirID which is usually 'd' followed by digits into just 370 // the digits 371 func dirIDtoNumber(dirID string) string { 372 if len(dirID) > 0 && dirID[0] == 'd' { 373 return dirID[1:] 374 } 375 fs.Debugf(nil, "Invalid directory id %q", dirID) 376 return dirID 377 } 378 379 // Converts a fileID which is usually 'f' followed by digits into just 380 // the digits 381 func fileIDtoNumber(fileID string) string { 382 if len(fileID) > 0 && fileID[0] == 'f' { 383 return fileID[1:] 384 } 385 fs.Debugf(nil, "Invalid file id %q", fileID) 386 return fileID 387 } 388 389 // list the objects into the function supplied 390 // 391 // If directories is set it only sends directories 392 // User function to process a File item from listAll 393 // 394 // Should return true to finish processing 395 type listAllFn func(*api.Item) bool 396 397 // Lists the directory required calling the user function on each item found 398 // 399 // If the user fn ever returns true then it early exits with found = true 400 func (f *Fs) listAll(ctx context.Context, dirID string, directoriesOnly bool, filesOnly bool, fn listAllFn) (found bool, err error) { 401 opts := rest.Opts{ 402 Method: "GET", 403 Path: "/listfolder", 404 Parameters: url.Values{}, 405 } 406 opts.Parameters.Set("folderid", dirIDtoNumber(dirID)) 407 // FIXME can do recursive 408 409 var result api.ItemResult 410 var resp *http.Response 411 err = f.pacer.Call(func() (bool, error) { 412 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 413 err = result.Error.Update(err) 414 return shouldRetry(resp, err) 415 }) 416 if err != nil { 417 return found, errors.Wrap(err, "couldn't list files") 418 } 419 for i := range result.Metadata.Contents { 420 item := &result.Metadata.Contents[i] 421 if item.IsFolder { 422 if filesOnly { 423 continue 424 } 425 } else { 426 if directoriesOnly { 427 continue 428 } 429 } 430 item.Name = f.opt.Enc.ToStandardName(item.Name) 431 if fn(item) { 432 found = true 433 break 434 } 435 } 436 return 437 } 438 439 // List the objects and directories in dir into entries. The 440 // entries can be returned in any order but should be for a 441 // complete directory. 442 // 443 // dir should be "" to list the root, and should not have 444 // trailing slashes. 445 // 446 // This should return ErrDirNotFound if the directory isn't 447 // found. 448 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 449 err = f.dirCache.FindRoot(ctx, false) 450 if err != nil { 451 return nil, err 452 } 453 directoryID, err := f.dirCache.FindDir(ctx, dir, false) 454 if err != nil { 455 return nil, err 456 } 457 var iErr error 458 _, err = f.listAll(ctx, directoryID, false, false, func(info *api.Item) bool { 459 remote := path.Join(dir, info.Name) 460 if info.IsFolder { 461 // cache the directory ID for later lookups 462 f.dirCache.Put(remote, info.ID) 463 d := fs.NewDir(remote, info.ModTime()).SetID(info.ID) 464 // FIXME more info from dir? 465 entries = append(entries, d) 466 } else { 467 o, err := f.newObjectWithInfo(ctx, remote, info) 468 if err != nil { 469 iErr = err 470 return true 471 } 472 entries = append(entries, o) 473 } 474 return false 475 }) 476 if err != nil { 477 return nil, err 478 } 479 if iErr != nil { 480 return nil, iErr 481 } 482 return entries, nil 483 } 484 485 // Creates from the parameters passed in a half finished Object which 486 // must have setMetaData called on it 487 // 488 // Returns the object, leaf, directoryID and error 489 // 490 // Used to create new objects 491 func (f *Fs) createObject(ctx context.Context, remote string, modTime time.Time, size int64) (o *Object, leaf string, directoryID string, err error) { 492 // Create the directory for the object if it doesn't exist 493 leaf, directoryID, err = f.dirCache.FindRootAndPath(ctx, remote, true) 494 if err != nil { 495 return 496 } 497 // Temporary Object under construction 498 o = &Object{ 499 fs: f, 500 remote: remote, 501 } 502 return o, leaf, directoryID, nil 503 } 504 505 // Put the object into the container 506 // 507 // Copy the reader in to the new object which is returned 508 // 509 // The new object may have been created if an error is returned 510 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 511 remote := src.Remote() 512 size := src.Size() 513 modTime := src.ModTime(ctx) 514 515 o, _, _, err := f.createObject(ctx, remote, modTime, size) 516 if err != nil { 517 return nil, err 518 } 519 return o, o.Update(ctx, in, src, options...) 520 } 521 522 // Mkdir creates the container if it doesn't exist 523 func (f *Fs) Mkdir(ctx context.Context, dir string) error { 524 err := f.dirCache.FindRoot(ctx, true) 525 if err != nil { 526 return err 527 } 528 if dir != "" { 529 _, err = f.dirCache.FindDir(ctx, dir, true) 530 } 531 return err 532 } 533 534 // purgeCheck removes the root directory, if check is set then it 535 // refuses to do so if it has anything in 536 func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error { 537 root := path.Join(f.root, dir) 538 if root == "" { 539 return errors.New("can't purge root directory") 540 } 541 dc := f.dirCache 542 err := dc.FindRoot(ctx, false) 543 if err != nil { 544 return err 545 } 546 rootID, err := dc.FindDir(ctx, dir, false) 547 if err != nil { 548 return err 549 } 550 551 opts := rest.Opts{ 552 Method: "POST", 553 Path: "/deletefolder", 554 Parameters: url.Values{}, 555 } 556 opts.Parameters.Set("folderid", dirIDtoNumber(rootID)) 557 if !check { 558 opts.Path = "/deletefolderrecursive" 559 } 560 var resp *http.Response 561 var result api.ItemResult 562 err = f.pacer.Call(func() (bool, error) { 563 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 564 err = result.Error.Update(err) 565 return shouldRetry(resp, err) 566 }) 567 if err != nil { 568 return errors.Wrap(err, "rmdir failed") 569 } 570 f.dirCache.FlushDir(dir) 571 if err != nil { 572 return err 573 } 574 return nil 575 } 576 577 // Rmdir deletes the root folder 578 // 579 // Returns an error if it isn't empty 580 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 581 return f.purgeCheck(ctx, dir, true) 582 } 583 584 // Precision return the precision of this Fs 585 func (f *Fs) Precision() time.Duration { 586 return time.Second 587 } 588 589 // Copy src to this remote using server side copy operations. 590 // 591 // This is stored with the remote path given 592 // 593 // It returns the destination Object and a possible error 594 // 595 // Will only be called if src.Fs().Name() == f.Name() 596 // 597 // If it isn't possible then return fs.ErrorCantCopy 598 func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 599 srcObj, ok := src.(*Object) 600 if !ok { 601 fs.Debugf(src, "Can't copy - not same remote type") 602 return nil, fs.ErrorCantCopy 603 } 604 err := srcObj.readMetaData(ctx) 605 if err != nil { 606 return nil, err 607 } 608 609 // Create temporary object 610 dstObj, leaf, directoryID, err := f.createObject(ctx, remote, srcObj.modTime, srcObj.size) 611 if err != nil { 612 return nil, err 613 } 614 615 // Copy the object 616 opts := rest.Opts{ 617 Method: "POST", 618 Path: "/copyfile", 619 Parameters: url.Values{}, 620 } 621 opts.Parameters.Set("fileid", fileIDtoNumber(srcObj.id)) 622 opts.Parameters.Set("toname", f.opt.Enc.FromStandardName(leaf)) 623 opts.Parameters.Set("tofolderid", dirIDtoNumber(directoryID)) 624 opts.Parameters.Set("mtime", fmt.Sprintf("%d", srcObj.modTime.Unix())) 625 var resp *http.Response 626 var result api.ItemResult 627 err = f.pacer.Call(func() (bool, error) { 628 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 629 err = result.Error.Update(err) 630 return shouldRetry(resp, err) 631 }) 632 if err != nil { 633 return nil, err 634 } 635 err = dstObj.setMetaData(&result.Metadata) 636 if err != nil { 637 return nil, err 638 } 639 return dstObj, nil 640 } 641 642 // Purge deletes all the files and the container 643 // 644 // Optional interface: Only implement this if you have a way of 645 // deleting all the files quicker than just running Remove() on the 646 // result of List() 647 func (f *Fs) Purge(ctx context.Context) error { 648 return f.purgeCheck(ctx, "", false) 649 } 650 651 // CleanUp empties the trash 652 func (f *Fs) CleanUp(ctx context.Context) error { 653 err := f.dirCache.FindRoot(ctx, false) 654 if err != nil { 655 return err 656 } 657 opts := rest.Opts{ 658 Method: "POST", 659 Path: "/trash_clear", 660 Parameters: url.Values{}, 661 } 662 opts.Parameters.Set("folderid", dirIDtoNumber(f.dirCache.RootID())) 663 var resp *http.Response 664 var result api.Error 665 return f.pacer.Call(func() (bool, error) { 666 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 667 err = result.Update(err) 668 return shouldRetry(resp, err) 669 }) 670 } 671 672 // Move src to this remote using server side move operations. 673 // 674 // This is stored with the remote path given 675 // 676 // It returns the destination Object and a possible error 677 // 678 // Will only be called if src.Fs().Name() == f.Name() 679 // 680 // If it isn't possible then return fs.ErrorCantMove 681 func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 682 srcObj, ok := src.(*Object) 683 if !ok { 684 fs.Debugf(src, "Can't move - not same remote type") 685 return nil, fs.ErrorCantMove 686 } 687 688 // Create temporary object 689 dstObj, leaf, directoryID, err := f.createObject(ctx, remote, srcObj.modTime, srcObj.size) 690 if err != nil { 691 return nil, err 692 } 693 694 // Do the move 695 opts := rest.Opts{ 696 Method: "POST", 697 Path: "/renamefile", 698 Parameters: url.Values{}, 699 } 700 opts.Parameters.Set("fileid", fileIDtoNumber(srcObj.id)) 701 opts.Parameters.Set("toname", f.opt.Enc.FromStandardName(leaf)) 702 opts.Parameters.Set("tofolderid", dirIDtoNumber(directoryID)) 703 var resp *http.Response 704 var result api.ItemResult 705 err = f.pacer.Call(func() (bool, error) { 706 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 707 err = result.Error.Update(err) 708 return shouldRetry(resp, err) 709 }) 710 if err != nil { 711 return nil, err 712 } 713 714 err = dstObj.setMetaData(&result.Metadata) 715 if err != nil { 716 return nil, err 717 } 718 return dstObj, nil 719 } 720 721 // DirMove moves src, srcRemote to this remote at dstRemote 722 // using server side move operations. 723 // 724 // Will only be called if src.Fs().Name() == f.Name() 725 // 726 // If it isn't possible then return fs.ErrorCantDirMove 727 // 728 // If destination exists then return fs.ErrorDirExists 729 func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error { 730 srcFs, ok := src.(*Fs) 731 if !ok { 732 fs.Debugf(srcFs, "Can't move directory - not same remote type") 733 return fs.ErrorCantDirMove 734 } 735 srcPath := path.Join(srcFs.root, srcRemote) 736 dstPath := path.Join(f.root, dstRemote) 737 738 // Refuse to move to or from the root 739 if srcPath == "" || dstPath == "" { 740 fs.Debugf(src, "DirMove error: Can't move root") 741 return errors.New("can't move root directory") 742 } 743 744 // find the root src directory 745 err := srcFs.dirCache.FindRoot(ctx, false) 746 if err != nil { 747 return err 748 } 749 750 // find the root dst directory 751 if dstRemote != "" { 752 err = f.dirCache.FindRoot(ctx, true) 753 if err != nil { 754 return err 755 } 756 } else { 757 if f.dirCache.FoundRoot() { 758 return fs.ErrorDirExists 759 } 760 } 761 762 // Find ID of dst parent, creating subdirs if necessary 763 var leaf, directoryID string 764 findPath := dstRemote 765 if dstRemote == "" { 766 findPath = f.root 767 } 768 leaf, directoryID, err = f.dirCache.FindPath(ctx, findPath, true) 769 if err != nil { 770 return err 771 } 772 773 // Check destination does not exist 774 if dstRemote != "" { 775 _, err = f.dirCache.FindDir(ctx, dstRemote, false) 776 if err == fs.ErrorDirNotFound { 777 // OK 778 } else if err != nil { 779 return err 780 } else { 781 return fs.ErrorDirExists 782 } 783 } 784 785 // Find ID of src 786 srcID, err := srcFs.dirCache.FindDir(ctx, srcRemote, false) 787 if err != nil { 788 return err 789 } 790 791 // Do the move 792 opts := rest.Opts{ 793 Method: "POST", 794 Path: "/renamefolder", 795 Parameters: url.Values{}, 796 } 797 opts.Parameters.Set("folderid", dirIDtoNumber(srcID)) 798 opts.Parameters.Set("toname", f.opt.Enc.FromStandardName(leaf)) 799 opts.Parameters.Set("tofolderid", dirIDtoNumber(directoryID)) 800 var resp *http.Response 801 var result api.ItemResult 802 err = f.pacer.Call(func() (bool, error) { 803 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 804 err = result.Error.Update(err) 805 return shouldRetry(resp, err) 806 }) 807 if err != nil { 808 return err 809 } 810 811 srcFs.dirCache.FlushDir(srcRemote) 812 return nil 813 } 814 815 // DirCacheFlush resets the directory cache - used in testing as an 816 // optional interface 817 func (f *Fs) DirCacheFlush() { 818 f.dirCache.ResetRoot() 819 } 820 821 // About gets quota information 822 func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) { 823 opts := rest.Opts{ 824 Method: "POST", 825 Path: "/userinfo", 826 } 827 var resp *http.Response 828 var q api.UserInfo 829 err = f.pacer.Call(func() (bool, error) { 830 resp, err = f.srv.CallJSON(ctx, &opts, nil, &q) 831 err = q.Error.Update(err) 832 return shouldRetry(resp, err) 833 }) 834 if err != nil { 835 return nil, errors.Wrap(err, "about failed") 836 } 837 usage = &fs.Usage{ 838 Total: fs.NewUsageValue(q.Quota), // quota of bytes that can be used 839 Used: fs.NewUsageValue(q.UsedQuota), // bytes in use 840 Free: fs.NewUsageValue(q.Quota - q.UsedQuota), // bytes which can be uploaded before reaching the quota 841 } 842 return usage, nil 843 } 844 845 // Hashes returns the supported hash sets. 846 func (f *Fs) Hashes() hash.Set { 847 return hash.Set(hash.MD5 | hash.SHA1) 848 } 849 850 // ------------------------------------------------------------ 851 852 // Fs returns the parent Fs 853 func (o *Object) Fs() fs.Info { 854 return o.fs 855 } 856 857 // Return a string version 858 func (o *Object) String() string { 859 if o == nil { 860 return "<nil>" 861 } 862 return o.remote 863 } 864 865 // Remote returns the remote path 866 func (o *Object) Remote() string { 867 return o.remote 868 } 869 870 // getHashes fetches the hashes into the object 871 func (o *Object) getHashes(ctx context.Context) (err error) { 872 var resp *http.Response 873 var result api.ChecksumFileResult 874 opts := rest.Opts{ 875 Method: "GET", 876 Path: "/checksumfile", 877 Parameters: url.Values{}, 878 } 879 opts.Parameters.Set("fileid", fileIDtoNumber(o.id)) 880 err = o.fs.pacer.Call(func() (bool, error) { 881 resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &result) 882 err = result.Error.Update(err) 883 return shouldRetry(resp, err) 884 }) 885 if err != nil { 886 return err 887 } 888 o.setHashes(&result.Hashes) 889 return o.setMetaData(&result.Metadata) 890 } 891 892 // Hash returns the SHA-1 of an object returning a lowercase hex string 893 func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) { 894 if t != hash.MD5 && t != hash.SHA1 { 895 return "", hash.ErrUnsupported 896 } 897 if o.md5 == "" && o.sha1 == "" { 898 err := o.getHashes(ctx) 899 if err != nil { 900 return "", errors.Wrap(err, "failed to get hash") 901 } 902 } 903 if t == hash.MD5 { 904 return o.md5, nil 905 } 906 return o.sha1, nil 907 } 908 909 // Size returns the size of an object in bytes 910 func (o *Object) Size() int64 { 911 err := o.readMetaData(context.TODO()) 912 if err != nil { 913 fs.Logf(o, "Failed to read metadata: %v", err) 914 return 0 915 } 916 return o.size 917 } 918 919 // setMetaData sets the metadata from info 920 func (o *Object) setMetaData(info *api.Item) (err error) { 921 if info.IsFolder { 922 return errors.Wrapf(fs.ErrorNotAFile, "%q is a folder", o.remote) 923 } 924 o.hasMetaData = true 925 o.size = info.Size 926 o.modTime = info.ModTime() 927 o.id = info.ID 928 return nil 929 } 930 931 // setHashes sets the hashes from that passed in 932 func (o *Object) setHashes(hashes *api.Hashes) { 933 o.sha1 = hashes.SHA1 934 o.md5 = hashes.MD5 935 } 936 937 // readMetaData gets the metadata if it hasn't already been fetched 938 // 939 // it also sets the info 940 func (o *Object) readMetaData(ctx context.Context) (err error) { 941 if o.hasMetaData { 942 return nil 943 } 944 info, err := o.fs.readMetaDataForPath(ctx, o.remote) 945 if err != nil { 946 //if apiErr, ok := err.(*api.Error); ok { 947 // FIXME 948 // if apiErr.Code == "not_found" || apiErr.Code == "trashed" { 949 // return fs.ErrorObjectNotFound 950 // } 951 //} 952 return err 953 } 954 return o.setMetaData(info) 955 } 956 957 // ModTime returns the modification time of the object 958 // 959 // 960 // It attempts to read the objects mtime and if that isn't present the 961 // LastModified returned in the http headers 962 func (o *Object) ModTime(ctx context.Context) time.Time { 963 err := o.readMetaData(ctx) 964 if err != nil { 965 fs.Logf(o, "Failed to read metadata: %v", err) 966 return time.Now() 967 } 968 return o.modTime 969 } 970 971 // SetModTime sets the modification time of the local fs object 972 func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error { 973 // Pcloud doesn't have a way of doing this so returning this 974 // error will cause the file to be re-uploaded to set the time. 975 return fs.ErrorCantSetModTime 976 } 977 978 // Storable returns a boolean showing whether this object storable 979 func (o *Object) Storable() bool { 980 return true 981 } 982 983 // downloadURL fetches the download link 984 func (o *Object) downloadURL(ctx context.Context) (URL string, err error) { 985 if o.id == "" { 986 return "", errors.New("can't download - no id") 987 } 988 if o.link.IsValid() { 989 return o.link.URL(), nil 990 } 991 var resp *http.Response 992 var result api.GetFileLinkResult 993 opts := rest.Opts{ 994 Method: "GET", 995 Path: "/getfilelink", 996 Parameters: url.Values{}, 997 } 998 opts.Parameters.Set("fileid", fileIDtoNumber(o.id)) 999 err = o.fs.pacer.Call(func() (bool, error) { 1000 resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &result) 1001 err = result.Error.Update(err) 1002 return shouldRetry(resp, err) 1003 }) 1004 if err != nil { 1005 return "", err 1006 } 1007 if !result.IsValid() { 1008 return "", errors.Errorf("fetched invalid link %+v", result) 1009 } 1010 o.link = &result 1011 return o.link.URL(), nil 1012 } 1013 1014 // Open an object for read 1015 func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) { 1016 url, err := o.downloadURL(ctx) 1017 if err != nil { 1018 return nil, err 1019 } 1020 var resp *http.Response 1021 opts := rest.Opts{ 1022 Method: "GET", 1023 RootURL: url, 1024 Options: options, 1025 } 1026 err = o.fs.pacer.Call(func() (bool, error) { 1027 resp, err = o.fs.srv.Call(ctx, &opts) 1028 return shouldRetry(resp, err) 1029 }) 1030 if err != nil { 1031 return nil, err 1032 } 1033 return resp.Body, err 1034 } 1035 1036 // Update the object with the contents of the io.Reader, modTime and size 1037 // 1038 // If existing is set then it updates the object rather than creating a new one 1039 // 1040 // The new object may have been created if an error is returned 1041 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) { 1042 o.fs.tokenRenewer.Start() 1043 defer o.fs.tokenRenewer.Stop() 1044 1045 size := src.Size() // NB can upload without size 1046 modTime := src.ModTime(ctx) 1047 remote := o.Remote() 1048 1049 // Create the directory for the object if it doesn't exist 1050 leaf, directoryID, err := o.fs.dirCache.FindRootAndPath(ctx, remote, true) 1051 if err != nil { 1052 return err 1053 } 1054 1055 // Experiments with pcloud indicate that it doesn't like any 1056 // form of request which doesn't have a Content-Length. 1057 // According to the docs if you close the connection at the 1058 // end then it should work without Content-Length, but I 1059 // couldn't get this to work using opts.Close (which sets 1060 // http.Request.Close). 1061 // 1062 // This means that chunked transfer encoding needs to be 1063 // disabled and a Content-Length needs to be supplied. This 1064 // also rules out streaming. 1065 // 1066 // Docs: https://docs.pcloud.com/methods/file/uploadfile.html 1067 var resp *http.Response 1068 var result api.UploadFileResponse 1069 opts := rest.Opts{ 1070 Method: "PUT", 1071 Path: "/uploadfile", 1072 Body: in, 1073 ContentType: fs.MimeType(ctx, o), 1074 ContentLength: &size, 1075 Parameters: url.Values{}, 1076 TransferEncoding: []string{"identity"}, // pcloud doesn't like chunked encoding 1077 } 1078 leaf = o.fs.opt.Enc.FromStandardName(leaf) 1079 opts.Parameters.Set("filename", leaf) 1080 opts.Parameters.Set("folderid", dirIDtoNumber(directoryID)) 1081 opts.Parameters.Set("nopartial", "1") 1082 opts.Parameters.Set("mtime", fmt.Sprintf("%d", modTime.Unix())) 1083 1084 // Special treatment for a 0 length upload. This doesn't work 1085 // with PUT even with Content-Length set (by setting 1086 // opts.Body=0), so upload it as a multpart form POST with 1087 // Content-Length set. 1088 if size == 0 { 1089 formReader, contentType, overhead, err := rest.MultipartUpload(in, opts.Parameters, "content", leaf) 1090 if err != nil { 1091 return errors.Wrap(err, "failed to make multipart upload for 0 length file") 1092 } 1093 1094 contentLength := overhead + size 1095 1096 opts.ContentType = contentType 1097 opts.Body = formReader 1098 opts.Method = "POST" 1099 opts.Parameters = nil 1100 opts.ContentLength = &contentLength 1101 } 1102 1103 err = o.fs.pacer.CallNoRetry(func() (bool, error) { 1104 resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &result) 1105 err = result.Error.Update(err) 1106 return shouldRetry(resp, err) 1107 }) 1108 if err != nil { 1109 // sometimes pcloud leaves a half complete file on 1110 // error, so delete it if it exists 1111 delObj, delErr := o.fs.NewObject(ctx, o.remote) 1112 if delErr == nil && delObj != nil { 1113 _ = delObj.Remove(ctx) 1114 } 1115 return err 1116 } 1117 if len(result.Items) != 1 { 1118 return errors.Errorf("failed to upload %v - not sure why", o) 1119 } 1120 o.setHashes(&result.Checksums[0]) 1121 return o.setMetaData(&result.Items[0]) 1122 } 1123 1124 // Remove an object 1125 func (o *Object) Remove(ctx context.Context) error { 1126 opts := rest.Opts{ 1127 Method: "POST", 1128 Path: "/deletefile", 1129 Parameters: url.Values{}, 1130 } 1131 var result api.ItemResult 1132 opts.Parameters.Set("fileid", fileIDtoNumber(o.id)) 1133 return o.fs.pacer.Call(func() (bool, error) { 1134 resp, err := o.fs.srv.CallJSON(ctx, &opts, nil, &result) 1135 err = result.Error.Update(err) 1136 return shouldRetry(resp, err) 1137 }) 1138 } 1139 1140 // ID returns the ID of the Object if known, or "" if not 1141 func (o *Object) ID() string { 1142 return o.id 1143 } 1144 1145 // Check the interfaces are satisfied 1146 var ( 1147 _ fs.Fs = (*Fs)(nil) 1148 _ fs.Purger = (*Fs)(nil) 1149 _ fs.CleanUpper = (*Fs)(nil) 1150 _ fs.Copier = (*Fs)(nil) 1151 _ fs.Mover = (*Fs)(nil) 1152 _ fs.DirMover = (*Fs)(nil) 1153 _ fs.DirCacheFlusher = (*Fs)(nil) 1154 _ fs.Abouter = (*Fs)(nil) 1155 _ fs.Object = (*Object)(nil) 1156 _ fs.IDer = (*Object)(nil) 1157 )