mirror of
https://github.com/rclone/rclone.git
synced 2025-11-23 21:44:49 +02:00
In the current design, OpenWriterAt provides the interface for random-access writes, and openChunkWriterFromOpenWriterAt wraps this interface to enable parallel chunk uploads using multiple goroutines. A global connection pool is already in place to manage SMB connections across files. However, currently only one connection is used per file, which makes multiple goroutines compete for the connection during multithreaded writes. This changes create separate connections for each goroutine, which allows true parallelism by giving each goroutine its own SMB connection Signed-off-by: sudipto baral <sudiptobaral.me@gmail.com>
955 lines
23 KiB
Go
955 lines
23 KiB
Go
// Package smb provides an interface to SMB servers
|
|
package smb
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/config"
|
|
"github.com/rclone/rclone/fs/config/configmap"
|
|
"github.com/rclone/rclone/fs/config/configstruct"
|
|
"github.com/rclone/rclone/fs/hash"
|
|
"github.com/rclone/rclone/lib/bucket"
|
|
"github.com/rclone/rclone/lib/encoder"
|
|
"github.com/rclone/rclone/lib/env"
|
|
"github.com/rclone/rclone/lib/pacer"
|
|
"github.com/rclone/rclone/lib/readers"
|
|
)
|
|
|
|
const (
|
|
minSleep = 10 * time.Millisecond
|
|
maxSleep = 2 * time.Second
|
|
decayConstant = 2 // bigger for slower decay, exponential
|
|
)
|
|
|
|
var (
|
|
currentUser = env.CurrentUser()
|
|
)
|
|
|
|
// Register with Fs
|
|
func init() {
|
|
fs.Register(&fs.RegInfo{
|
|
Name: "smb",
|
|
Description: "SMB / CIFS",
|
|
NewFs: NewFs,
|
|
|
|
Options: []fs.Option{{
|
|
Name: "host",
|
|
Help: "SMB server hostname to connect to.\n\nE.g. \"example.com\".",
|
|
Required: true,
|
|
Sensitive: true,
|
|
}, {
|
|
Name: "user",
|
|
Help: "SMB username.",
|
|
Default: currentUser,
|
|
Sensitive: true,
|
|
}, {
|
|
Name: "port",
|
|
Help: "SMB port number.",
|
|
Default: 445,
|
|
}, {
|
|
Name: "pass",
|
|
Help: "SMB password.",
|
|
IsPassword: true,
|
|
}, {
|
|
Name: "domain",
|
|
Help: "Domain name for NTLM authentication.",
|
|
Default: "WORKGROUP",
|
|
Sensitive: true,
|
|
}, {
|
|
Name: "spn",
|
|
Help: `Service principal name.
|
|
|
|
Rclone presents this name to the server. Some servers use this as further
|
|
authentication, and it often needs to be set for clusters. For example:
|
|
|
|
cifs/remotehost:1020
|
|
|
|
Leave blank if not sure.
|
|
`,
|
|
Sensitive: true,
|
|
}, {
|
|
Name: "use_kerberos",
|
|
Help: `Use Kerberos authentication.
|
|
|
|
If set, rclone will use Kerberos authentication instead of NTLM. This
|
|
requires a valid Kerberos configuration and credentials cache to be
|
|
available, either in the default locations or as specified by the
|
|
KRB5_CONFIG and KRB5CCNAME environment variables.
|
|
`,
|
|
Default: false,
|
|
}, {
|
|
Name: "idle_timeout",
|
|
Default: fs.Duration(60 * time.Second),
|
|
Help: `Max time before closing idle connections.
|
|
|
|
If no connections have been returned to the connection pool in the time
|
|
given, rclone will empty the connection pool.
|
|
|
|
Set to 0 to keep connections indefinitely.
|
|
`,
|
|
Advanced: true,
|
|
}, {
|
|
Name: "hide_special_share",
|
|
Help: "Hide special shares (e.g. print$) which users aren't supposed to access.",
|
|
Default: true,
|
|
Advanced: true,
|
|
}, {
|
|
Name: "case_insensitive",
|
|
Help: "Whether the server is configured to be case-insensitive.\n\nAlways true on Windows shares.",
|
|
Default: true,
|
|
Advanced: true,
|
|
}, {
|
|
Name: "kerberos_ccache",
|
|
Help: `Path to the Kerberos credential cache (krb5cc).
|
|
|
|
Overrides the default KRB5CCNAME environment variable and allows this
|
|
instance of the SMB backend to use a different Kerberos cache file.
|
|
This is useful when mounting multiple SMB with different credentials
|
|
or running in multi-user environments.
|
|
|
|
Supported formats:
|
|
- FILE:/path/to/ccache – Use the specified file.
|
|
- DIR:/path/to/ccachedir – Use the primary file inside the specified directory.
|
|
- /path/to/ccache – Interpreted as a file path.`,
|
|
Advanced: true,
|
|
}, {
|
|
Name: config.ConfigEncoding,
|
|
Help: config.ConfigEncodingHelp,
|
|
Advanced: true,
|
|
Default: encoder.EncodeZero |
|
|
// path separator
|
|
encoder.EncodeSlash |
|
|
encoder.EncodeBackSlash |
|
|
// windows
|
|
encoder.EncodeWin |
|
|
encoder.EncodeCtl |
|
|
encoder.EncodeDot |
|
|
// the file turns into 8.3 names (and cannot be converted back)
|
|
encoder.EncodeRightSpace |
|
|
encoder.EncodeRightPeriod |
|
|
//
|
|
encoder.EncodeInvalidUtf8,
|
|
},
|
|
}})
|
|
}
|
|
|
|
// Options defines the configuration for this backend
|
|
type Options struct {
|
|
Host string `config:"host"`
|
|
Port string `config:"port"`
|
|
User string `config:"user"`
|
|
Pass string `config:"pass"`
|
|
Domain string `config:"domain"`
|
|
SPN string `config:"spn"`
|
|
UseKerberos bool `config:"use_kerberos"`
|
|
KerberosCCache string `config:"kerberos_ccache"`
|
|
HideSpecial bool `config:"hide_special_share"`
|
|
CaseInsensitive bool `config:"case_insensitive"`
|
|
IdleTimeout fs.Duration `config:"idle_timeout"`
|
|
|
|
Enc encoder.MultiEncoder `config:"encoding"`
|
|
}
|
|
|
|
// Fs represents a SMB remote
|
|
type Fs struct {
|
|
name string // name of this remote
|
|
root string // the path we are working on if any
|
|
opt Options // parsed config options
|
|
features *fs.Features // optional features
|
|
pacer *fs.Pacer // pacer for operations
|
|
|
|
sessions atomic.Int32
|
|
poolMu sync.Mutex
|
|
pool []*conn
|
|
drain *time.Timer // used to drain the pool when we stop using the connections
|
|
|
|
ctx context.Context
|
|
}
|
|
|
|
// Object describes a file at the server
|
|
type Object struct {
|
|
fs *Fs // reference to Fs
|
|
remote string // the remote path
|
|
statResult os.FileInfo
|
|
}
|
|
|
|
// NewFs constructs an Fs from the path
|
|
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
|
|
// Parse config into Options struct
|
|
opt := new(Options)
|
|
err := configstruct.Set(m, opt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
root = strings.Trim(root, "/")
|
|
|
|
f := &Fs{
|
|
name: name,
|
|
opt: *opt,
|
|
ctx: ctx,
|
|
root: root,
|
|
}
|
|
f.features = (&fs.Features{
|
|
CaseInsensitive: opt.CaseInsensitive,
|
|
CanHaveEmptyDirectories: true,
|
|
BucketBased: true,
|
|
PartialUploads: true,
|
|
}).Fill(ctx, f)
|
|
|
|
f.pacer = fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant)))
|
|
// set the pool drainer timer going
|
|
if opt.IdleTimeout > 0 {
|
|
f.drain = time.AfterFunc(time.Duration(opt.IdleTimeout), func() { _ = f.drainPool(ctx) })
|
|
}
|
|
|
|
// test if the root exists as a file
|
|
share, dir := f.split("")
|
|
if share == "" || dir == "" {
|
|
return f, nil
|
|
}
|
|
cn, err := f.getConnection(ctx, share)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stat, err := cn.smbShare.Stat(f.toSambaPath(dir))
|
|
f.putConnection(&cn, err)
|
|
if err != nil {
|
|
// ignore stat error here
|
|
return f, nil
|
|
}
|
|
if !stat.IsDir() {
|
|
f.root, err = path.Dir(root), fs.ErrorIsFile
|
|
}
|
|
fs.Debugf(f, "Using root directory %q", f.root)
|
|
return f, err
|
|
}
|
|
|
|
// Name of the remote (as passed into NewFs)
|
|
func (f *Fs) Name() string {
|
|
return f.name
|
|
}
|
|
|
|
// Root of the remote (as passed into NewFs)
|
|
func (f *Fs) Root() string {
|
|
return f.root
|
|
}
|
|
|
|
// String converts this Fs to a string
|
|
func (f *Fs) String() string {
|
|
bucket, file := f.split("")
|
|
if bucket == "" {
|
|
return fmt.Sprintf("smb://%s@%s:%s/", f.opt.User, f.opt.Host, f.opt.Port)
|
|
}
|
|
return fmt.Sprintf("smb://%s@%s:%s/%s/%s", f.opt.User, f.opt.Host, f.opt.Port, bucket, file)
|
|
}
|
|
|
|
// Features returns the optional features of this Fs
|
|
func (f *Fs) Features() *fs.Features {
|
|
return f.features
|
|
}
|
|
|
|
// Hashes returns nothing as SMB itself doesn't have a way to tell checksums
|
|
func (f *Fs) Hashes() hash.Set {
|
|
return hash.NewHashSet()
|
|
}
|
|
|
|
// Precision returns the precision of mtime
|
|
func (f *Fs) Precision() time.Duration {
|
|
return time.Millisecond
|
|
}
|
|
|
|
// NewObject creates a new file object
|
|
func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
|
share, path := f.split(remote)
|
|
return f.findObjectSeparate(ctx, share, path)
|
|
}
|
|
|
|
func (f *Fs) findObjectSeparate(ctx context.Context, share, path string) (fs.Object, error) {
|
|
if share == "" || path == "" {
|
|
return nil, fs.ErrorIsDir
|
|
}
|
|
cn, err := f.getConnection(ctx, share)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stat, err := cn.smbShare.Stat(f.toSambaPath(path))
|
|
f.putConnection(&cn, err)
|
|
if err != nil {
|
|
return nil, translateError(err, false)
|
|
}
|
|
if stat.IsDir() {
|
|
return nil, fs.ErrorIsDir
|
|
}
|
|
|
|
return f.makeEntry(share, path, stat), nil
|
|
}
|
|
|
|
// Mkdir creates a directory on the server
|
|
func (f *Fs) Mkdir(ctx context.Context, dir string) (err error) {
|
|
share, path := f.split(dir)
|
|
if share == "" || path == "" {
|
|
return nil
|
|
}
|
|
cn, err := f.getConnection(ctx, share)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = cn.smbShare.MkdirAll(f.toSambaPath(path), 0o755)
|
|
f.putConnection(&cn, err)
|
|
return err
|
|
}
|
|
|
|
// Rmdir removes an empty directory on the server
|
|
func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
|
share, path := f.split(dir)
|
|
if share == "" || path == "" {
|
|
return nil
|
|
}
|
|
cn, err := f.getConnection(ctx, share)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = cn.smbShare.Remove(f.toSambaPath(path))
|
|
f.putConnection(&cn, err)
|
|
return err
|
|
}
|
|
|
|
// Put uploads a file
|
|
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
|
o := &Object{
|
|
fs: f,
|
|
remote: src.Remote(),
|
|
}
|
|
|
|
err := o.Update(ctx, in, src, options...)
|
|
if err == nil {
|
|
return o, nil
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
// PutStream uploads to the remote path with the modTime given of indeterminate size
|
|
//
|
|
// May create the object even if it returns an error - if so
|
|
// will return the object and the error, otherwise will return
|
|
// nil and the error
|
|
func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
|
o := &Object{
|
|
fs: f,
|
|
remote: src.Remote(),
|
|
}
|
|
|
|
err := o.Update(ctx, in, src, options...)
|
|
if err == nil {
|
|
return o, nil
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
// Move src to this remote using server-side move operations.
|
|
//
|
|
// This is stored with the remote path given.
|
|
//
|
|
// It returns the destination Object and a possible error.
|
|
//
|
|
// Will only be called if src.Fs().Name() == f.Name()
|
|
//
|
|
// If it isn't possible then return fs.ErrorCantMove
|
|
func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (_ fs.Object, err error) {
|
|
dstShare, dstPath := f.split(remote)
|
|
srcObj, ok := src.(*Object)
|
|
if !ok {
|
|
fs.Debugf(src, "Can't move - not same remote type")
|
|
return nil, fs.ErrorCantMove
|
|
}
|
|
srcShare, srcPath := srcObj.split()
|
|
if dstShare != srcShare {
|
|
fs.Debugf(src, "Can't move - must be on the same share")
|
|
return nil, fs.ErrorCantMove
|
|
}
|
|
|
|
err = f.ensureDirectory(ctx, dstShare, dstPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to make parent directories: %w", err)
|
|
}
|
|
|
|
cn, err := f.getConnection(ctx, dstShare)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = cn.smbShare.Rename(f.toSambaPath(srcPath), f.toSambaPath(dstPath))
|
|
f.putConnection(&cn, err)
|
|
if err != nil {
|
|
return nil, translateError(err, false)
|
|
}
|
|
return f.findObjectSeparate(ctx, dstShare, dstPath)
|
|
}
|
|
|
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
|
// using server-side move operations.
|
|
//
|
|
// Will only be called if src.Fs().Name() == f.Name()
|
|
//
|
|
// If it isn't possible then return fs.ErrorCantDirMove
|
|
//
|
|
// If destination exists then return fs.ErrorDirExists
|
|
func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) (err error) {
|
|
dstShare, dstPath := f.split(dstRemote)
|
|
srcFs, ok := src.(*Fs)
|
|
if !ok {
|
|
fs.Debugf(src, "Can't move - not same remote type")
|
|
return fs.ErrorCantDirMove
|
|
}
|
|
srcShare, srcPath := srcFs.split(srcRemote)
|
|
if dstShare != srcShare {
|
|
fs.Debugf(src, "Can't move - must be on the same share")
|
|
return fs.ErrorCantDirMove
|
|
}
|
|
|
|
err = f.ensureDirectory(ctx, dstShare, dstPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to make parent directories: %w", err)
|
|
}
|
|
|
|
cn, err := f.getConnection(ctx, dstShare)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.putConnection(&cn, err)
|
|
|
|
_, err = cn.smbShare.Stat(dstPath)
|
|
if os.IsNotExist(err) {
|
|
err = cn.smbShare.Rename(f.toSambaPath(srcPath), f.toSambaPath(dstPath))
|
|
return translateError(err, true)
|
|
}
|
|
return fs.ErrorDirExists
|
|
}
|
|
|
|
// List files and directories in a directory
|
|
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
|
|
share, _path := f.split(dir)
|
|
|
|
cn, err := f.getConnection(ctx, share)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.putConnection(&cn, err)
|
|
|
|
if share == "" {
|
|
shares, err := cn.smbSession.ListSharenames()
|
|
for _, shh := range shares {
|
|
shh = f.toNativePath(shh)
|
|
if strings.HasSuffix(shh, "$") && f.opt.HideSpecial {
|
|
continue
|
|
}
|
|
entries = append(entries, fs.NewDir(shh, time.Time{}))
|
|
}
|
|
return entries, err
|
|
}
|
|
|
|
dirents, err := cn.smbShare.ReadDir(f.toSambaPath(_path))
|
|
if err != nil {
|
|
return entries, translateError(err, true)
|
|
}
|
|
for _, file := range dirents {
|
|
nfn := f.toNativePath(file.Name())
|
|
if file.IsDir() {
|
|
entries = append(entries, fs.NewDir(path.Join(dir, nfn), file.ModTime()))
|
|
} else {
|
|
entries = append(entries, f.makeEntryRelative(share, _path, nfn, file))
|
|
}
|
|
}
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
// About returns things about remaining and used spaces
|
|
func (f *Fs) About(ctx context.Context) (_ *fs.Usage, err error) {
|
|
share, dir := f.split("/")
|
|
if share == "" {
|
|
// Just return empty info rather than an error if called on the root
|
|
return &fs.Usage{}, nil
|
|
}
|
|
dir = f.toSambaPath(dir)
|
|
|
|
cn, err := f.getConnection(ctx, share)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stat, err := cn.smbShare.Statfs(dir)
|
|
f.putConnection(&cn, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bs := stat.BlockSize()
|
|
usage := &fs.Usage{
|
|
Total: fs.NewUsageValue(bs * stat.TotalBlockCount()),
|
|
Used: fs.NewUsageValue(bs * (stat.TotalBlockCount() - stat.FreeBlockCount())),
|
|
Free: fs.NewUsageValue(bs * stat.AvailableBlockCount()),
|
|
}
|
|
return usage, nil
|
|
}
|
|
|
|
type smbWriterAt struct {
|
|
pool *filePool
|
|
closed bool
|
|
closeMu sync.Mutex
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
func (w *smbWriterAt) WriteAt(p []byte, off int64) (int, error) {
|
|
w.closeMu.Lock()
|
|
if w.closed {
|
|
w.closeMu.Unlock()
|
|
return 0, errors.New("writer already closed")
|
|
}
|
|
w.wg.Add(1)
|
|
w.closeMu.Unlock()
|
|
defer w.wg.Done()
|
|
|
|
f, err := w.pool.get()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to get file from pool: %w", err)
|
|
}
|
|
|
|
n, writeErr := f.WriteAt(p, off)
|
|
w.pool.put(f, writeErr)
|
|
|
|
if writeErr != nil {
|
|
return n, fmt.Errorf("failed to write at offset %d: %w", off, writeErr)
|
|
}
|
|
|
|
return n, writeErr
|
|
}
|
|
|
|
func (w *smbWriterAt) Close() error {
|
|
w.closeMu.Lock()
|
|
defer w.closeMu.Unlock()
|
|
|
|
if w.closed {
|
|
return nil
|
|
}
|
|
w.closed = true
|
|
|
|
// Wait for all pending writes to finish
|
|
w.wg.Wait()
|
|
|
|
var errs []error
|
|
|
|
// Drain the pool
|
|
if err := w.pool.drain(); err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to drain file pool: %w", err))
|
|
}
|
|
|
|
// Remove session
|
|
w.pool.fs.removeSession()
|
|
|
|
if len(errs) > 0 {
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// OpenWriterAt opens with a handle for random access writes
|
|
//
|
|
// Pass in the remote desired and the size if known.
|
|
//
|
|
// It truncates any existing object
|
|
func (f *Fs) OpenWriterAt(ctx context.Context, remote string, size int64) (fs.WriterAtCloser, error) {
|
|
o := &Object{
|
|
fs: f,
|
|
remote: remote,
|
|
}
|
|
share, filename := o.split()
|
|
if share == "" || filename == "" {
|
|
return nil, fs.ErrorIsDir
|
|
}
|
|
|
|
err := o.fs.ensureDirectory(ctx, share, filename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to make parent directories: %w", err)
|
|
}
|
|
|
|
smbPath := o.fs.toSambaPath(filename)
|
|
|
|
// One-time truncate
|
|
cn, err := o.fs.getConnection(ctx, share)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file, err := cn.smbShare.OpenFile(smbPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
|
|
if err != nil {
|
|
o.fs.putConnection(&cn, err)
|
|
return nil, err
|
|
}
|
|
if size > 0 {
|
|
if truncateErr := file.Truncate(size); truncateErr != nil {
|
|
_ = file.Close()
|
|
o.fs.putConnection(&cn, truncateErr)
|
|
return nil, fmt.Errorf("failed to truncate file: %w", truncateErr)
|
|
}
|
|
}
|
|
if closeErr := file.Close(); closeErr != nil {
|
|
o.fs.putConnection(&cn, closeErr)
|
|
return nil, fmt.Errorf("failed to close file after truncate: %w", closeErr)
|
|
}
|
|
o.fs.putConnection(&cn, nil)
|
|
|
|
// Add a new session
|
|
o.fs.addSession()
|
|
|
|
return &smbWriterAt{
|
|
pool: newFilePool(ctx, o.fs, share, smbPath),
|
|
}, nil
|
|
}
|
|
|
|
// Shutdown the backend, closing any background tasks and any
|
|
// cached connections.
|
|
func (f *Fs) Shutdown(ctx context.Context) error {
|
|
return f.drainPool(ctx)
|
|
}
|
|
|
|
func (f *Fs) makeEntry(share, _path string, stat os.FileInfo) *Object {
|
|
remote := path.Join(share, _path)
|
|
return &Object{
|
|
fs: f,
|
|
remote: trimPathPrefix(remote, f.root),
|
|
statResult: stat,
|
|
}
|
|
}
|
|
|
|
func (f *Fs) makeEntryRelative(share, _path, relative string, stat os.FileInfo) *Object {
|
|
return f.makeEntry(share, path.Join(_path, relative), stat)
|
|
}
|
|
|
|
func (f *Fs) ensureDirectory(ctx context.Context, share, _path string) error {
|
|
dir := path.Dir(_path)
|
|
if dir == "." {
|
|
return nil
|
|
}
|
|
cn, err := f.getConnection(ctx, share)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = cn.smbShare.MkdirAll(f.toSambaPath(dir), 0o755)
|
|
f.putConnection(&cn, err)
|
|
return err
|
|
}
|
|
|
|
/// Object
|
|
|
|
// Remote returns the remote path
|
|
func (o *Object) Remote() string {
|
|
return o.remote
|
|
}
|
|
|
|
// ModTime is the last modified time (read-only)
|
|
func (o *Object) ModTime(ctx context.Context) time.Time {
|
|
return o.statResult.ModTime()
|
|
}
|
|
|
|
// Size is the file length
|
|
func (o *Object) Size() int64 {
|
|
return o.statResult.Size()
|
|
}
|
|
|
|
// Fs returns the parent Fs
|
|
func (o *Object) Fs() fs.Info {
|
|
return o.fs
|
|
}
|
|
|
|
// Hash always returns empty value
|
|
func (o *Object) Hash(ctx context.Context, ty hash.Type) (string, error) {
|
|
return "", hash.ErrUnsupported
|
|
}
|
|
|
|
// Storable returns if this object is storable
|
|
func (o *Object) Storable() bool {
|
|
return true
|
|
}
|
|
|
|
// SetModTime sets modTime on a particular file
|
|
func (o *Object) SetModTime(ctx context.Context, t time.Time) (err error) {
|
|
share, reqDir := o.split()
|
|
if share == "" || reqDir == "" {
|
|
return fs.ErrorCantSetModTime
|
|
}
|
|
reqDir = o.fs.toSambaPath(reqDir)
|
|
|
|
cn, err := o.fs.getConnection(ctx, share)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer o.fs.putConnection(&cn, err)
|
|
|
|
err = cn.smbShare.Chtimes(reqDir, t, t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fi, err := cn.smbShare.Stat(reqDir)
|
|
if err != nil {
|
|
return fmt.Errorf("SetModTime: stat: %w", err)
|
|
}
|
|
o.statResult = fi
|
|
return err
|
|
}
|
|
|
|
// Open an object for read
|
|
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
|
|
share, filename := o.split()
|
|
if share == "" || filename == "" {
|
|
return nil, fs.ErrorIsDir
|
|
}
|
|
filename = o.fs.toSambaPath(filename)
|
|
|
|
var offset, limit int64 = 0, -1
|
|
for _, option := range options {
|
|
switch x := option.(type) {
|
|
case *fs.SeekOption:
|
|
offset = x.Offset
|
|
case *fs.RangeOption:
|
|
offset, limit = x.Decode(o.Size())
|
|
default:
|
|
if option.Mandatory() {
|
|
fs.Logf(o, "Unsupported mandatory option: %v", option)
|
|
}
|
|
}
|
|
}
|
|
|
|
o.fs.addSession() // Show session in use
|
|
defer o.fs.removeSession()
|
|
|
|
cn, err := o.fs.getConnection(ctx, share)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fl, err := cn.smbShare.OpenFile(filename, os.O_RDONLY, 0)
|
|
if err != nil {
|
|
o.fs.putConnection(&cn, err)
|
|
return nil, fmt.Errorf("failed to open: %w", err)
|
|
}
|
|
pos, err := fl.Seek(offset, io.SeekStart)
|
|
if err != nil {
|
|
o.fs.putConnection(&cn, err)
|
|
return nil, fmt.Errorf("failed to seek: %w", err)
|
|
}
|
|
if pos != offset {
|
|
err = fmt.Errorf("failed to seek: wrong position (expected=%d, reported=%d)", offset, pos)
|
|
o.fs.putConnection(&cn, err)
|
|
return nil, err
|
|
}
|
|
|
|
in = readers.NewLimitedReadCloser(fl, limit)
|
|
in = &boundReadCloser{
|
|
rc: in,
|
|
close: func() error {
|
|
o.fs.putConnection(&cn, nil)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
return in, nil
|
|
}
|
|
|
|
// Update the Object from in with modTime and size
|
|
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) {
|
|
share, filename := o.split()
|
|
if share == "" || filename == "" {
|
|
return fs.ErrorIsDir
|
|
}
|
|
|
|
err = o.fs.ensureDirectory(ctx, share, filename)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to make parent directories: %w", err)
|
|
}
|
|
|
|
filename = o.fs.toSambaPath(filename)
|
|
|
|
o.fs.addSession() // Show session in use
|
|
defer o.fs.removeSession()
|
|
|
|
cn, err := o.fs.getConnection(ctx, share)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
o.fs.putConnection(&cn, err)
|
|
}()
|
|
|
|
fl, err := cn.smbShare.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open: %w", err)
|
|
}
|
|
|
|
// remove the file if upload failed
|
|
remove := func() {
|
|
// Windows doesn't allow removal of files without closing file
|
|
removeErr := fl.Close()
|
|
if removeErr != nil {
|
|
fs.Debugf(src, "failed to close the file for delete: %v", removeErr)
|
|
// try to remove the file anyway; the file may be already closed
|
|
}
|
|
|
|
removeErr = cn.smbShare.Remove(filename)
|
|
if removeErr != nil {
|
|
fs.Debugf(src, "failed to remove: %v", removeErr)
|
|
} else {
|
|
fs.Debugf(src, "removed after failed upload: %v", err)
|
|
}
|
|
}
|
|
|
|
_, err = fl.ReadFrom(in)
|
|
if err != nil {
|
|
remove()
|
|
return fmt.Errorf("Update ReadFrom failed: %w", err)
|
|
}
|
|
|
|
err = fl.Close()
|
|
if err != nil {
|
|
remove()
|
|
return fmt.Errorf("Update Close failed: %w", err)
|
|
}
|
|
|
|
// Set the modified time and also o.statResult
|
|
err = o.SetModTime(ctx, src.ModTime(ctx))
|
|
if err != nil {
|
|
return fmt.Errorf("Update SetModTime failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Remove an object
|
|
func (o *Object) Remove(ctx context.Context) (err error) {
|
|
share, filename := o.split()
|
|
if share == "" || filename == "" {
|
|
return fs.ErrorIsDir
|
|
}
|
|
filename = o.fs.toSambaPath(filename)
|
|
|
|
cn, err := o.fs.getConnection(ctx, share)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = cn.smbShare.Remove(filename)
|
|
o.fs.putConnection(&cn, err)
|
|
|
|
return err
|
|
}
|
|
|
|
// String converts this Object to a string
|
|
func (o *Object) String() string {
|
|
if o == nil {
|
|
return "<nil>"
|
|
}
|
|
return o.remote
|
|
}
|
|
|
|
/// Misc
|
|
|
|
// split returns share name and path in the share from the rootRelativePath
|
|
// relative to f.root
|
|
func (f *Fs) split(rootRelativePath string) (shareName, filepath string) {
|
|
return bucket.Split(path.Join(f.root, rootRelativePath))
|
|
}
|
|
|
|
// split returns share name and path in the share from the object
|
|
func (o *Object) split() (shareName, filepath string) {
|
|
return o.fs.split(o.remote)
|
|
}
|
|
|
|
func (f *Fs) toSambaPath(path string) string {
|
|
// 1. encode via Rclone's escaping system
|
|
// 2. convert to backslash-separated path
|
|
return strings.ReplaceAll(f.opt.Enc.FromStandardPath(path), "/", "\\")
|
|
}
|
|
|
|
func (f *Fs) toNativePath(path string) string {
|
|
// 1. convert *back* to slash-separated path
|
|
// 2. encode via Rclone's escaping system
|
|
return f.opt.Enc.ToStandardPath(strings.ReplaceAll(path, "\\", "/"))
|
|
}
|
|
|
|
func ensureSuffix(s, suffix string) string {
|
|
if strings.HasSuffix(s, suffix) {
|
|
return s
|
|
}
|
|
return s + suffix
|
|
}
|
|
|
|
func trimPathPrefix(s, prefix string) string {
|
|
// we need to clean the paths to make tests pass!
|
|
s = betterPathClean(s)
|
|
prefix = betterPathClean(prefix)
|
|
if s == prefix || s == prefix+"/" {
|
|
return ""
|
|
}
|
|
prefix = ensureSuffix(prefix, "/")
|
|
return strings.TrimPrefix(s, prefix)
|
|
}
|
|
|
|
func betterPathClean(p string) string {
|
|
d := path.Clean(p)
|
|
if d == "." {
|
|
return ""
|
|
}
|
|
return d
|
|
}
|
|
|
|
type boundReadCloser struct {
|
|
rc io.ReadCloser
|
|
close func() error
|
|
}
|
|
|
|
func (r *boundReadCloser) Read(p []byte) (n int, err error) {
|
|
return r.rc.Read(p)
|
|
}
|
|
|
|
func (r *boundReadCloser) Close() error {
|
|
err1 := r.rc.Close()
|
|
err2 := r.close()
|
|
if err1 != nil {
|
|
return err1
|
|
}
|
|
return err2
|
|
}
|
|
|
|
func translateError(e error, dir bool) error {
|
|
if os.IsNotExist(e) {
|
|
if dir {
|
|
return fs.ErrorDirNotFound
|
|
}
|
|
return fs.ErrorObjectNotFound
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
var (
|
|
_ fs.Fs = &Fs{}
|
|
_ fs.PutStreamer = &Fs{}
|
|
_ fs.Mover = &Fs{}
|
|
_ fs.DirMover = &Fs{}
|
|
_ fs.Abouter = &Fs{}
|
|
_ fs.Shutdowner = &Fs{}
|
|
_ fs.Object = &Object{}
|
|
_ io.ReadCloser = &boundReadCloser{}
|
|
)
|