github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/imagekit/imagekit.go (about) 1 // Package imagekit provides an interface to the ImageKit.io media library. 2 package imagekit 3 4 import ( 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "math" 10 "net/http" 11 "path" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/rclone/rclone/backend/imagekit/client" 17 "github.com/rclone/rclone/fs" 18 "github.com/rclone/rclone/fs/config" 19 "github.com/rclone/rclone/fs/config/configmap" 20 "github.com/rclone/rclone/fs/config/configstruct" 21 "github.com/rclone/rclone/fs/hash" 22 "github.com/rclone/rclone/lib/encoder" 23 "github.com/rclone/rclone/lib/pacer" 24 "github.com/rclone/rclone/lib/readers" 25 "github.com/rclone/rclone/lib/version" 26 ) 27 28 const ( 29 minSleep = 1 * time.Millisecond 30 maxSleep = 100 * time.Millisecond 31 decayConstant = 2 32 ) 33 34 var systemMetadataInfo = map[string]fs.MetadataHelp{ 35 "btime": { 36 Help: "Time of file birth (creation) read from Last-Modified header", 37 Type: "RFC 3339", 38 Example: "2006-01-02T15:04:05.999999999Z07:00", 39 ReadOnly: true, 40 }, 41 "size": { 42 Help: "Size of the object in bytes", 43 Type: "int64", 44 ReadOnly: true, 45 }, 46 "file-type": { 47 Help: "Type of the file", 48 Type: "string", 49 Example: "image", 50 ReadOnly: true, 51 }, 52 "height": { 53 Help: "Height of the image or video in pixels", 54 Type: "int", 55 ReadOnly: true, 56 }, 57 "width": { 58 Help: "Width of the image or video in pixels", 59 Type: "int", 60 ReadOnly: true, 61 }, 62 "has-alpha": { 63 Help: "Whether the image has alpha channel or not", 64 Type: "bool", 65 ReadOnly: true, 66 }, 67 "tags": { 68 Help: "Tags associated with the file", 69 Type: "string", 70 Example: "tag1,tag2", 71 ReadOnly: true, 72 }, 73 "google-tags": { 74 Help: "AI generated tags by Google Cloud Vision associated with the image", 75 Type: "string", 76 Example: "tag1,tag2", 77 ReadOnly: true, 78 }, 79 "aws-tags": { 80 Help: "AI generated tags by AWS Rekognition associated with the image", 81 Type: "string", 82 Example: "tag1,tag2", 83 ReadOnly: true, 84 }, 85 "is-private-file": { 86 Help: "Whether the file is private or not", 87 Type: "bool", 88 ReadOnly: true, 89 }, 90 "custom-coordinates": { 91 Help: "Custom coordinates of the file", 92 Type: "string", 93 Example: "0,0,100,100", 94 ReadOnly: true, 95 }, 96 } 97 98 // Register with Fs 99 func init() { 100 fs.Register(&fs.RegInfo{ 101 Name: "imagekit", 102 Description: "ImageKit.io", 103 NewFs: NewFs, 104 MetadataInfo: &fs.MetadataInfo{ 105 System: systemMetadataInfo, 106 Help: `Any metadata supported by the underlying remote is read and written.`, 107 }, 108 Options: []fs.Option{ 109 { 110 Name: "endpoint", 111 Help: "You can find your ImageKit.io URL endpoint in your [dashboard](https://imagekit.io/dashboard/developer/api-keys)", 112 Required: true, 113 }, 114 { 115 Name: "public_key", 116 Help: "You can find your ImageKit.io public key in your [dashboard](https://imagekit.io/dashboard/developer/api-keys)", 117 Required: true, 118 Sensitive: true, 119 }, 120 { 121 Name: "private_key", 122 Help: "You can find your ImageKit.io private key in your [dashboard](https://imagekit.io/dashboard/developer/api-keys)", 123 Required: true, 124 Sensitive: true, 125 }, 126 { 127 Name: "only_signed", 128 Help: "If you have configured `Restrict unsigned image URLs` in your dashboard settings, set this to true.", 129 Default: false, 130 Advanced: true, 131 }, 132 { 133 Name: "versions", 134 Help: "Include old versions in directory listings.", 135 Default: false, 136 Advanced: true, 137 }, 138 { 139 Name: "upload_tags", 140 Help: "Tags to add to the uploaded files, e.g. \"tag1,tag2\".", 141 Default: "", 142 Advanced: true, 143 }, 144 { 145 Name: config.ConfigEncoding, 146 Help: config.ConfigEncodingHelp, 147 Advanced: true, 148 Default: (encoder.EncodeZero | 149 encoder.EncodeSlash | 150 encoder.EncodeQuestion | 151 encoder.EncodeHashPercent | 152 encoder.EncodeCtl | 153 encoder.EncodeDel | 154 encoder.EncodeDot | 155 encoder.EncodeDoubleQuote | 156 encoder.EncodePercent | 157 encoder.EncodeBackSlash | 158 encoder.EncodeDollar | 159 encoder.EncodeLtGt | 160 encoder.EncodeSquareBracket | 161 encoder.EncodeInvalidUtf8), 162 }, 163 }, 164 }) 165 } 166 167 // Options defines the configuration for this backend 168 type Options struct { 169 Endpoint string `config:"endpoint"` 170 PublicKey string `config:"public_key"` 171 PrivateKey string `config:"private_key"` 172 OnlySigned bool `config:"only_signed"` 173 Versions bool `config:"versions"` 174 Enc encoder.MultiEncoder `config:"encoding"` 175 } 176 177 // Fs represents a remote to ImageKit 178 type Fs struct { 179 name string // name of remote 180 root string // root path 181 opt Options // parsed options 182 features *fs.Features // optional features 183 ik *client.ImageKit // ImageKit client 184 pacer *fs.Pacer // pacer for API calls 185 } 186 187 // Object describes a ImageKit file 188 type Object struct { 189 fs *Fs // The Fs this object is part of 190 remote string // The remote path 191 filePath string // The path to the file 192 contentType string // The content type of the object if known - may be "" 193 timestamp time.Time // The timestamp of the object if known - may be zero 194 file client.File // The media file if known - may be nil 195 versionID string // If present this points to an object version 196 } 197 198 // NewFs constructs an Fs from the path, container:path 199 func NewFs(ctx context.Context, name string, root string, m configmap.Mapper) (fs.Fs, error) { 200 opt := new(Options) 201 err := configstruct.Set(m, opt) 202 203 if err != nil { 204 return nil, err 205 } 206 207 ik, err := client.New(ctx, client.NewParams{ 208 URLEndpoint: opt.Endpoint, 209 PublicKey: opt.PublicKey, 210 PrivateKey: opt.PrivateKey, 211 }) 212 213 if err != nil { 214 return nil, err 215 } 216 217 f := &Fs{ 218 name: name, 219 opt: *opt, 220 ik: ik, 221 pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))), 222 } 223 224 f.root = path.Join("/", root) 225 226 f.features = (&fs.Features{ 227 CaseInsensitive: false, 228 DuplicateFiles: false, 229 ReadMimeType: true, 230 WriteMimeType: false, 231 CanHaveEmptyDirectories: true, 232 BucketBased: false, 233 ServerSideAcrossConfigs: false, 234 IsLocal: false, 235 SlowHash: true, 236 ReadMetadata: true, 237 WriteMetadata: false, 238 UserMetadata: false, 239 FilterAware: true, 240 PartialUploads: false, 241 NoMultiThreading: false, 242 }).Fill(ctx, f) 243 244 if f.root != "/" { 245 246 r := f.root 247 248 folderPath := f.EncodePath(r[:strings.LastIndex(r, "/")+1]) 249 fileName := f.EncodeFileName(r[strings.LastIndex(r, "/")+1:]) 250 251 file := f.getFileByName(ctx, folderPath, fileName) 252 253 if file != nil { 254 newRoot := path.Dir(f.root) 255 f.root = newRoot 256 return f, fs.ErrorIsFile 257 } 258 259 } 260 return f, nil 261 } 262 263 // Name of the remote (as passed into NewFs) 264 func (f *Fs) Name() string { 265 return f.name 266 } 267 268 // Root of the remote (as passed into NewFs) 269 func (f *Fs) Root() string { 270 return strings.TrimLeft(f.root, "/") 271 } 272 273 // String returns a description of the FS 274 func (f *Fs) String() string { 275 return fmt.Sprintf("FS imagekit: %s", f.root) 276 } 277 278 // Precision of the ModTimes in this Fs 279 func (f *Fs) Precision() time.Duration { 280 return fs.ModTimeNotSupported 281 } 282 283 // Hashes returns the supported hash types of the filesystem. 284 func (f *Fs) Hashes() hash.Set { 285 return hash.NewHashSet() 286 } 287 288 // Features returns the optional features of this Fs. 289 func (f *Fs) Features() *fs.Features { 290 return f.features 291 } 292 293 // List the objects and directories in dir into entries. The 294 // entries can be returned in any order but should be for a 295 // complete directory. 296 // 297 // dir should be "" to list the root, and should not have 298 // trailing slashes. 299 // 300 // This should return ErrDirNotFound if the directory isn't 301 // found. 302 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 303 304 remote := path.Join(f.root, dir) 305 306 remote = f.EncodePath(remote) 307 308 if remote != "/" { 309 parentFolderPath, folderName := path.Split(remote) 310 folderExists, err := f.getFolderByName(ctx, parentFolderPath, folderName) 311 312 if err != nil { 313 return make(fs.DirEntries, 0), err 314 } 315 316 if folderExists == nil { 317 return make(fs.DirEntries, 0), fs.ErrorDirNotFound 318 } 319 } 320 321 folders, folderError := f.getFolders(ctx, remote) 322 323 if folderError != nil { 324 return make(fs.DirEntries, 0), folderError 325 } 326 327 files, fileError := f.getFiles(ctx, remote, f.opt.Versions) 328 329 if fileError != nil { 330 return make(fs.DirEntries, 0), fileError 331 } 332 333 res := make([]fs.DirEntry, 0, len(folders)+len(files)) 334 335 for _, folder := range folders { 336 folderPath := f.DecodePath(strings.TrimLeft(strings.Replace(folder.FolderPath, f.EncodePath(f.root), "", 1), "/")) 337 res = append(res, fs.NewDir(folderPath, folder.UpdatedAt)) 338 } 339 340 for _, file := range files { 341 res = append(res, f.newObject(ctx, remote, file)) 342 } 343 344 return res, nil 345 } 346 347 func (f *Fs) newObject(ctx context.Context, remote string, file client.File) *Object { 348 remoteFile := strings.TrimLeft(strings.Replace(file.FilePath, f.EncodePath(f.root), "", 1), "/") 349 350 folderPath, fileName := path.Split(remoteFile) 351 352 folderPath = f.DecodePath(folderPath) 353 fileName = f.DecodeFileName(fileName) 354 355 remoteFile = path.Join(folderPath, fileName) 356 357 if file.Type == "file-version" { 358 remoteFile = version.Add(remoteFile, file.UpdatedAt) 359 360 return &Object{ 361 fs: f, 362 remote: remoteFile, 363 filePath: file.FilePath, 364 contentType: file.Mime, 365 timestamp: file.UpdatedAt, 366 file: file, 367 versionID: file.VersionInfo["id"], 368 } 369 } 370 371 return &Object{ 372 fs: f, 373 remote: remoteFile, 374 filePath: file.FilePath, 375 contentType: file.Mime, 376 timestamp: file.UpdatedAt, 377 file: file, 378 } 379 } 380 381 // NewObject finds the Object at remote. If it can't be found 382 // it returns the error ErrorObjectNotFound. 383 // 384 // If remote points to a directory then it should return 385 // ErrorIsDir if possible without doing any extra work, 386 // otherwise ErrorObjectNotFound. 387 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 388 r := path.Join(f.root, remote) 389 390 folderPath, fileName := path.Split(r) 391 392 folderPath = f.EncodePath(folderPath) 393 fileName = f.EncodeFileName(fileName) 394 395 isFolder, err := f.getFolderByName(ctx, folderPath, fileName) 396 397 if err != nil { 398 return nil, err 399 } 400 401 if isFolder != nil { 402 return nil, fs.ErrorIsDir 403 } 404 405 file := f.getFileByName(ctx, folderPath, fileName) 406 407 if file == nil { 408 return nil, fs.ErrorObjectNotFound 409 } 410 411 return f.newObject(ctx, r, *file), nil 412 } 413 414 // Put in to the remote path with the modTime given of the given size 415 // 416 // When called from outside an Fs by rclone, src.Size() will always be >= 0. 417 // But for unknown-sized objects (indicated by src.Size() == -1), Put should either 418 // return an error or upload it properly (rather than e.g. calling panic). 419 // 420 // May create the object even if it returns an error - if so 421 // will return the object and the error, otherwise will return 422 // nil and the error 423 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 424 return uploadFile(ctx, f, in, src.Remote(), options...) 425 } 426 427 // Mkdir makes the directory (container, bucket) 428 // 429 // Shouldn't return an error if it already exists 430 func (f *Fs) Mkdir(ctx context.Context, dir string) (err error) { 431 remote := path.Join(f.root, dir) 432 parentFolderPath, folderName := path.Split(remote) 433 434 parentFolderPath = f.EncodePath(parentFolderPath) 435 folderName = f.EncodeFileName(folderName) 436 437 err = f.pacer.Call(func() (bool, error) { 438 var res *http.Response 439 res, err = f.ik.CreateFolder(ctx, client.CreateFolderParam{ 440 ParentFolderPath: parentFolderPath, 441 FolderName: folderName, 442 }) 443 444 return f.shouldRetry(ctx, res, err) 445 }) 446 447 return err 448 } 449 450 // Rmdir removes the directory (container, bucket) if empty 451 // 452 // Return an error if it doesn't exist or isn't empty 453 func (f *Fs) Rmdir(ctx context.Context, dir string) (err error) { 454 455 entries, err := f.List(ctx, dir) 456 457 if err != nil { 458 return err 459 } 460 461 if len(entries) > 0 { 462 return errors.New("directory is not empty") 463 } 464 465 err = f.pacer.Call(func() (bool, error) { 466 var res *http.Response 467 res, err = f.ik.DeleteFolder(ctx, client.DeleteFolderParam{ 468 FolderPath: f.EncodePath(path.Join(f.root, dir)), 469 }) 470 471 if res.StatusCode == http.StatusNotFound { 472 return false, fs.ErrorDirNotFound 473 } 474 475 return f.shouldRetry(ctx, res, err) 476 }) 477 478 return err 479 } 480 481 // Purge deletes all the files and the container 482 // 483 // Optional interface: Only implement this if you have a way of 484 // deleting all the files quicker than just running Remove() on the 485 // result of List() 486 func (f *Fs) Purge(ctx context.Context, dir string) (err error) { 487 488 remote := path.Join(f.root, dir) 489 490 err = f.pacer.Call(func() (bool, error) { 491 var res *http.Response 492 res, err = f.ik.DeleteFolder(ctx, client.DeleteFolderParam{ 493 FolderPath: f.EncodePath(remote), 494 }) 495 496 if res.StatusCode == http.StatusNotFound { 497 return false, fs.ErrorDirNotFound 498 } 499 500 return f.shouldRetry(ctx, res, err) 501 }) 502 503 return err 504 } 505 506 // PublicLink generates a public link to the remote path (usually readable by anyone) 507 func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) { 508 509 duration := time.Duration(math.Abs(float64(expire))) 510 511 expireSeconds := duration.Seconds() 512 513 fileRemote := path.Join(f.root, remote) 514 515 folderPath, fileName := path.Split(fileRemote) 516 folderPath = f.EncodePath(folderPath) 517 fileName = f.EncodeFileName(fileName) 518 519 file := f.getFileByName(ctx, folderPath, fileName) 520 521 if file == nil { 522 return "", fs.ErrorObjectNotFound 523 } 524 525 // Pacer not needed as this doesn't use the API 526 url, err := f.ik.URL(client.URLParam{ 527 Src: file.URL, 528 Signed: *file.IsPrivateFile || f.opt.OnlySigned, 529 ExpireSeconds: int64(expireSeconds), 530 QueryParameters: map[string]string{ 531 "updatedAt": file.UpdatedAt.String(), 532 }, 533 }) 534 535 if err != nil { 536 return "", err 537 } 538 539 return url, nil 540 } 541 542 // Fs returns read only access to the Fs that this object is part of 543 func (o *Object) Fs() fs.Info { 544 return o.fs 545 } 546 547 // Hash returns the selected checksum of the file 548 // If no checksum is available it returns "" 549 func (o *Object) Hash(ctx context.Context, ty hash.Type) (string, error) { 550 return "", hash.ErrUnsupported 551 } 552 553 // Storable says whether this object can be stored 554 func (o *Object) Storable() bool { 555 return true 556 } 557 558 // String returns a description of the Object 559 func (o *Object) String() string { 560 if o == nil { 561 return "<nil>" 562 } 563 return o.file.Name 564 } 565 566 // Remote returns the remote path 567 func (o *Object) Remote() string { 568 return o.remote 569 } 570 571 // ModTime returns the modification date of the file 572 // It should return a best guess if one isn't available 573 func (o *Object) ModTime(context.Context) time.Time { 574 return o.file.UpdatedAt 575 } 576 577 // Size returns the size of the file 578 func (o *Object) Size() int64 { 579 return int64(o.file.Size) 580 } 581 582 // MimeType returns the MIME type of the file 583 func (o *Object) MimeType(context.Context) string { 584 return o.contentType 585 } 586 587 // Open opens the file for read. Call Close() on the returned io.ReadCloser 588 func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) { 589 // Offset and Count for range download 590 var offset int64 591 var count int64 592 593 fs.FixRangeOption(options, -1) 594 partialContent := false 595 for _, option := range options { 596 switch x := option.(type) { 597 case *fs.RangeOption: 598 offset, count = x.Decode(-1) 599 partialContent = true 600 case *fs.SeekOption: 601 offset = x.Offset 602 partialContent = true 603 default: 604 if option.Mandatory() { 605 fs.Logf(o, "Unsupported mandatory option: %v", option) 606 } 607 } 608 } 609 610 // Pacer not needed as this doesn't use the API 611 url, err := o.fs.ik.URL(client.URLParam{ 612 Src: o.file.URL, 613 Signed: *o.file.IsPrivateFile || o.fs.opt.OnlySigned, 614 QueryParameters: map[string]string{ 615 "tr": "orig-true", 616 "updatedAt": o.file.UpdatedAt.String(), 617 }, 618 }) 619 620 if err != nil { 621 return nil, err 622 } 623 624 client := &http.Client{} 625 req, _ := http.NewRequest("GET", url, nil) 626 req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+count-1)) 627 resp, err := client.Do(req) 628 629 if err != nil { 630 return nil, err 631 } 632 633 end := resp.ContentLength 634 635 if partialContent && resp.StatusCode == http.StatusOK { 636 skip := offset 637 638 if offset < 0 { 639 skip = end + offset + 1 640 } 641 642 _, err = io.CopyN(io.Discard, resp.Body, skip) 643 if err != nil { 644 if resp != nil { 645 _ = resp.Body.Close() 646 } 647 return nil, err 648 } 649 650 return readers.NewLimitedReadCloser(resp.Body, end-skip), nil 651 } 652 653 return resp.Body, nil 654 } 655 656 // Update in to the object with the modTime given of the given size 657 // 658 // When called from outside an Fs by rclone, src.Size() will always be >= 0. 659 // But for unknown-sized objects (indicated by src.Size() == -1), Upload should either 660 // return an error or update the object properly (rather than e.g. calling panic). 661 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) { 662 663 srcRemote := o.Remote() 664 665 remote := path.Join(o.fs.root, srcRemote) 666 folderPath, fileName := path.Split(remote) 667 668 UseUniqueFileName := new(bool) 669 *UseUniqueFileName = false 670 671 var resp *client.UploadResult 672 673 err = o.fs.pacer.Call(func() (bool, error) { 674 var res *http.Response 675 res, resp, err = o.fs.ik.Upload(ctx, in, client.UploadParam{ 676 FileName: fileName, 677 Folder: folderPath, 678 IsPrivateFile: o.file.IsPrivateFile, 679 }) 680 681 return o.fs.shouldRetry(ctx, res, err) 682 }) 683 684 if err != nil { 685 return err 686 } 687 688 fileID := resp.FileID 689 690 _, file, err := o.fs.ik.File(ctx, fileID) 691 692 if err != nil { 693 return err 694 } 695 696 o.file = *file 697 698 return nil 699 } 700 701 // Remove this object 702 func (o *Object) Remove(ctx context.Context) (err error) { 703 err = o.fs.pacer.Call(func() (bool, error) { 704 var res *http.Response 705 res, err = o.fs.ik.DeleteFile(ctx, o.file.FileID) 706 707 return o.fs.shouldRetry(ctx, res, err) 708 }) 709 710 return err 711 } 712 713 // SetModTime sets the metadata on the object to set the modification date 714 func (o *Object) SetModTime(ctx context.Context, t time.Time) error { 715 return fs.ErrorCantSetModTime 716 } 717 718 func uploadFile(ctx context.Context, f *Fs, in io.Reader, srcRemote string, options ...fs.OpenOption) (fs.Object, error) { 719 remote := path.Join(f.root, srcRemote) 720 folderPath, fileName := path.Split(remote) 721 722 folderPath = f.EncodePath(folderPath) 723 fileName = f.EncodeFileName(fileName) 724 725 UseUniqueFileName := new(bool) 726 *UseUniqueFileName = false 727 728 err := f.pacer.Call(func() (bool, error) { 729 var res *http.Response 730 var err error 731 res, _, err = f.ik.Upload(ctx, in, client.UploadParam{ 732 FileName: fileName, 733 Folder: folderPath, 734 IsPrivateFile: &f.opt.OnlySigned, 735 }) 736 737 return f.shouldRetry(ctx, res, err) 738 }) 739 740 if err != nil { 741 return nil, err 742 } 743 744 return f.NewObject(ctx, srcRemote) 745 } 746 747 // Metadata returns the metadata for the object 748 func (o *Object) Metadata(ctx context.Context) (metadata fs.Metadata, err error) { 749 750 metadata.Set("btime", o.file.CreatedAt.Format(time.RFC3339)) 751 metadata.Set("size", strconv.FormatUint(o.file.Size, 10)) 752 metadata.Set("file-type", o.file.FileType) 753 metadata.Set("height", strconv.Itoa(o.file.Height)) 754 metadata.Set("width", strconv.Itoa(o.file.Width)) 755 metadata.Set("has-alpha", strconv.FormatBool(o.file.HasAlpha)) 756 757 for k, v := range o.file.EmbeddedMetadata { 758 metadata.Set(k, fmt.Sprint(v)) 759 } 760 761 if o.file.Tags != nil { 762 metadata.Set("tags", strings.Join(o.file.Tags, ",")) 763 } 764 765 if o.file.CustomCoordinates != nil { 766 metadata.Set("custom-coordinates", *o.file.CustomCoordinates) 767 } 768 769 if o.file.IsPrivateFile != nil { 770 metadata.Set("is-private-file", strconv.FormatBool(*o.file.IsPrivateFile)) 771 } 772 773 if o.file.AITags != nil { 774 googleTags := []string{} 775 awsTags := []string{} 776 777 for _, tag := range o.file.AITags { 778 if tag.Source == "google-auto-tagging" { 779 googleTags = append(googleTags, tag.Name) 780 } else if tag.Source == "aws-auto-tagging" { 781 awsTags = append(awsTags, tag.Name) 782 } 783 } 784 785 if len(googleTags) > 0 { 786 metadata.Set("google-tags", strings.Join(googleTags, ",")) 787 } 788 789 if len(awsTags) > 0 { 790 metadata.Set("aws-tags", strings.Join(awsTags, ",")) 791 } 792 } 793 794 return metadata, nil 795 } 796 797 // Copy src to this remote using server-side move operations. 798 // 799 // This is stored with the remote path given. 800 // 801 // It returns the destination Object and a possible error. 802 // 803 // Will only be called if src.Fs().Name() == f.Name() 804 // 805 // If it isn't possible then return fs.ErrorCantMove 806 func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 807 srcObj, ok := src.(*Object) 808 if !ok { 809 return nil, fs.ErrorCantMove 810 } 811 812 file, err := srcObj.Open(ctx) 813 814 if err != nil { 815 return nil, err 816 } 817 818 return uploadFile(ctx, f, file, remote) 819 } 820 821 // Check the interfaces are satisfied. 822 var ( 823 _ fs.Fs = &Fs{} 824 _ fs.Purger = &Fs{} 825 _ fs.PublicLinker = &Fs{} 826 _ fs.Object = &Object{} 827 _ fs.Copier = &Fs{} 828 )