github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/smb/smb.go (about) 1 // Package smb provides an interface to SMB servers 2 package smb 3 4 import ( 5 "context" 6 "fmt" 7 "io" 8 "os" 9 "path" 10 "strings" 11 "sync" 12 "sync/atomic" 13 "time" 14 15 "github.com/rclone/rclone/fs" 16 "github.com/rclone/rclone/fs/config" 17 "github.com/rclone/rclone/fs/config/configmap" 18 "github.com/rclone/rclone/fs/config/configstruct" 19 "github.com/rclone/rclone/fs/hash" 20 "github.com/rclone/rclone/lib/bucket" 21 "github.com/rclone/rclone/lib/encoder" 22 "github.com/rclone/rclone/lib/env" 23 "github.com/rclone/rclone/lib/pacer" 24 "github.com/rclone/rclone/lib/readers" 25 ) 26 27 const ( 28 minSleep = 100 * time.Millisecond 29 maxSleep = 2 * time.Second 30 decayConstant = 2 // bigger for slower decay, exponential 31 ) 32 33 var ( 34 currentUser = env.CurrentUser() 35 ) 36 37 // Register with Fs 38 func init() { 39 fs.Register(&fs.RegInfo{ 40 Name: "smb", 41 Description: "SMB / CIFS", 42 NewFs: NewFs, 43 44 Options: []fs.Option{{ 45 Name: "host", 46 Help: "SMB server hostname to connect to.\n\nE.g. \"example.com\".", 47 Required: true, 48 Sensitive: true, 49 }, { 50 Name: "user", 51 Help: "SMB username.", 52 Default: currentUser, 53 Sensitive: true, 54 }, { 55 Name: "port", 56 Help: "SMB port number.", 57 Default: 445, 58 }, { 59 Name: "pass", 60 Help: "SMB password.", 61 IsPassword: true, 62 }, { 63 Name: "domain", 64 Help: "Domain name for NTLM authentication.", 65 Default: "WORKGROUP", 66 Sensitive: true, 67 }, { 68 Name: "spn", 69 Help: `Service principal name. 70 71 Rclone presents this name to the server. Some servers use this as further 72 authentication, and it often needs to be set for clusters. For example: 73 74 cifs/remotehost:1020 75 76 Leave blank if not sure. 77 `, 78 Sensitive: true, 79 }, { 80 Name: "idle_timeout", 81 Default: fs.Duration(60 * time.Second), 82 Help: `Max time before closing idle connections. 83 84 If no connections have been returned to the connection pool in the time 85 given, rclone will empty the connection pool. 86 87 Set to 0 to keep connections indefinitely. 88 `, 89 Advanced: true, 90 }, { 91 Name: "hide_special_share", 92 Help: "Hide special shares (e.g. print$) which users aren't supposed to access.", 93 Default: true, 94 Advanced: true, 95 }, { 96 Name: "case_insensitive", 97 Help: "Whether the server is configured to be case-insensitive.\n\nAlways true on Windows shares.", 98 Default: true, 99 Advanced: true, 100 }, { 101 Name: config.ConfigEncoding, 102 Help: config.ConfigEncodingHelp, 103 Advanced: true, 104 Default: encoder.EncodeZero | 105 // path separator 106 encoder.EncodeSlash | 107 encoder.EncodeBackSlash | 108 // windows 109 encoder.EncodeWin | 110 encoder.EncodeCtl | 111 encoder.EncodeDot | 112 // the file turns into 8.3 names (and cannot be converted back) 113 encoder.EncodeRightSpace | 114 encoder.EncodeRightPeriod | 115 // 116 encoder.EncodeInvalidUtf8, 117 }, 118 }}) 119 } 120 121 // Options defines the configuration for this backend 122 type Options struct { 123 Host string `config:"host"` 124 Port string `config:"port"` 125 User string `config:"user"` 126 Pass string `config:"pass"` 127 Domain string `config:"domain"` 128 SPN string `config:"spn"` 129 HideSpecial bool `config:"hide_special_share"` 130 CaseInsensitive bool `config:"case_insensitive"` 131 IdleTimeout fs.Duration `config:"idle_timeout"` 132 133 Enc encoder.MultiEncoder `config:"encoding"` 134 } 135 136 // Fs represents a SMB remote 137 type Fs struct { 138 name string // name of this remote 139 root string // the path we are working on if any 140 opt Options // parsed config options 141 features *fs.Features // optional features 142 pacer *fs.Pacer // pacer for operations 143 144 sessions atomic.Int32 145 poolMu sync.Mutex 146 pool []*conn 147 drain *time.Timer // used to drain the pool when we stop using the connections 148 149 ctx context.Context 150 } 151 152 // Object describes a file at the server 153 type Object struct { 154 fs *Fs // reference to Fs 155 remote string // the remote path 156 statResult os.FileInfo 157 } 158 159 // NewFs constructs an Fs from the path 160 func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) { 161 // Parse config into Options struct 162 opt := new(Options) 163 err := configstruct.Set(m, opt) 164 if err != nil { 165 return nil, err 166 } 167 168 root = strings.Trim(root, "/") 169 170 f := &Fs{ 171 name: name, 172 opt: *opt, 173 ctx: ctx, 174 root: root, 175 } 176 f.features = (&fs.Features{ 177 CaseInsensitive: opt.CaseInsensitive, 178 CanHaveEmptyDirectories: true, 179 BucketBased: true, 180 PartialUploads: true, 181 }).Fill(ctx, f) 182 183 f.pacer = fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))) 184 // set the pool drainer timer going 185 if opt.IdleTimeout > 0 { 186 f.drain = time.AfterFunc(time.Duration(opt.IdleTimeout), func() { _ = f.drainPool(ctx) }) 187 } 188 189 // test if the root exists as a file 190 share, dir := f.split("") 191 if share == "" || dir == "" { 192 return f, nil 193 } 194 cn, err := f.getConnection(ctx, share) 195 if err != nil { 196 return nil, err 197 } 198 stat, err := cn.smbShare.Stat(f.toSambaPath(dir)) 199 f.putConnection(&cn) 200 if err != nil { 201 // ignore stat error here 202 return f, nil 203 } 204 if !stat.IsDir() { 205 f.root, err = path.Dir(root), fs.ErrorIsFile 206 } 207 fs.Debugf(f, "Using root directory %q", f.root) 208 return f, err 209 } 210 211 // Name of the remote (as passed into NewFs) 212 func (f *Fs) Name() string { 213 return f.name 214 } 215 216 // Root of the remote (as passed into NewFs) 217 func (f *Fs) Root() string { 218 return f.root 219 } 220 221 // String converts this Fs to a string 222 func (f *Fs) String() string { 223 bucket, file := f.split("") 224 if bucket == "" { 225 return fmt.Sprintf("smb://%s@%s:%s/", f.opt.User, f.opt.Host, f.opt.Port) 226 } 227 return fmt.Sprintf("smb://%s@%s:%s/%s/%s", f.opt.User, f.opt.Host, f.opt.Port, bucket, file) 228 } 229 230 // Features returns the optional features of this Fs 231 func (f *Fs) Features() *fs.Features { 232 return f.features 233 } 234 235 // Hashes returns nothing as SMB itself doesn't have a way to tell checksums 236 func (f *Fs) Hashes() hash.Set { 237 return hash.NewHashSet() 238 } 239 240 // Precision returns the precision of mtime 241 func (f *Fs) Precision() time.Duration { 242 return time.Millisecond 243 } 244 245 // NewObject creates a new file object 246 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 247 share, path := f.split(remote) 248 return f.findObjectSeparate(ctx, share, path) 249 } 250 251 func (f *Fs) findObjectSeparate(ctx context.Context, share, path string) (fs.Object, error) { 252 if share == "" || path == "" { 253 return nil, fs.ErrorIsDir 254 } 255 cn, err := f.getConnection(ctx, share) 256 if err != nil { 257 return nil, err 258 } 259 stat, err := cn.smbShare.Stat(f.toSambaPath(path)) 260 f.putConnection(&cn) 261 if err != nil { 262 return nil, translateError(err, false) 263 } 264 if stat.IsDir() { 265 return nil, fs.ErrorIsDir 266 } 267 268 return f.makeEntry(share, path, stat), nil 269 } 270 271 // Mkdir creates a directory on the server 272 func (f *Fs) Mkdir(ctx context.Context, dir string) (err error) { 273 share, path := f.split(dir) 274 if share == "" || path == "" { 275 return nil 276 } 277 cn, err := f.getConnection(ctx, share) 278 if err != nil { 279 return err 280 } 281 err = cn.smbShare.MkdirAll(f.toSambaPath(path), 0o755) 282 f.putConnection(&cn) 283 return err 284 } 285 286 // Rmdir removes an empty directory on the server 287 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 288 share, path := f.split(dir) 289 if share == "" || path == "" { 290 return nil 291 } 292 cn, err := f.getConnection(ctx, share) 293 if err != nil { 294 return err 295 } 296 err = cn.smbShare.Remove(f.toSambaPath(path)) 297 f.putConnection(&cn) 298 return err 299 } 300 301 // Put uploads a file 302 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 303 o := &Object{ 304 fs: f, 305 remote: src.Remote(), 306 } 307 308 err := o.Update(ctx, in, src, options...) 309 if err == nil { 310 return o, nil 311 } 312 313 return nil, err 314 } 315 316 // PutStream uploads to the remote path with the modTime given of indeterminate size 317 // 318 // May create the object even if it returns an error - if so 319 // will return the object and the error, otherwise will return 320 // nil and the error 321 func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 322 o := &Object{ 323 fs: f, 324 remote: src.Remote(), 325 } 326 327 err := o.Update(ctx, in, src, options...) 328 if err == nil { 329 return o, nil 330 } 331 332 return nil, err 333 } 334 335 // Move src to this remote using server-side move operations. 336 // 337 // This is stored with the remote path given. 338 // 339 // It returns the destination Object and a possible error. 340 // 341 // Will only be called if src.Fs().Name() == f.Name() 342 // 343 // If it isn't possible then return fs.ErrorCantMove 344 func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (_ fs.Object, err error) { 345 dstShare, dstPath := f.split(remote) 346 srcObj, ok := src.(*Object) 347 if !ok { 348 fs.Debugf(src, "Can't move - not same remote type") 349 return nil, fs.ErrorCantMove 350 } 351 srcShare, srcPath := srcObj.split() 352 if dstShare != srcShare { 353 fs.Debugf(src, "Can't move - must be on the same share") 354 return nil, fs.ErrorCantMove 355 } 356 357 err = f.ensureDirectory(ctx, dstShare, dstPath) 358 if err != nil { 359 return nil, fmt.Errorf("failed to make parent directories: %w", err) 360 } 361 362 cn, err := f.getConnection(ctx, dstShare) 363 if err != nil { 364 return nil, err 365 } 366 err = cn.smbShare.Rename(f.toSambaPath(srcPath), f.toSambaPath(dstPath)) 367 f.putConnection(&cn) 368 if err != nil { 369 return nil, translateError(err, false) 370 } 371 return f.findObjectSeparate(ctx, dstShare, dstPath) 372 } 373 374 // DirMove moves src, srcRemote to this remote at dstRemote 375 // using server-side move operations. 376 // 377 // Will only be called if src.Fs().Name() == f.Name() 378 // 379 // If it isn't possible then return fs.ErrorCantDirMove 380 // 381 // If destination exists then return fs.ErrorDirExists 382 func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) (err error) { 383 dstShare, dstPath := f.split(dstRemote) 384 srcFs, ok := src.(*Fs) 385 if !ok { 386 fs.Debugf(src, "Can't move - not same remote type") 387 return fs.ErrorCantDirMove 388 } 389 srcShare, srcPath := srcFs.split(srcRemote) 390 if dstShare != srcShare { 391 fs.Debugf(src, "Can't move - must be on the same share") 392 return fs.ErrorCantDirMove 393 } 394 395 err = f.ensureDirectory(ctx, dstShare, dstPath) 396 if err != nil { 397 return fmt.Errorf("failed to make parent directories: %w", err) 398 } 399 400 cn, err := f.getConnection(ctx, dstShare) 401 if err != nil { 402 return err 403 } 404 defer f.putConnection(&cn) 405 406 _, err = cn.smbShare.Stat(dstPath) 407 if os.IsNotExist(err) { 408 err = cn.smbShare.Rename(f.toSambaPath(srcPath), f.toSambaPath(dstPath)) 409 return translateError(err, true) 410 } 411 return fs.ErrorDirExists 412 } 413 414 // List files and directories in a directory 415 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 416 share, _path := f.split(dir) 417 418 cn, err := f.getConnection(ctx, share) 419 if err != nil { 420 return nil, err 421 } 422 defer f.putConnection(&cn) 423 424 if share == "" { 425 shares, err := cn.smbSession.ListSharenames() 426 for _, shh := range shares { 427 shh = f.toNativePath(shh) 428 if strings.HasSuffix(shh, "$") && f.opt.HideSpecial { 429 continue 430 } 431 entries = append(entries, fs.NewDir(shh, time.Time{})) 432 } 433 return entries, err 434 } 435 436 dirents, err := cn.smbShare.ReadDir(f.toSambaPath(_path)) 437 if err != nil { 438 return entries, translateError(err, true) 439 } 440 for _, file := range dirents { 441 nfn := f.toNativePath(file.Name()) 442 if file.IsDir() { 443 entries = append(entries, fs.NewDir(path.Join(dir, nfn), file.ModTime())) 444 } else { 445 entries = append(entries, f.makeEntryRelative(share, _path, nfn, file)) 446 } 447 } 448 449 return entries, nil 450 } 451 452 // About returns things about remaining and used spaces 453 func (f *Fs) About(ctx context.Context) (_ *fs.Usage, err error) { 454 share, dir := f.split("/") 455 if share == "" { 456 // Just return empty info rather than an error if called on the root 457 return &fs.Usage{}, nil 458 } 459 dir = f.toSambaPath(dir) 460 461 cn, err := f.getConnection(ctx, share) 462 if err != nil { 463 return nil, err 464 } 465 stat, err := cn.smbShare.Statfs(dir) 466 f.putConnection(&cn) 467 if err != nil { 468 return nil, err 469 } 470 471 bs := int64(stat.BlockSize()) 472 usage := &fs.Usage{ 473 Total: fs.NewUsageValue(bs * int64(stat.TotalBlockCount())), 474 Used: fs.NewUsageValue(bs * int64(stat.TotalBlockCount()-stat.FreeBlockCount())), 475 Free: fs.NewUsageValue(bs * int64(stat.AvailableBlockCount())), 476 } 477 return usage, nil 478 } 479 480 // OpenWriterAt opens with a handle for random access writes 481 // 482 // Pass in the remote desired and the size if known. 483 // 484 // It truncates any existing object 485 func (f *Fs) OpenWriterAt(ctx context.Context, remote string, size int64) (fs.WriterAtCloser, error) { 486 var err error 487 o := &Object{ 488 fs: f, 489 remote: remote, 490 } 491 share, filename := o.split() 492 if share == "" || filename == "" { 493 return nil, fs.ErrorIsDir 494 } 495 496 err = o.fs.ensureDirectory(ctx, share, filename) 497 if err != nil { 498 return nil, fmt.Errorf("failed to make parent directories: %w", err) 499 } 500 501 filename = o.fs.toSambaPath(filename) 502 503 o.fs.addSession() // Show session in use 504 defer o.fs.removeSession() 505 506 cn, err := o.fs.getConnection(ctx, share) 507 if err != nil { 508 return nil, err 509 } 510 511 fl, err := cn.smbShare.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) 512 if err != nil { 513 return nil, fmt.Errorf("failed to open: %w", err) 514 } 515 516 return fl, nil 517 } 518 519 // Shutdown the backend, closing any background tasks and any 520 // cached connections. 521 func (f *Fs) Shutdown(ctx context.Context) error { 522 return f.drainPool(ctx) 523 } 524 525 func (f *Fs) makeEntry(share, _path string, stat os.FileInfo) *Object { 526 remote := path.Join(share, _path) 527 return &Object{ 528 fs: f, 529 remote: trimPathPrefix(remote, f.root), 530 statResult: stat, 531 } 532 } 533 534 func (f *Fs) makeEntryRelative(share, _path, relative string, stat os.FileInfo) *Object { 535 return f.makeEntry(share, path.Join(_path, relative), stat) 536 } 537 538 func (f *Fs) ensureDirectory(ctx context.Context, share, _path string) error { 539 dir := path.Dir(_path) 540 if dir == "." { 541 return nil 542 } 543 cn, err := f.getConnection(ctx, share) 544 if err != nil { 545 return err 546 } 547 err = cn.smbShare.MkdirAll(f.toSambaPath(dir), 0o755) 548 f.putConnection(&cn) 549 return err 550 } 551 552 /// Object 553 554 // Remote returns the remote path 555 func (o *Object) Remote() string { 556 return o.remote 557 } 558 559 // ModTime is the last modified time (read-only) 560 func (o *Object) ModTime(ctx context.Context) time.Time { 561 return o.statResult.ModTime() 562 } 563 564 // Size is the file length 565 func (o *Object) Size() int64 { 566 return o.statResult.Size() 567 } 568 569 // Fs returns the parent Fs 570 func (o *Object) Fs() fs.Info { 571 return o.fs 572 } 573 574 // Hash always returns empty value 575 func (o *Object) Hash(ctx context.Context, ty hash.Type) (string, error) { 576 return "", hash.ErrUnsupported 577 } 578 579 // Storable returns if this object is storable 580 func (o *Object) Storable() bool { 581 return true 582 } 583 584 // SetModTime sets modTime on a particular file 585 func (o *Object) SetModTime(ctx context.Context, t time.Time) (err error) { 586 share, reqDir := o.split() 587 if share == "" || reqDir == "" { 588 return fs.ErrorCantSetModTime 589 } 590 reqDir = o.fs.toSambaPath(reqDir) 591 592 cn, err := o.fs.getConnection(ctx, share) 593 if err != nil { 594 return err 595 } 596 defer o.fs.putConnection(&cn) 597 598 err = cn.smbShare.Chtimes(reqDir, t, t) 599 if err != nil { 600 return err 601 } 602 603 fi, err := cn.smbShare.Stat(reqDir) 604 if err == nil { 605 o.statResult = fi 606 } 607 return err 608 } 609 610 // Open an object for read 611 func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) { 612 share, filename := o.split() 613 if share == "" || filename == "" { 614 return nil, fs.ErrorIsDir 615 } 616 filename = o.fs.toSambaPath(filename) 617 618 var offset, limit int64 = 0, -1 619 for _, option := range options { 620 switch x := option.(type) { 621 case *fs.SeekOption: 622 offset = x.Offset 623 case *fs.RangeOption: 624 offset, limit = x.Decode(o.Size()) 625 default: 626 if option.Mandatory() { 627 fs.Logf(o, "Unsupported mandatory option: %v", option) 628 } 629 } 630 } 631 632 o.fs.addSession() // Show session in use 633 defer o.fs.removeSession() 634 635 cn, err := o.fs.getConnection(ctx, share) 636 if err != nil { 637 return nil, err 638 } 639 fl, err := cn.smbShare.OpenFile(filename, os.O_RDONLY, 0) 640 if err != nil { 641 o.fs.putConnection(&cn) 642 return nil, fmt.Errorf("failed to open: %w", err) 643 } 644 pos, err := fl.Seek(offset, io.SeekStart) 645 if err != nil { 646 o.fs.putConnection(&cn) 647 return nil, fmt.Errorf("failed to seek: %w", err) 648 } 649 if pos != offset { 650 o.fs.putConnection(&cn) 651 return nil, fmt.Errorf("failed to seek: wrong position (expected=%d, reported=%d)", offset, pos) 652 } 653 654 in = readers.NewLimitedReadCloser(fl, limit) 655 in = &boundReadCloser{ 656 rc: in, 657 close: func() error { 658 o.fs.putConnection(&cn) 659 return nil 660 }, 661 } 662 663 return in, nil 664 } 665 666 // Update the Object from in with modTime and size 667 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) { 668 share, filename := o.split() 669 if share == "" || filename == "" { 670 return fs.ErrorIsDir 671 } 672 673 err = o.fs.ensureDirectory(ctx, share, filename) 674 if err != nil { 675 return fmt.Errorf("failed to make parent directories: %w", err) 676 } 677 678 filename = o.fs.toSambaPath(filename) 679 680 o.fs.addSession() // Show session in use 681 defer o.fs.removeSession() 682 683 cn, err := o.fs.getConnection(ctx, share) 684 if err != nil { 685 return err 686 } 687 defer func() { 688 o.statResult, _ = cn.smbShare.Stat(filename) 689 o.fs.putConnection(&cn) 690 }() 691 692 fl, err := cn.smbShare.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) 693 if err != nil { 694 return fmt.Errorf("failed to open: %w", err) 695 } 696 697 // remove the file if upload failed 698 remove := func() { 699 // Windows doesn't allow removal of files without closing file 700 removeErr := fl.Close() 701 if removeErr != nil { 702 fs.Debugf(src, "failed to close the file for delete: %v", removeErr) 703 // try to remove the file anyway; the file may be already closed 704 } 705 706 removeErr = cn.smbShare.Remove(filename) 707 if removeErr != nil { 708 fs.Debugf(src, "failed to remove: %v", removeErr) 709 } else { 710 fs.Debugf(src, "removed after failed upload: %v", err) 711 } 712 } 713 714 _, err = fl.ReadFrom(in) 715 if err != nil { 716 remove() 717 return fmt.Errorf("Update ReadFrom failed: %w", err) 718 } 719 720 err = fl.Close() 721 if err != nil { 722 remove() 723 return fmt.Errorf("Update Close failed: %w", err) 724 } 725 726 // Set the modified time 727 err = o.SetModTime(ctx, src.ModTime(ctx)) 728 if err != nil { 729 return fmt.Errorf("Update SetModTime failed: %w", err) 730 } 731 732 return nil 733 } 734 735 // Remove an object 736 func (o *Object) Remove(ctx context.Context) (err error) { 737 share, filename := o.split() 738 if share == "" || filename == "" { 739 return fs.ErrorIsDir 740 } 741 filename = o.fs.toSambaPath(filename) 742 743 cn, err := o.fs.getConnection(ctx, share) 744 if err != nil { 745 return err 746 } 747 748 err = cn.smbShare.Remove(filename) 749 o.fs.putConnection(&cn) 750 751 return err 752 } 753 754 // String converts this Object to a string 755 func (o *Object) String() string { 756 if o == nil { 757 return "<nil>" 758 } 759 return o.remote 760 } 761 762 /// Misc 763 764 // split returns share name and path in the share from the rootRelativePath 765 // relative to f.root 766 func (f *Fs) split(rootRelativePath string) (shareName, filepath string) { 767 return bucket.Split(path.Join(f.root, rootRelativePath)) 768 } 769 770 // split returns share name and path in the share from the object 771 func (o *Object) split() (shareName, filepath string) { 772 return o.fs.split(o.remote) 773 } 774 775 func (f *Fs) toSambaPath(path string) string { 776 // 1. encode via Rclone's escaping system 777 // 2. convert to backslash-separated path 778 return strings.ReplaceAll(f.opt.Enc.FromStandardPath(path), "/", "\\") 779 } 780 781 func (f *Fs) toNativePath(path string) string { 782 // 1. convert *back* to slash-separated path 783 // 2. encode via Rclone's escaping system 784 return f.opt.Enc.ToStandardPath(strings.ReplaceAll(path, "\\", "/")) 785 } 786 787 func ensureSuffix(s, suffix string) string { 788 if strings.HasSuffix(s, suffix) { 789 return s 790 } 791 return s + suffix 792 } 793 794 func trimPathPrefix(s, prefix string) string { 795 // we need to clean the paths to make tests pass! 796 s = betterPathClean(s) 797 prefix = betterPathClean(prefix) 798 if s == prefix || s == prefix+"/" { 799 return "" 800 } 801 prefix = ensureSuffix(prefix, "/") 802 return strings.TrimPrefix(s, prefix) 803 } 804 805 func betterPathClean(p string) string { 806 d := path.Clean(p) 807 if d == "." { 808 return "" 809 } 810 return d 811 } 812 813 type boundReadCloser struct { 814 rc io.ReadCloser 815 close func() error 816 } 817 818 func (r *boundReadCloser) Read(p []byte) (n int, err error) { 819 return r.rc.Read(p) 820 } 821 822 func (r *boundReadCloser) Close() error { 823 err1 := r.rc.Close() 824 err2 := r.close() 825 if err1 != nil { 826 return err1 827 } 828 return err2 829 } 830 831 func translateError(e error, dir bool) error { 832 if os.IsNotExist(e) { 833 if dir { 834 return fs.ErrorDirNotFound 835 } 836 return fs.ErrorObjectNotFound 837 } 838 839 return e 840 } 841 842 var ( 843 _ fs.Fs = &Fs{} 844 _ fs.PutStreamer = &Fs{} 845 _ fs.Mover = &Fs{} 846 _ fs.DirMover = &Fs{} 847 _ fs.Abouter = &Fs{} 848 _ fs.Shutdowner = &Fs{} 849 _ fs.Object = &Object{} 850 _ io.ReadCloser = &boundReadCloser{} 851 )