github.com/10XDev/rclone@v1.52.3-0.20200626220027-16af9ab76b2a/backend/yandex/yandex.go (about) 1 package yandex 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "log" 9 "net/http" 10 "net/url" 11 "path" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/pkg/errors" 17 "github.com/rclone/rclone/backend/yandex/api" 18 "github.com/rclone/rclone/fs" 19 "github.com/rclone/rclone/fs/config" 20 "github.com/rclone/rclone/fs/config/configmap" 21 "github.com/rclone/rclone/fs/config/configstruct" 22 "github.com/rclone/rclone/fs/config/obscure" 23 "github.com/rclone/rclone/fs/fserrors" 24 "github.com/rclone/rclone/fs/hash" 25 "github.com/rclone/rclone/lib/encoder" 26 "github.com/rclone/rclone/lib/oauthutil" 27 "github.com/rclone/rclone/lib/pacer" 28 "github.com/rclone/rclone/lib/readers" 29 "github.com/rclone/rclone/lib/rest" 30 "golang.org/x/oauth2" 31 ) 32 33 //oAuth 34 const ( 35 rcloneClientID = "ac39b43b9eba4cae8ffb788c06d816a8" 36 rcloneEncryptedClientSecret = "EfyyNZ3YUEwXM5yAhi72G9YwKn2mkFrYwJNS7cY0TJAhFlX9K-uJFbGlpO-RYjrJ" 37 rootURL = "https://cloud-api.yandex.com/v1/disk" 38 minSleep = 10 * time.Millisecond 39 maxSleep = 2 * time.Second // may needs to be increased, testing needed 40 decayConstant = 2 // bigger for slower decay, exponential 41 ) 42 43 // Globals 44 var ( 45 // Description of how to auth for this app 46 oauthConfig = &oauth2.Config{ 47 Endpoint: oauth2.Endpoint{ 48 AuthURL: "https://oauth.yandex.com/authorize", //same as https://oauth.yandex.ru/authorize 49 TokenURL: "https://oauth.yandex.com/token", //same as https://oauth.yandex.ru/token 50 }, 51 ClientID: rcloneClientID, 52 ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), 53 RedirectURL: oauthutil.RedirectURL, 54 } 55 ) 56 57 // Register with Fs 58 func init() { 59 fs.Register(&fs.RegInfo{ 60 Name: "yandex", 61 Description: "Yandex Disk", 62 NewFs: NewFs, 63 Config: func(name string, m configmap.Mapper) { 64 err := oauthutil.Config("yandex", name, m, oauthConfig, nil) 65 if err != nil { 66 log.Fatalf("Failed to configure token: %v", err) 67 return 68 } 69 }, 70 Options: []fs.Option{{ 71 Name: config.ConfigClientID, 72 Help: "Yandex Client Id\nLeave blank normally.", 73 }, { 74 Name: config.ConfigClientSecret, 75 Help: "Yandex Client Secret\nLeave blank normally.", 76 }, { 77 Name: "unlink", 78 Help: "Remove existing public link to file/folder with link command rather than creating.\nDefault is false, meaning link command will create or retrieve public link.", 79 Default: false, 80 Advanced: true, 81 }, { 82 Name: config.ConfigEncoding, 83 Help: config.ConfigEncodingHelp, 84 Advanced: true, 85 // Of the control characters \t \n \r are allowed 86 // it doesn't seem worth making an exception for this 87 Default: (encoder.Display | 88 encoder.EncodeInvalidUtf8), 89 }}, 90 }) 91 } 92 93 // Options defines the configuration for this backend 94 type Options struct { 95 Token string `config:"token"` 96 Unlink bool `config:"unlink"` 97 Enc encoder.MultiEncoder `config:"encoding"` 98 } 99 100 // Fs represents a remote yandex 101 type Fs struct { 102 name string 103 root string // root path 104 opt Options // parsed options 105 features *fs.Features // optional features 106 srv *rest.Client // the connection to the yandex server 107 pacer *fs.Pacer // pacer for API calls 108 diskRoot string // root path with "disk:/" container name 109 } 110 111 // Object describes a swift object 112 type Object struct { 113 fs *Fs // what this object is part of 114 remote string // The remote path 115 hasMetaData bool // whether info below has been set 116 md5sum string // The MD5Sum of the object 117 size int64 // Bytes in the object 118 modTime time.Time // Modified time of the object 119 mimeType string // Content type according to the server 120 121 } 122 123 // ------------------------------------------------------------ 124 125 // Name of the remote (as passed into NewFs) 126 func (f *Fs) Name() string { 127 return f.name 128 } 129 130 // Root of the remote (as passed into NewFs) 131 func (f *Fs) Root() string { 132 return f.root 133 } 134 135 // String converts this Fs to a string 136 func (f *Fs) String() string { 137 return fmt.Sprintf("Yandex %s", f.root) 138 } 139 140 // Precision return the precision of this Fs 141 func (f *Fs) Precision() time.Duration { 142 return time.Nanosecond 143 } 144 145 // Hashes returns the supported hash sets. 146 func (f *Fs) Hashes() hash.Set { 147 return hash.Set(hash.MD5) 148 } 149 150 // Features returns the optional features of this Fs 151 func (f *Fs) Features() *fs.Features { 152 return f.features 153 } 154 155 // retryErrorCodes is a slice of error codes that we will retry 156 var retryErrorCodes = []int{ 157 429, // Too Many Requests. 158 500, // Internal Server Error 159 502, // Bad Gateway 160 503, // Service Unavailable 161 504, // Gateway Timeout 162 509, // Bandwidth Limit Exceeded 163 } 164 165 // shouldRetry returns a boolean as to whether this resp and err 166 // deserve to be retried. It returns the err as a convenience 167 func shouldRetry(resp *http.Response, err error) (bool, error) { 168 return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err 169 } 170 171 // errorHandler parses a non 2xx error response into an error 172 func errorHandler(resp *http.Response) error { 173 // Decode error response 174 errResponse := new(api.ErrorResponse) 175 err := rest.DecodeJSON(resp, &errResponse) 176 if err != nil { 177 fs.Debugf(nil, "Couldn't decode error response: %v", err) 178 } 179 if errResponse.Message == "" { 180 errResponse.Message = resp.Status 181 } 182 if errResponse.StatusCode == 0 { 183 errResponse.StatusCode = resp.StatusCode 184 } 185 return errResponse 186 } 187 188 // Sets root in f 189 func (f *Fs) setRoot(root string) { 190 //Set root path 191 f.root = strings.Trim(root, "/") 192 //Set disk root path. 193 //Adding "disk:" to root path as all paths on disk start with it 194 var diskRoot string 195 if f.root == "" { 196 diskRoot = "disk:/" 197 } else { 198 diskRoot = "disk:/" + f.root + "/" 199 } 200 f.diskRoot = diskRoot 201 } 202 203 // filePath returns an escaped file path (f.root, file) 204 func (f *Fs) filePath(file string) string { 205 return path.Join(f.diskRoot, file) 206 } 207 208 // dirPath returns an escaped file path (f.root, file) ending with '/' 209 func (f *Fs) dirPath(file string) string { 210 return path.Join(f.diskRoot, file) + "/" 211 } 212 213 func (f *Fs) readMetaDataForPath(ctx context.Context, path string, options *api.ResourceInfoRequestOptions) (*api.ResourceInfoResponse, error) { 214 opts := rest.Opts{ 215 Method: "GET", 216 Path: "/resources", 217 Parameters: url.Values{}, 218 } 219 220 opts.Parameters.Set("path", f.opt.Enc.FromStandardPath(path)) 221 222 if options.SortMode != nil { 223 opts.Parameters.Set("sort", options.SortMode.String()) 224 } 225 if options.Limit != 0 { 226 opts.Parameters.Set("limit", strconv.FormatUint(options.Limit, 10)) 227 } 228 if options.Offset != 0 { 229 opts.Parameters.Set("offset", strconv.FormatUint(options.Offset, 10)) 230 } 231 if options.Fields != nil { 232 opts.Parameters.Set("fields", strings.Join(options.Fields, ",")) 233 } 234 235 var err error 236 var info api.ResourceInfoResponse 237 var resp *http.Response 238 err = f.pacer.Call(func() (bool, error) { 239 resp, err = f.srv.CallJSON(ctx, &opts, nil, &info) 240 return shouldRetry(resp, err) 241 }) 242 243 if err != nil { 244 return nil, err 245 } 246 247 info.Name = f.opt.Enc.ToStandardName(info.Name) 248 return &info, nil 249 } 250 251 // NewFs constructs an Fs from the path, container:path 252 func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) { 253 ctx := context.TODO() 254 // Parse config into Options struct 255 opt := new(Options) 256 err := configstruct.Set(m, opt) 257 if err != nil { 258 return nil, err 259 } 260 261 token, err := oauthutil.GetToken(name, m) 262 if err != nil { 263 log.Fatalf("Couldn't read OAuth token (this should never happen).") 264 } 265 if token.RefreshToken == "" { 266 log.Fatalf("Unable to get RefreshToken. If you are upgrading from older versions of rclone, please run `rclone config` and re-configure this backend.") 267 } 268 if token.TokenType != "OAuth" { 269 token.TokenType = "OAuth" 270 err = oauthutil.PutToken(name, m, token, false) 271 if err != nil { 272 log.Fatalf("Couldn't save OAuth token (this should never happen).") 273 } 274 log.Printf("Automatically upgraded OAuth config.") 275 } 276 oAuthClient, _, err := oauthutil.NewClient(name, m, oauthConfig) 277 if err != nil { 278 log.Fatalf("Failed to configure Yandex: %v", err) 279 } 280 281 f := &Fs{ 282 name: name, 283 opt: *opt, 284 srv: rest.NewClient(oAuthClient).SetRoot(rootURL), 285 pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))), 286 } 287 f.setRoot(root) 288 f.features = (&fs.Features{ 289 ReadMimeType: true, 290 WriteMimeType: true, 291 CanHaveEmptyDirectories: true, 292 }).Fill(f) 293 f.srv.SetErrorHandler(errorHandler) 294 295 // Check to see if the object exists and is a file 296 //request object meta info 297 // Check to see if the object exists and is a file 298 //request object meta info 299 if info, err := f.readMetaDataForPath(ctx, f.diskRoot, &api.ResourceInfoRequestOptions{}); err != nil { 300 301 } else { 302 if info.ResourceType == "file" { 303 rootDir := path.Dir(root) 304 if rootDir == "." { 305 rootDir = "" 306 } 307 f.setRoot(rootDir) 308 // return an error with an fs which points to the parent 309 return f, fs.ErrorIsFile 310 } 311 } 312 return f, nil 313 } 314 315 // Convert a list item into a DirEntry 316 func (f *Fs) itemToDirEntry(ctx context.Context, remote string, object *api.ResourceInfoResponse) (fs.DirEntry, error) { 317 switch object.ResourceType { 318 case "dir": 319 t, err := time.Parse(time.RFC3339Nano, object.Modified) 320 if err != nil { 321 return nil, errors.Wrap(err, "error parsing time in directory item") 322 } 323 d := fs.NewDir(remote, t).SetSize(object.Size) 324 return d, nil 325 case "file": 326 o, err := f.newObjectWithInfo(ctx, remote, object) 327 if err != nil { 328 return nil, err 329 } 330 return o, nil 331 default: 332 fs.Debugf(f, "Unknown resource type %q", object.ResourceType) 333 } 334 return nil, nil 335 } 336 337 // List the objects and directories in dir into entries. The 338 // entries can be returned in any order but should be for a 339 // complete directory. 340 // 341 // dir should be "" to list the root, and should not have 342 // trailing slashes. 343 // 344 // This should return ErrDirNotFound if the directory isn't 345 // found. 346 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 347 root := f.dirPath(dir) 348 349 var limit uint64 = 1000 // max number of objects per request 350 var itemsCount uint64 // number of items per page in response 351 var offset uint64 // for the next page of requests 352 353 for { 354 opts := &api.ResourceInfoRequestOptions{ 355 Limit: limit, 356 Offset: offset, 357 } 358 info, err := f.readMetaDataForPath(ctx, root, opts) 359 360 if err != nil { 361 if apiErr, ok := err.(*api.ErrorResponse); ok { 362 // does not exist 363 if apiErr.ErrorName == "DiskNotFoundError" { 364 return nil, fs.ErrorDirNotFound 365 } 366 } 367 return nil, err 368 } 369 itemsCount = uint64(len(info.Embedded.Items)) 370 371 if info.ResourceType == "dir" { 372 //list all subdirs 373 for _, element := range info.Embedded.Items { 374 element.Name = f.opt.Enc.ToStandardName(element.Name) 375 remote := path.Join(dir, element.Name) 376 entry, err := f.itemToDirEntry(ctx, remote, &element) 377 if err != nil { 378 return nil, err 379 } 380 if entry != nil { 381 entries = append(entries, entry) 382 } 383 } 384 } else if info.ResourceType == "file" { 385 return nil, fs.ErrorIsFile 386 } 387 388 //offset for the next page of items 389 offset += itemsCount 390 //check if we reached end of list 391 if itemsCount < limit { 392 break 393 } 394 } 395 396 return entries, nil 397 } 398 399 // Return an Object from a path 400 // 401 // If it can't be found it returns the error fs.ErrorObjectNotFound. 402 func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.ResourceInfoResponse) (fs.Object, error) { 403 o := &Object{ 404 fs: f, 405 remote: remote, 406 } 407 var err error 408 if info != nil { 409 err = o.setMetaData(info) 410 } else { 411 err = o.readMetaData(ctx) 412 if apiErr, ok := err.(*api.ErrorResponse); ok { 413 // does not exist 414 if apiErr.ErrorName == "DiskNotFoundError" { 415 return nil, fs.ErrorObjectNotFound 416 } 417 } 418 } 419 if err != nil { 420 return nil, err 421 } 422 return o, nil 423 } 424 425 // NewObject finds the Object at remote. If it can't be found it 426 // returns the error fs.ErrorObjectNotFound. 427 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 428 return f.newObjectWithInfo(ctx, remote, nil) 429 } 430 431 // Creates from the parameters passed in a half finished Object which 432 // must have setMetaData called on it 433 // 434 // Used to create new objects 435 func (f *Fs) createObject(remote string, modTime time.Time, size int64) (o *Object) { 436 // Temporary Object under construction 437 o = &Object{ 438 fs: f, 439 remote: remote, 440 size: size, 441 modTime: modTime, 442 } 443 return o 444 } 445 446 // Put the object 447 // 448 // Copy the reader in to the new object which is returned 449 // 450 // The new object may have been created if an error is returned 451 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 452 o := f.createObject(src.Remote(), src.ModTime(ctx), src.Size()) 453 return o, o.Update(ctx, in, src, options...) 454 } 455 456 // PutStream uploads to the remote path with the modTime given of indeterminate size 457 func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 458 return f.Put(ctx, in, src, options...) 459 } 460 461 // CreateDir makes a directory 462 func (f *Fs) CreateDir(ctx context.Context, path string) (err error) { 463 //fmt.Printf("CreateDir: %s\n", path) 464 465 var resp *http.Response 466 opts := rest.Opts{ 467 Method: "PUT", 468 Path: "/resources", 469 Parameters: url.Values{}, 470 NoResponse: true, 471 } 472 473 // If creating a directory with a : use (undocumented) disk: prefix 474 if strings.IndexRune(path, ':') >= 0 { 475 path = "disk:" + path 476 } 477 opts.Parameters.Set("path", f.opt.Enc.FromStandardPath(path)) 478 479 err = f.pacer.Call(func() (bool, error) { 480 resp, err = f.srv.Call(ctx, &opts) 481 return shouldRetry(resp, err) 482 }) 483 if err != nil { 484 // fmt.Printf("CreateDir %q Error: %s\n", path, err.Error()) 485 return err 486 } 487 // fmt.Printf("...Id %q\n", *info.Id) 488 return nil 489 } 490 491 // This really needs improvement and especially proper error checking 492 // but Yandex does not publish a List of possible errors and when they're 493 // expected to occur. 494 func (f *Fs) mkDirs(ctx context.Context, path string) (err error) { 495 //trim filename from path 496 //dirString := strings.TrimSuffix(path, filepath.Base(path)) 497 //trim "disk:" from path 498 dirString := strings.TrimPrefix(path, "disk:") 499 if dirString == "" { 500 return nil 501 } 502 503 if err = f.CreateDir(ctx, dirString); err != nil { 504 if apiErr, ok := err.(*api.ErrorResponse); ok { 505 // already exists 506 if apiErr.ErrorName != "DiskPathPointsToExistentDirectoryError" { 507 // 2 if it fails then create all directories in the path from root. 508 dirs := strings.Split(dirString, "/") //path separator 509 var mkdirpath = "/" //path separator / 510 for _, element := range dirs { 511 if element != "" { 512 mkdirpath += element + "/" //path separator / 513 if err = f.CreateDir(ctx, mkdirpath); err != nil { 514 // ignore errors while creating dirs 515 } 516 } 517 } 518 } 519 return nil 520 } 521 } 522 return err 523 } 524 525 func (f *Fs) mkParentDirs(ctx context.Context, resPath string) error { 526 // defer log.Trace(dirPath, "")("") 527 // chop off trailing / if it exists 528 if strings.HasSuffix(resPath, "/") { 529 resPath = resPath[:len(resPath)-1] 530 } 531 parent := path.Dir(resPath) 532 if parent == "." { 533 parent = "" 534 } 535 return f.mkDirs(ctx, parent) 536 } 537 538 // Mkdir creates the container if it doesn't exist 539 func (f *Fs) Mkdir(ctx context.Context, dir string) error { 540 path := f.filePath(dir) 541 return f.mkDirs(ctx, path) 542 } 543 544 // waitForJob waits for the job with status in url to complete 545 func (f *Fs) waitForJob(ctx context.Context, location string) (err error) { 546 opts := rest.Opts{ 547 RootURL: location, 548 Method: "GET", 549 } 550 deadline := time.Now().Add(fs.Config.Timeout) 551 for time.Now().Before(deadline) { 552 var resp *http.Response 553 var body []byte 554 err = f.pacer.Call(func() (bool, error) { 555 resp, err = f.srv.Call(ctx, &opts) 556 if err != nil { 557 return fserrors.ShouldRetry(err), err 558 } 559 body, err = rest.ReadBody(resp) 560 return fserrors.ShouldRetry(err), err 561 }) 562 if err != nil { 563 return err 564 } 565 // Try to decode the body first as an api.AsyncOperationStatus 566 var status api.AsyncStatus 567 err = json.Unmarshal(body, &status) 568 if err != nil { 569 return errors.Wrapf(err, "async status result not JSON: %q", body) 570 } 571 572 switch status.Status { 573 case "failure": 574 return errors.Errorf("async operation returned %q", status.Status) 575 case "success": 576 return nil 577 } 578 579 time.Sleep(1 * time.Second) 580 } 581 return errors.Errorf("async operation didn't complete after %v", fs.Config.Timeout) 582 } 583 584 func (f *Fs) delete(ctx context.Context, path string, hardDelete bool) (err error) { 585 opts := rest.Opts{ 586 Method: "DELETE", 587 Path: "/resources", 588 Parameters: url.Values{}, 589 } 590 591 opts.Parameters.Set("path", f.opt.Enc.FromStandardPath(path)) 592 opts.Parameters.Set("permanently", strconv.FormatBool(hardDelete)) 593 594 var resp *http.Response 595 var body []byte 596 err = f.pacer.Call(func() (bool, error) { 597 resp, err = f.srv.Call(ctx, &opts) 598 if err != nil { 599 return fserrors.ShouldRetry(err), err 600 } 601 body, err = rest.ReadBody(resp) 602 return fserrors.ShouldRetry(err), err 603 }) 604 if err != nil { 605 return err 606 } 607 608 // if 202 Accepted it's an async operation we have to wait for it complete before retuning 609 if resp.StatusCode == 202 { 610 var info api.AsyncInfo 611 err = json.Unmarshal(body, &info) 612 if err != nil { 613 return errors.Wrapf(err, "async info result not JSON: %q", body) 614 } 615 return f.waitForJob(ctx, info.HRef) 616 } 617 return nil 618 } 619 620 // purgeCheck remotes the root directory, if check is set then it 621 // refuses to do so if it has anything in 622 func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error { 623 root := f.filePath(dir) 624 if check { 625 //to comply with rclone logic we check if the directory is empty before delete. 626 //send request to get list of objects in this directory. 627 info, err := f.readMetaDataForPath(ctx, root, &api.ResourceInfoRequestOptions{}) 628 if err != nil { 629 return errors.Wrap(err, "rmdir failed") 630 } 631 if len(info.Embedded.Items) != 0 { 632 return fs.ErrorDirectoryNotEmpty 633 } 634 } 635 //delete directory 636 return f.delete(ctx, root, false) 637 } 638 639 // Rmdir deletes the container 640 // 641 // Returns an error if it isn't empty 642 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 643 return f.purgeCheck(ctx, dir, true) 644 } 645 646 // Purge deletes all the files and the container 647 // 648 // Optional interface: Only implement this if you have a way of 649 // deleting all the files quicker than just running Remove() on the 650 // result of List() 651 func (f *Fs) Purge(ctx context.Context) error { 652 return f.purgeCheck(ctx, "", false) 653 } 654 655 // copyOrMoves copies or moves directories or files depending on the method parameter 656 func (f *Fs) copyOrMove(ctx context.Context, method, src, dst string, overwrite bool) (err error) { 657 opts := rest.Opts{ 658 Method: "POST", 659 Path: "/resources/" + method, 660 Parameters: url.Values{}, 661 } 662 663 opts.Parameters.Set("from", f.opt.Enc.FromStandardPath(src)) 664 opts.Parameters.Set("path", f.opt.Enc.FromStandardPath(dst)) 665 opts.Parameters.Set("overwrite", strconv.FormatBool(overwrite)) 666 667 var resp *http.Response 668 var body []byte 669 err = f.pacer.Call(func() (bool, error) { 670 resp, err = f.srv.Call(ctx, &opts) 671 if err != nil { 672 return fserrors.ShouldRetry(err), err 673 } 674 body, err = rest.ReadBody(resp) 675 return fserrors.ShouldRetry(err), err 676 }) 677 if err != nil { 678 return err 679 } 680 681 // if 202 Accepted it's an async operation we have to wait for it complete before retuning 682 if resp.StatusCode == 202 { 683 var info api.AsyncInfo 684 err = json.Unmarshal(body, &info) 685 if err != nil { 686 return errors.Wrapf(err, "async info result not JSON: %q", body) 687 } 688 return f.waitForJob(ctx, info.HRef) 689 } 690 return nil 691 } 692 693 // Copy src to this remote using server side copy operations. 694 // 695 // This is stored with the remote path given 696 // 697 // It returns the destination Object and a possible error 698 // 699 // Will only be called if src.Fs().Name() == f.Name() 700 // 701 // If it isn't possible then return fs.ErrorCantCopy 702 func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 703 srcObj, ok := src.(*Object) 704 if !ok { 705 fs.Debugf(src, "Can't copy - not same remote type") 706 return nil, fs.ErrorCantCopy 707 } 708 709 dstPath := f.filePath(remote) 710 err := f.mkParentDirs(ctx, dstPath) 711 if err != nil { 712 return nil, err 713 } 714 err = f.copyOrMove(ctx, "copy", srcObj.filePath(), dstPath, false) 715 716 if err != nil { 717 return nil, errors.Wrap(err, "couldn't copy file") 718 } 719 720 return f.NewObject(ctx, remote) 721 } 722 723 // Move src to this remote using server side move operations. 724 // 725 // This is stored with the remote path given 726 // 727 // It returns the destination Object and a possible error 728 // 729 // Will only be called if src.Fs().Name() == f.Name() 730 // 731 // If it isn't possible then return fs.ErrorCantMove 732 func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 733 srcObj, ok := src.(*Object) 734 if !ok { 735 fs.Debugf(src, "Can't move - not same remote type") 736 return nil, fs.ErrorCantMove 737 } 738 739 dstPath := f.filePath(remote) 740 err := f.mkParentDirs(ctx, dstPath) 741 if err != nil { 742 return nil, err 743 } 744 err = f.copyOrMove(ctx, "move", srcObj.filePath(), dstPath, false) 745 746 if err != nil { 747 return nil, errors.Wrap(err, "couldn't move file") 748 } 749 750 return f.NewObject(ctx, remote) 751 } 752 753 // DirMove moves src, srcRemote to this remote at dstRemote 754 // using server side move operations. 755 // 756 // Will only be called if src.Fs().Name() == f.Name() 757 // 758 // If it isn't possible then return fs.ErrorCantDirMove 759 // 760 // If destination exists then return fs.ErrorDirExists 761 func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error { 762 srcFs, ok := src.(*Fs) 763 if !ok { 764 fs.Debugf(srcFs, "Can't move directory - not same remote type") 765 return fs.ErrorCantDirMove 766 } 767 srcPath := path.Join(srcFs.diskRoot, srcRemote) 768 dstPath := f.dirPath(dstRemote) 769 770 //fmt.Printf("Move src: %s (FullPath: %s), dst: %s (FullPath: %s)\n", srcRemote, srcPath, dstRemote, dstPath) 771 772 // Refuse to move to or from the root 773 if srcPath == "disk:/" || dstPath == "disk:/" { 774 fs.Debugf(src, "DirMove error: Can't move root") 775 return errors.New("can't move root directory") 776 } 777 778 err := f.mkParentDirs(ctx, dstPath) 779 if err != nil { 780 return err 781 } 782 783 _, err = f.readMetaDataForPath(ctx, dstPath, &api.ResourceInfoRequestOptions{}) 784 if apiErr, ok := err.(*api.ErrorResponse); ok { 785 // does not exist 786 if apiErr.ErrorName == "DiskNotFoundError" { 787 // OK 788 } 789 } else if err != nil { 790 return err 791 } else { 792 return fs.ErrorDirExists 793 } 794 795 err = f.copyOrMove(ctx, "move", srcPath, dstPath, false) 796 797 if err != nil { 798 return errors.Wrap(err, "couldn't move directory") 799 } 800 return nil 801 } 802 803 // PublicLink generates a public link to the remote path (usually readable by anyone) 804 func (f *Fs) PublicLink(ctx context.Context, remote string) (link string, err error) { 805 var path string 806 if f.opt.Unlink { 807 path = "/resources/unpublish" 808 } else { 809 path = "/resources/publish" 810 } 811 opts := rest.Opts{ 812 Method: "PUT", 813 Path: f.opt.Enc.FromStandardPath(path), 814 Parameters: url.Values{}, 815 NoResponse: true, 816 } 817 818 opts.Parameters.Set("path", f.opt.Enc.FromStandardPath(f.filePath(remote))) 819 820 var resp *http.Response 821 err = f.pacer.Call(func() (bool, error) { 822 resp, err = f.srv.Call(ctx, &opts) 823 return shouldRetry(resp, err) 824 }) 825 826 if apiErr, ok := err.(*api.ErrorResponse); ok { 827 // does not exist 828 if apiErr.ErrorName == "DiskNotFoundError" { 829 return "", fs.ErrorObjectNotFound 830 } 831 } 832 if err != nil { 833 if f.opt.Unlink { 834 return "", errors.Wrap(err, "couldn't remove public link") 835 } 836 return "", errors.Wrap(err, "couldn't create public link") 837 } 838 839 info, err := f.readMetaDataForPath(ctx, f.filePath(remote), &api.ResourceInfoRequestOptions{}) 840 if err != nil { 841 return "", err 842 } 843 844 if info.PublicURL == "" { 845 return "", errors.New("couldn't create public link - no link path received") 846 } 847 return info.PublicURL, nil 848 } 849 850 // CleanUp permanently deletes all trashed files/folders 851 func (f *Fs) CleanUp(ctx context.Context) (err error) { 852 var resp *http.Response 853 opts := rest.Opts{ 854 Method: "DELETE", 855 Path: "/trash/resources", 856 NoResponse: true, 857 } 858 859 err = f.pacer.Call(func() (bool, error) { 860 resp, err = f.srv.Call(ctx, &opts) 861 return shouldRetry(resp, err) 862 }) 863 return err 864 } 865 866 // About gets quota information 867 func (f *Fs) About(ctx context.Context) (*fs.Usage, error) { 868 opts := rest.Opts{ 869 Method: "GET", 870 Path: "/", 871 } 872 873 var resp *http.Response 874 var info api.DiskInfo 875 var err error 876 err = f.pacer.Call(func() (bool, error) { 877 resp, err = f.srv.CallJSON(ctx, &opts, nil, &info) 878 return shouldRetry(resp, err) 879 }) 880 881 if err != nil { 882 return nil, err 883 } 884 885 usage := &fs.Usage{ 886 Total: fs.NewUsageValue(info.TotalSpace), 887 Used: fs.NewUsageValue(info.UsedSpace), 888 Free: fs.NewUsageValue(info.TotalSpace - info.UsedSpace), 889 } 890 return usage, nil 891 } 892 893 // ------------------------------------------------------------ 894 895 // Fs returns the parent Fs 896 func (o *Object) Fs() fs.Info { 897 return o.fs 898 } 899 900 // Return a string version 901 func (o *Object) String() string { 902 if o == nil { 903 return "<nil>" 904 } 905 return o.remote 906 } 907 908 // Remote returns the remote path 909 func (o *Object) Remote() string { 910 return o.remote 911 } 912 913 // Returns the full remote path for the object 914 func (o *Object) filePath() string { 915 return o.fs.filePath(o.remote) 916 } 917 918 // setMetaData sets the fs data from a storage.Object 919 func (o *Object) setMetaData(info *api.ResourceInfoResponse) (err error) { 920 o.hasMetaData = true 921 o.size = info.Size 922 o.md5sum = info.Md5 923 o.mimeType = info.MimeType 924 925 var modTimeString string 926 modTimeObj, ok := info.CustomProperties["rclone_modified"] 927 if ok { 928 // read modTime from rclone_modified custom_property of object 929 modTimeString, ok = modTimeObj.(string) 930 } 931 if !ok { 932 // read modTime from Modified property of object as a fallback 933 modTimeString = info.Modified 934 } 935 t, err := time.Parse(time.RFC3339Nano, modTimeString) 936 if err != nil { 937 return errors.Wrapf(err, "failed to parse modtime from %q", modTimeString) 938 } 939 o.modTime = t 940 return nil 941 } 942 943 // readMetaData reads ands sets the new metadata for a storage.Object 944 func (o *Object) readMetaData(ctx context.Context) (err error) { 945 if o.hasMetaData { 946 return nil 947 } 948 info, err := o.fs.readMetaDataForPath(ctx, o.filePath(), &api.ResourceInfoRequestOptions{}) 949 if err != nil { 950 return err 951 } 952 if info.ResourceType != "file" { 953 return fs.ErrorNotAFile 954 } 955 return o.setMetaData(info) 956 } 957 958 // ModTime returns the modification time of the object 959 // 960 // It attempts to read the objects mtime and if that isn't present the 961 // LastModified returned in the http headers 962 func (o *Object) ModTime(ctx context.Context) time.Time { 963 err := o.readMetaData(ctx) 964 if err != nil { 965 fs.Logf(o, "Failed to read metadata: %v", err) 966 return time.Now() 967 } 968 return o.modTime 969 } 970 971 // Size returns the size of an object in bytes 972 func (o *Object) Size() int64 { 973 ctx := context.TODO() 974 err := o.readMetaData(ctx) 975 if err != nil { 976 fs.Logf(o, "Failed to read metadata: %v", err) 977 return 0 978 } 979 return o.size 980 } 981 982 // Hash returns the Md5sum of an object returning a lowercase hex string 983 func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) { 984 if t != hash.MD5 { 985 return "", hash.ErrUnsupported 986 } 987 return o.md5sum, nil 988 } 989 990 // Storable returns whether this object is storable 991 func (o *Object) Storable() bool { 992 return true 993 } 994 995 func (o *Object) setCustomProperty(ctx context.Context, property string, value string) (err error) { 996 var resp *http.Response 997 opts := rest.Opts{ 998 Method: "PATCH", 999 Path: "/resources", 1000 Parameters: url.Values{}, 1001 NoResponse: true, 1002 } 1003 1004 opts.Parameters.Set("path", o.fs.opt.Enc.FromStandardPath(o.filePath())) 1005 rcm := map[string]interface{}{ 1006 property: value, 1007 } 1008 cpr := api.CustomPropertyResponse{CustomProperties: rcm} 1009 1010 err = o.fs.pacer.Call(func() (bool, error) { 1011 resp, err = o.fs.srv.CallJSON(ctx, &opts, &cpr, nil) 1012 return shouldRetry(resp, err) 1013 }) 1014 return err 1015 } 1016 1017 // SetModTime sets the modification time of the local fs object 1018 // 1019 // Commits the datastore 1020 func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error { 1021 // set custom_property 'rclone_modified' of object to modTime 1022 err := o.setCustomProperty(ctx, "rclone_modified", modTime.Format(time.RFC3339Nano)) 1023 if err != nil { 1024 return err 1025 } 1026 o.modTime = modTime 1027 return nil 1028 } 1029 1030 // Open an object for read 1031 func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) { 1032 // prepare download 1033 var resp *http.Response 1034 var dl api.AsyncInfo 1035 opts := rest.Opts{ 1036 Method: "GET", 1037 Path: "/resources/download", 1038 Parameters: url.Values{}, 1039 } 1040 1041 opts.Parameters.Set("path", o.fs.opt.Enc.FromStandardPath(o.filePath())) 1042 1043 err = o.fs.pacer.Call(func() (bool, error) { 1044 resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &dl) 1045 return shouldRetry(resp, err) 1046 }) 1047 1048 if err != nil { 1049 return nil, err 1050 } 1051 1052 // perform the download 1053 opts = rest.Opts{ 1054 RootURL: dl.HRef, 1055 Method: "GET", 1056 Options: options, 1057 } 1058 err = o.fs.pacer.Call(func() (bool, error) { 1059 resp, err = o.fs.srv.Call(ctx, &opts) 1060 return shouldRetry(resp, err) 1061 }) 1062 if err != nil { 1063 return nil, err 1064 } 1065 return resp.Body, err 1066 } 1067 1068 func (o *Object) upload(ctx context.Context, in io.Reader, overwrite bool, mimeType string, options ...fs.OpenOption) (err error) { 1069 // prepare upload 1070 var resp *http.Response 1071 var ur api.AsyncInfo 1072 opts := rest.Opts{ 1073 Method: "GET", 1074 Path: "/resources/upload", 1075 Parameters: url.Values{}, 1076 Options: options, 1077 } 1078 1079 opts.Parameters.Set("path", o.fs.opt.Enc.FromStandardPath(o.filePath())) 1080 opts.Parameters.Set("overwrite", strconv.FormatBool(overwrite)) 1081 1082 err = o.fs.pacer.Call(func() (bool, error) { 1083 resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &ur) 1084 return shouldRetry(resp, err) 1085 }) 1086 1087 if err != nil { 1088 return err 1089 } 1090 1091 // perform the actual upload 1092 opts = rest.Opts{ 1093 RootURL: ur.HRef, 1094 Method: "PUT", 1095 ContentType: mimeType, 1096 Body: in, 1097 NoResponse: true, 1098 } 1099 1100 err = o.fs.pacer.Call(func() (bool, error) { 1101 resp, err = o.fs.srv.Call(ctx, &opts) 1102 return shouldRetry(resp, err) 1103 }) 1104 1105 return err 1106 } 1107 1108 // Update the already existing object 1109 // 1110 // Copy the reader into the object updating modTime and size 1111 // 1112 // The new object may have been created if an error is returned 1113 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error { 1114 in1 := readers.NewCountingReader(in) 1115 modTime := src.ModTime(ctx) 1116 remote := o.filePath() 1117 1118 //create full path to file before upload. 1119 err := o.fs.mkParentDirs(ctx, remote) 1120 if err != nil { 1121 return err 1122 } 1123 1124 //upload file 1125 err = o.upload(ctx, in1, true, fs.MimeType(ctx, src), options...) 1126 if err != nil { 1127 return err 1128 } 1129 1130 //if file uploaded successfully then return metadata 1131 o.modTime = modTime 1132 o.md5sum = "" // according to unit tests after put the md5 is empty. 1133 o.size = int64(in1.BytesRead()) // better solution o.readMetaData() ? 1134 //and set modTime of uploaded file 1135 err = o.SetModTime(ctx, modTime) 1136 1137 return err 1138 } 1139 1140 // Remove an object 1141 func (o *Object) Remove(ctx context.Context) error { 1142 return o.fs.delete(ctx, o.filePath(), false) 1143 } 1144 1145 // MimeType of an Object if known, "" otherwise 1146 func (o *Object) MimeType(ctx context.Context) string { 1147 return o.mimeType 1148 } 1149 1150 // Check the interfaces are satisfied 1151 var ( 1152 _ fs.Fs = (*Fs)(nil) 1153 _ fs.Purger = (*Fs)(nil) 1154 _ fs.Copier = (*Fs)(nil) 1155 _ fs.Mover = (*Fs)(nil) 1156 _ fs.DirMover = (*Fs)(nil) 1157 _ fs.PublicLinker = (*Fs)(nil) 1158 _ fs.CleanUpper = (*Fs)(nil) 1159 _ fs.Abouter = (*Fs)(nil) 1160 _ fs.Object = (*Object)(nil) 1161 _ fs.MimeTyper = (*Object)(nil) 1162 )