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