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