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