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