mirror of
https://github.com/rclone/rclone.git
synced 2024-11-22 10:54:48 +08:00
smb: backend to support SMB - fixes #2042
This commit is contained in:
parent
4e078765f9
commit
2aa264b33c
|
@ -74,6 +74,7 @@ Rclone *("rsync for cloud storage")* is a command-line program to sync files and
|
||||||
* Seafile [:page_facing_up:](https://rclone.org/seafile/)
|
* Seafile [:page_facing_up:](https://rclone.org/seafile/)
|
||||||
* SeaweedFS [:page_facing_up:](https://rclone.org/s3/#seaweedfs)
|
* SeaweedFS [:page_facing_up:](https://rclone.org/s3/#seaweedfs)
|
||||||
* SFTP [:page_facing_up:](https://rclone.org/sftp/)
|
* SFTP [:page_facing_up:](https://rclone.org/sftp/)
|
||||||
|
* SMB / CIFS [:page_facing_up:](https://rclone.org/smb/)
|
||||||
* StackPath [:page_facing_up:](https://rclone.org/s3/#stackpath)
|
* StackPath [:page_facing_up:](https://rclone.org/s3/#stackpath)
|
||||||
* Storj [:page_facing_up:](https://rclone.org/storj/)
|
* Storj [:page_facing_up:](https://rclone.org/storj/)
|
||||||
* SugarSync [:page_facing_up:](https://rclone.org/sugarsync/)
|
* SugarSync [:page_facing_up:](https://rclone.org/sugarsync/)
|
||||||
|
|
|
@ -44,6 +44,7 @@ import (
|
||||||
_ "github.com/rclone/rclone/backend/sftp"
|
_ "github.com/rclone/rclone/backend/sftp"
|
||||||
_ "github.com/rclone/rclone/backend/sharefile"
|
_ "github.com/rclone/rclone/backend/sharefile"
|
||||||
_ "github.com/rclone/rclone/backend/sia"
|
_ "github.com/rclone/rclone/backend/sia"
|
||||||
|
_ "github.com/rclone/rclone/backend/smb"
|
||||||
_ "github.com/rclone/rclone/backend/storj"
|
_ "github.com/rclone/rclone/backend/storj"
|
||||||
_ "github.com/rclone/rclone/backend/sugarsync"
|
_ "github.com/rclone/rclone/backend/sugarsync"
|
||||||
_ "github.com/rclone/rclone/backend/swift"
|
_ "github.com/rclone/rclone/backend/swift"
|
||||||
|
|
229
backend/smb/connpool.go
Normal file
229
backend/smb/connpool.go
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
package smb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
smb2 "github.com/hirochachacha/go-smb2"
|
||||||
|
"github.com/rclone/rclone/fs"
|
||||||
|
"github.com/rclone/rclone/fs/accounting"
|
||||||
|
"github.com/rclone/rclone/fs/config/obscure"
|
||||||
|
"github.com/rclone/rclone/fs/fshttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dial starts a client connection to the given SMB server. It is a
|
||||||
|
// convenience function that connects to the given network address,
|
||||||
|
// initiates the SMB handshake, and then sets up a Client.
|
||||||
|
func (f *Fs) dial(ctx context.Context, network, addr string) (*conn, error) {
|
||||||
|
dialer := fshttp.NewDialer(ctx)
|
||||||
|
tconn, err := dialer.Dial(network, addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pass := ""
|
||||||
|
if f.opt.Pass != "" {
|
||||||
|
pass, err = obscure.Reveal(f.opt.Pass)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d := &smb2.Dialer{
|
||||||
|
Initiator: &smb2.NTLMInitiator{
|
||||||
|
User: f.opt.User,
|
||||||
|
Password: pass,
|
||||||
|
Domain: f.opt.Domain,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := d.DialContext(ctx, tconn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &conn{
|
||||||
|
smbSession: session,
|
||||||
|
conn: &tconn,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// conn encapsulates a SMB client and corresponding SMB client
|
||||||
|
type conn struct {
|
||||||
|
conn *net.Conn
|
||||||
|
smbSession *smb2.Session
|
||||||
|
smbShare *smb2.Share
|
||||||
|
shareName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closes the connection
|
||||||
|
func (c *conn) close() (err error) {
|
||||||
|
if c.smbShare != nil {
|
||||||
|
err = c.smbShare.Umount()
|
||||||
|
}
|
||||||
|
sessionLogoffErr := c.smbSession.Logoff()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sessionLogoffErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if it's closed
|
||||||
|
func (c *conn) closed() bool {
|
||||||
|
var nopErr error
|
||||||
|
if c.smbShare != nil {
|
||||||
|
// stat the current directory
|
||||||
|
_, nopErr = c.smbShare.Stat(".")
|
||||||
|
} else {
|
||||||
|
// list the shares
|
||||||
|
_, nopErr = c.smbSession.ListSharenames()
|
||||||
|
}
|
||||||
|
return nopErr == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show that we are using a SMB session
|
||||||
|
//
|
||||||
|
// Call removeSession() when done
|
||||||
|
func (f *Fs) addSession() {
|
||||||
|
atomic.AddInt32(&f.sessions, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the SMB session is no longer in use
|
||||||
|
func (f *Fs) removeSession() {
|
||||||
|
atomic.AddInt32(&f.sessions, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSessions shows whether there are any sessions in use
|
||||||
|
func (f *Fs) getSessions() int32 {
|
||||||
|
return atomic.LoadInt32(&f.sessions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open a new connection to the SMB server.
|
||||||
|
func (f *Fs) newConnection(ctx context.Context, share string) (c *conn, err error) {
|
||||||
|
c, err = f.dial(ctx, "tcp", f.opt.Host+":"+f.opt.Port)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("couldn't connect SMB: %w", err)
|
||||||
|
}
|
||||||
|
if share != "" {
|
||||||
|
// mount the specified share as well if user requested
|
||||||
|
c.smbShare, err = c.smbSession.Mount(share)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.smbSession.Logoff()
|
||||||
|
return nil, fmt.Errorf("couldn't initialize SMB: %w", err)
|
||||||
|
}
|
||||||
|
c.smbShare = c.smbShare.WithContext(ctx)
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the specified share is mounted or the session is unmounted
|
||||||
|
func (c *conn) mountShare(share string) (err error) {
|
||||||
|
if c.shareName == share {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if c.smbShare != nil {
|
||||||
|
err = c.smbShare.Umount()
|
||||||
|
c.smbShare = nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if share != "" {
|
||||||
|
c.smbShare, err = c.smbSession.Mount(share)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.shareName = share
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a SMB connection from the pool, or open a new one
|
||||||
|
func (f *Fs) getConnection(ctx context.Context, share string) (c *conn, err error) {
|
||||||
|
accounting.LimitTPS(ctx)
|
||||||
|
f.poolMu.Lock()
|
||||||
|
for len(f.pool) > 0 {
|
||||||
|
c = f.pool[0]
|
||||||
|
f.pool = f.pool[1:]
|
||||||
|
err = c.mountShare(share)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fs.Debugf(f, "Discarding unusable SMB connection: %v", err)
|
||||||
|
c = nil
|
||||||
|
}
|
||||||
|
f.poolMu.Unlock()
|
||||||
|
if c != nil {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
|
c, err = f.newConnection(ctx, share)
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
})
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a SMB connection to the pool
|
||||||
|
//
|
||||||
|
// It nils the pointed to connection out so it can't be reused
|
||||||
|
func (f *Fs) putConnection(pc **conn) {
|
||||||
|
c := *pc
|
||||||
|
*pc = nil
|
||||||
|
|
||||||
|
var nopErr error
|
||||||
|
if c.smbShare != nil {
|
||||||
|
// stat the current directory
|
||||||
|
_, nopErr = c.smbShare.Stat(".")
|
||||||
|
} else {
|
||||||
|
// list the shares
|
||||||
|
_, nopErr = c.smbSession.ListSharenames()
|
||||||
|
}
|
||||||
|
if nopErr != nil {
|
||||||
|
fs.Debugf(f, "Connection failed, closing: %v", nopErr)
|
||||||
|
_ = c.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f.poolMu.Lock()
|
||||||
|
f.pool = append(f.pool, c)
|
||||||
|
if f.opt.IdleTimeout > 0 {
|
||||||
|
f.drain.Reset(time.Duration(f.opt.IdleTimeout)) // nudge on the pool emptying timer
|
||||||
|
}
|
||||||
|
f.poolMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain the pool of any connections
|
||||||
|
func (f *Fs) drainPool(ctx context.Context) (err error) {
|
||||||
|
f.poolMu.Lock()
|
||||||
|
defer f.poolMu.Unlock()
|
||||||
|
if sessions := f.getSessions(); sessions != 0 {
|
||||||
|
fs.Debugf(f, "Not closing %d unused connections as %d sessions active", len(f.pool), sessions)
|
||||||
|
if f.opt.IdleTimeout > 0 {
|
||||||
|
f.drain.Reset(time.Duration(f.opt.IdleTimeout)) // nudge on the pool emptying timer
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if f.opt.IdleTimeout > 0 {
|
||||||
|
f.drain.Stop()
|
||||||
|
}
|
||||||
|
if len(f.pool) != 0 {
|
||||||
|
fs.Debugf(f, "Closing %d unused connections", len(f.pool))
|
||||||
|
}
|
||||||
|
for i, c := range f.pool {
|
||||||
|
if !c.closed() {
|
||||||
|
cErr := c.close()
|
||||||
|
if cErr != nil {
|
||||||
|
err = cErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.pool[i] = nil
|
||||||
|
}
|
||||||
|
f.pool = nil
|
||||||
|
return err
|
||||||
|
}
|
789
backend/smb/smb.go
Normal file
789
backend/smb/smb.go
Normal file
|
@ -0,0 +1,789 @@
|
||||||
|
// Package smb provides an interface to SMB servers
|
||||||
|
package smb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"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 = 100 * 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,
|
||||||
|
}, {
|
||||||
|
Name: "user",
|
||||||
|
Help: "SMB username.",
|
||||||
|
Default: currentUser,
|
||||||
|
}, {
|
||||||
|
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",
|
||||||
|
}, {
|
||||||
|
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: 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"`
|
||||||
|
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 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,
|
||||||
|
}).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)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
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 = 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)
|
||||||
|
|
||||||
|
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 == "" {
|
||||||
|
return nil, fs.ErrorListBucketRequired
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bs := int64(stat.BlockSize())
|
||||||
|
usage := &fs.Usage{
|
||||||
|
Total: fs.NewUsageValue(bs * int64(stat.TotalBlockCount())),
|
||||||
|
Used: fs.NewUsageValue(bs * int64(stat.TotalBlockCount()-stat.FreeBlockCount())),
|
||||||
|
Free: fs.NewUsageValue(bs * int64(stat.AvailableBlockCount())),
|
||||||
|
}
|
||||||
|
return usage, 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 {
|
||||||
|
cn, err := f.getConnection(ctx, share)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = cn.smbShare.MkdirAll(f.toSambaPath(path.Dir(_path)), 0o755)
|
||||||
|
f.putConnection(&cn)
|
||||||
|
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 = cn.smbShare.Chtimes(reqDir, t, t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fi, err := cn.smbShare.Stat(reqDir)
|
||||||
|
if err == nil {
|
||||||
|
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)
|
||||||
|
return nil, fmt.Errorf("failed to open: %w", err)
|
||||||
|
}
|
||||||
|
pos, err := fl.Seek(offset, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
o.fs.putConnection(&cn)
|
||||||
|
return nil, fmt.Errorf("failed to seek: %w", err)
|
||||||
|
}
|
||||||
|
if pos != offset {
|
||||||
|
o.fs.putConnection(&cn)
|
||||||
|
return nil, fmt.Errorf("failed to seek: wrong position (expected=%d, reported=%d)", offset, pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
in = readers.NewLimitedReadCloser(fl, limit)
|
||||||
|
in = &boundReadCloser{
|
||||||
|
rc: in,
|
||||||
|
close: func() error {
|
||||||
|
o.fs.putConnection(&cn)
|
||||||
|
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.statResult, _ = cn.smbShare.Stat(filename)
|
||||||
|
o.fs.putConnection(&cn)
|
||||||
|
}()
|
||||||
|
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
|
||||||
|
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{}
|
||||||
|
)
|
17
backend/smb/smb_test.go
Normal file
17
backend/smb/smb_test.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// Test smb filesystem interface
|
||||||
|
package smb_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rclone/rclone/backend/smb"
|
||||||
|
"github.com/rclone/rclone/fstest/fstests"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestIntegration runs integration tests against the remote
|
||||||
|
func TestIntegration(t *testing.T) {
|
||||||
|
fstests.Run(t, &fstests.Opt{
|
||||||
|
RemoteName: "TestSMB:rclone",
|
||||||
|
NilObject: (*smb.Object)(nil),
|
||||||
|
})
|
||||||
|
}
|
|
@ -68,6 +68,7 @@ docs = [
|
||||||
"putio.md",
|
"putio.md",
|
||||||
"seafile.md",
|
"seafile.md",
|
||||||
"sftp.md",
|
"sftp.md",
|
||||||
|
"smb.md",
|
||||||
"storj.md",
|
"storj.md",
|
||||||
"sugarsync.md",
|
"sugarsync.md",
|
||||||
"tardigrade.md", # stub only to redirect to storj.md
|
"tardigrade.md", # stub only to redirect to storj.md
|
||||||
|
|
|
@ -162,6 +162,7 @@ WebDAV or S3, that work out of the box.)
|
||||||
{{< provider name="SeaweedFS" home="https://github.com/chrislusf/seaweedfs/" config="/s3/#seaweedfs" >}}
|
{{< provider name="SeaweedFS" home="https://github.com/chrislusf/seaweedfs/" config="/s3/#seaweedfs" >}}
|
||||||
{{< provider name="SFTP" home="https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol" config="/sftp/" >}}
|
{{< provider name="SFTP" home="https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol" config="/sftp/" >}}
|
||||||
{{< provider name="Sia" home="https://sia.tech/" config="/sia/" >}}
|
{{< provider name="Sia" home="https://sia.tech/" config="/sia/" >}}
|
||||||
|
{{< provider name="SMB / CIFS" home="https://en.wikipedia.org/wiki/Server_Message_Block" config="/smb/" >}}
|
||||||
{{< provider name="StackPath" home="https://www.stackpath.com/products/object-storage/" config="/s3/#stackpath" >}}
|
{{< provider name="StackPath" home="https://www.stackpath.com/products/object-storage/" config="/s3/#stackpath" >}}
|
||||||
{{< provider name="Storj" home="https://storj.io/" config="/storj/" >}}
|
{{< provider name="Storj" home="https://storj.io/" config="/storj/" >}}
|
||||||
{{< provider name="SugarSync" home="https://sugarsync.com/" config="/sugarsync/" >}}
|
{{< provider name="SugarSync" home="https://sugarsync.com/" config="/sugarsync/" >}}
|
||||||
|
|
|
@ -69,6 +69,7 @@ See the following for detailed instructions for
|
||||||
* [Seafile](/seafile/)
|
* [Seafile](/seafile/)
|
||||||
* [SFTP](/sftp/)
|
* [SFTP](/sftp/)
|
||||||
* [Sia](/sia/)
|
* [Sia](/sia/)
|
||||||
|
* [SMB](/smb/)
|
||||||
* [Storj](/storj/)
|
* [Storj](/storj/)
|
||||||
* [SugarSync](/sugarsync/)
|
* [SugarSync](/sugarsync/)
|
||||||
* [Union](/union/)
|
* [Union](/union/)
|
||||||
|
|
|
@ -50,6 +50,7 @@ Here is an overview of the major features of each cloud storage system.
|
||||||
| Seafile | - | - | No | No | - | - |
|
| Seafile | - | - | No | No | - | - |
|
||||||
| SFTP | MD5, SHA1 ² | R/W | Depends | No | - | - |
|
| SFTP | MD5, SHA1 ² | R/W | Depends | No | - | - |
|
||||||
| Sia | - | - | No | No | - | - |
|
| Sia | - | - | No | No | - | - |
|
||||||
|
| SMB | - | - | Yes | No | - | - |
|
||||||
| SugarSync | - | - | No | No | - | - |
|
| SugarSync | - | - | No | No | - | - |
|
||||||
| Storj | - | R | No | No | - | - |
|
| Storj | - | R | No | No | - | - |
|
||||||
| Uptobox | - | - | No | Yes | - | - |
|
| Uptobox | - | - | No | Yes | - | - |
|
||||||
|
@ -501,6 +502,7 @@ upon backend-specific capabilities.
|
||||||
| Seafile | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
| Seafile | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||||
| SFTP | No | No | Yes | Yes | No | No | Yes | No | Yes | Yes |
|
| SFTP | No | No | Yes | Yes | No | No | Yes | No | Yes | Yes |
|
||||||
| Sia | No | No | No | No | No | No | Yes | No | No | Yes |
|
| Sia | No | No | No | No | No | No | Yes | No | No | Yes |
|
||||||
|
| SMB | No | No | Yes | Yes | No | No | Yes | No | No | Yes |
|
||||||
| SugarSync | Yes | Yes | Yes | Yes | No | No | Yes | Yes | No | Yes |
|
| SugarSync | Yes | Yes | Yes | Yes | No | No | Yes | Yes | No | Yes |
|
||||||
| Storj | Yes † | No | Yes | No | No | Yes | Yes | No | No | No |
|
| Storj | Yes † | No | Yes | No | No | Yes | Yes | No | No | No |
|
||||||
| Uptobox | No | Yes | Yes | Yes | No | No | No | No | No | No |
|
| Uptobox | No | Yes | Yes | Yes | No | No | No | No | No | No |
|
||||||
|
|
231
docs/content/smb.md
Normal file
231
docs/content/smb.md
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
---
|
||||||
|
title: "SMB / CIFS"
|
||||||
|
description: "Rclone docs for SMB backend"
|
||||||
|
---
|
||||||
|
|
||||||
|
# {{< icon "fa fa-server" >}} SMB
|
||||||
|
|
||||||
|
SMB is [a communication protocol to share files over network](https://en.wikipedia.org/wiki/Server_Message_Block).
|
||||||
|
|
||||||
|
This relies on [go-smb2 library](https://github.com/hirochachacha/go-smb2/) for communication with SMB protocol.
|
||||||
|
|
||||||
|
Paths are specified as `remote:sharename` (or `remote:` for the `lsd`
|
||||||
|
command.) You may put subdirectories in too, e.g. `remote:item/path/to/dir`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
The first path segment must be the name of the share, which you entered when you started to share on Windows. On smbd, it's the section title in `smb.conf` (usually in `/etc/samba/`) file.
|
||||||
|
You can find shares by quering the root if you're unsure (e.g. `rclone lsd remote:`).
|
||||||
|
|
||||||
|
You can't access to the shared printers from rclone, obviously.
|
||||||
|
|
||||||
|
You can't use Anonymous access for logging in. You have to use the `guest` user with an empty password instead.
|
||||||
|
The rclone client tries to avoid 8.3 names when uploading files by encoding trailing spaces and periods.
|
||||||
|
Alternatively, [the local backend](/local/#paths-on-windows) on Windows can access SMB servers using UNC paths, by `\\server\share`. This doesn't apply to non-Windows OSes, such as Linux and macOS.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Here is an example of making a SMB configuration.
|
||||||
|
|
||||||
|
First run
|
||||||
|
|
||||||
|
rclone config
|
||||||
|
|
||||||
|
This will guide you through an interactive setup process.
|
||||||
|
|
||||||
|
```
|
||||||
|
No remotes found, make a new one?
|
||||||
|
n) New remote
|
||||||
|
s) Set configuration password
|
||||||
|
q) Quit config
|
||||||
|
n/s/q> n
|
||||||
|
name> remote
|
||||||
|
Option Storage.
|
||||||
|
Type of storage to configure.
|
||||||
|
Choose a number from below, or type in your own value.
|
||||||
|
XX / SMB / CIFS
|
||||||
|
\ (smb)
|
||||||
|
Storage> smb
|
||||||
|
|
||||||
|
Option host.
|
||||||
|
Samba hostname to connect to.
|
||||||
|
E.g. "example.com".
|
||||||
|
Enter a value.
|
||||||
|
host> localhost
|
||||||
|
|
||||||
|
Option user.
|
||||||
|
Samba username.
|
||||||
|
Enter a string value. Press Enter for the default (lesmi).
|
||||||
|
user> guest
|
||||||
|
|
||||||
|
Option port.
|
||||||
|
Samba port number.
|
||||||
|
Enter a signed integer. Press Enter for the default (445).
|
||||||
|
port>
|
||||||
|
|
||||||
|
Option pass.
|
||||||
|
Samba password.
|
||||||
|
Choose an alternative below. Press Enter for the default (n).
|
||||||
|
y) Yes, type in my own password
|
||||||
|
g) Generate random password
|
||||||
|
n) No, leave this optional password blank (default)
|
||||||
|
y/g/n> g
|
||||||
|
Password strength in bits.
|
||||||
|
64 is just about memorable
|
||||||
|
128 is secure
|
||||||
|
1024 is the maximum
|
||||||
|
Bits> 64
|
||||||
|
Your password is: XXXX
|
||||||
|
Use this password? Please note that an obscured version of this
|
||||||
|
password (and not the password itself) will be stored under your
|
||||||
|
configuration file, so keep this generated password in a safe place.
|
||||||
|
y) Yes (default)
|
||||||
|
n) No
|
||||||
|
y/n> y
|
||||||
|
|
||||||
|
Option domain.
|
||||||
|
Domain name for NTLM authentication.
|
||||||
|
Enter a string value. Press Enter for the default (WORKGROUP).
|
||||||
|
domain>
|
||||||
|
|
||||||
|
Edit advanced config?
|
||||||
|
y) Yes
|
||||||
|
n) No (default)
|
||||||
|
y/n> n
|
||||||
|
|
||||||
|
Configuration complete.
|
||||||
|
Options:
|
||||||
|
- type: samba
|
||||||
|
- host: localhost
|
||||||
|
- user: guest
|
||||||
|
- pass: *** ENCRYPTED ***
|
||||||
|
Keep this "remote" remote?
|
||||||
|
y) Yes this is OK (default)
|
||||||
|
e) Edit this remote
|
||||||
|
d) Delete this remote
|
||||||
|
y/e/d> d
|
||||||
|
```
|
||||||
|
|
||||||
|
{{< rem autogenerated options start" - DO NOT EDIT - instead edit fs.RegInfo in backend/smb/smb.go then run make backenddocs" >}}
|
||||||
|
### Standard options
|
||||||
|
|
||||||
|
Here are the Standard options specific to smb (SMB / CIFS).
|
||||||
|
|
||||||
|
#### --smb-host
|
||||||
|
|
||||||
|
SMB server hostname to connect to.
|
||||||
|
|
||||||
|
E.g. "example.com".
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- Config: host
|
||||||
|
- Env Var: RCLONE_SMB_HOST
|
||||||
|
- Type: string
|
||||||
|
- Required: true
|
||||||
|
|
||||||
|
#### --smb-user
|
||||||
|
|
||||||
|
SMB username.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- Config: user
|
||||||
|
- Env Var: RCLONE_SMB_USER
|
||||||
|
- Type: string
|
||||||
|
- Default: "$USER"
|
||||||
|
|
||||||
|
#### --smb-port
|
||||||
|
|
||||||
|
SMB port number.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- Config: port
|
||||||
|
- Env Var: RCLONE_SMB_PORT
|
||||||
|
- Type: int
|
||||||
|
- Default: 445
|
||||||
|
|
||||||
|
#### --smb-pass
|
||||||
|
|
||||||
|
SMB password.
|
||||||
|
|
||||||
|
**NB** Input to this must be obscured - see [rclone obscure](/commands/rclone_obscure/).
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- Config: pass
|
||||||
|
- Env Var: RCLONE_SMB_PASS
|
||||||
|
- Type: string
|
||||||
|
- Required: false
|
||||||
|
|
||||||
|
#### --smb-domain
|
||||||
|
|
||||||
|
Domain name for NTLM authentication.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- Config: domain
|
||||||
|
- Env Var: RCLONE_SMB_DOMAIN
|
||||||
|
- Type: string
|
||||||
|
- Default: "WORKGROUP"
|
||||||
|
|
||||||
|
### Advanced options
|
||||||
|
|
||||||
|
Here are the Advanced options specific to smb (SMB / CIFS).
|
||||||
|
|
||||||
|
#### --smb-idle-timeout
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- Config: idle_timeout
|
||||||
|
- Env Var: RCLONE_SMB_IDLE_TIMEOUT
|
||||||
|
- Type: Duration
|
||||||
|
- Default: 1m0s
|
||||||
|
|
||||||
|
#### --smb-hide-special-share
|
||||||
|
|
||||||
|
Hide special shares (e.g. print$) which users aren't supposed to access.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- Config: hide_special_share
|
||||||
|
- Env Var: RCLONE_SMB_HIDE_SPECIAL_SHARE
|
||||||
|
- Type: bool
|
||||||
|
- Default: true
|
||||||
|
|
||||||
|
#### --smb-case-insensitive
|
||||||
|
|
||||||
|
Whether the server is configured to be case-insensitive.
|
||||||
|
|
||||||
|
Always true on Windows shares.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- Config: case_insensitive
|
||||||
|
- Env Var: RCLONE_SMB_CASE_INSENSITIVE
|
||||||
|
- Type: bool
|
||||||
|
- Default: true
|
||||||
|
|
||||||
|
#### --smb-encoding
|
||||||
|
|
||||||
|
The encoding for the backend.
|
||||||
|
|
||||||
|
See the [encoding section in the overview](/overview/#encoding) for more info.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- Config: encoding
|
||||||
|
- Env Var: RCLONE_SMB_ENCODING
|
||||||
|
- Type: MultiEncoder
|
||||||
|
- Default: Slash,LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe,BackSlash,Ctl,RightSpace,RightPeriod,InvalidUtf8,Dot
|
||||||
|
|
||||||
|
{{< rem autogenerated options stop >}}
|
|
@ -92,6 +92,7 @@
|
||||||
<a class="dropdown-item" href="/seafile/"><i class="fa fa-server fa-fw"></i> Seafile</a>
|
<a class="dropdown-item" href="/seafile/"><i class="fa fa-server fa-fw"></i> Seafile</a>
|
||||||
<a class="dropdown-item" href="/sftp/"><i class="fa fa-server fa-fw"></i> SFTP</a>
|
<a class="dropdown-item" href="/sftp/"><i class="fa fa-server fa-fw"></i> SFTP</a>
|
||||||
<a class="dropdown-item" href="/sia/"><i class="fa fa-globe fa-fw"></i> Sia</a>
|
<a class="dropdown-item" href="/sia/"><i class="fa fa-globe fa-fw"></i> Sia</a>
|
||||||
|
<a class="dropdown-item" href="/smb/"><i class="fa fa-server fa-fw"></i> SMB / CIFS</a>
|
||||||
<a class="dropdown-item" href="/storj/"><i class="fas fa-dove fa-fw"></i> Storj</a>
|
<a class="dropdown-item" href="/storj/"><i class="fas fa-dove fa-fw"></i> Storj</a>
|
||||||
<a class="dropdown-item" href="/sugarsync/"><i class="fas fa-dove fa-fw"></i> SugarSync</a>
|
<a class="dropdown-item" href="/sugarsync/"><i class="fas fa-dove fa-fw"></i> SugarSync</a>
|
||||||
<a class="dropdown-item" href="/uptobox/"><i class="fa fa-archive fa-fw"></i> Uptobox</a>
|
<a class="dropdown-item" href="/uptobox/"><i class="fa fa-archive fa-fw"></i> Uptobox</a>
|
||||||
|
|
|
@ -369,6 +369,9 @@ backends:
|
||||||
- backend: "sia"
|
- backend: "sia"
|
||||||
remote: "TestSia:"
|
remote: "TestSia:"
|
||||||
fastlist: false
|
fastlist: false
|
||||||
|
- backend: "smb"
|
||||||
|
remote: "TestSMB:rclone"
|
||||||
|
fastlist: false
|
||||||
- backend: "storj"
|
- backend: "storj"
|
||||||
remote: "TestStorj:"
|
remote: "TestStorj:"
|
||||||
fastlist: true
|
fastlist: true
|
||||||
|
|
28
fstest/testserver/init.d/TestSMB
Executable file
28
fstest/testserver/init.d/TestSMB
Executable file
|
@ -0,0 +1,28 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
NAME=smb
|
||||||
|
USER=rclone
|
||||||
|
PASS=GNF3Cqeu
|
||||||
|
WORKGROUP=thepub
|
||||||
|
|
||||||
|
. $(dirname "$0")/docker.bash
|
||||||
|
|
||||||
|
start() {
|
||||||
|
docker run --rm -d --name $NAME dperson/samba \
|
||||||
|
-p \
|
||||||
|
-u "rclone;${PASS}" \
|
||||||
|
-w "${WORKGROUP}" \
|
||||||
|
-s "public;/share" \
|
||||||
|
-s "rclone;/rclone;yes;no;no;rclone"
|
||||||
|
|
||||||
|
echo type=smb
|
||||||
|
echo host=$(docker_ip)
|
||||||
|
echo user=$USER
|
||||||
|
echo pass=$(rclone obscure $PASS)
|
||||||
|
echo domain=$WORKGROUP
|
||||||
|
echo _connect=$(docker_ip):139
|
||||||
|
}
|
||||||
|
|
||||||
|
. $(dirname "$0")/run.bash
|
2
go.mod
2
go.mod
|
@ -84,6 +84,7 @@ require (
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/gdamore/encoding v1.0.0 // indirect
|
github.com/gdamore/encoding v1.0.0 // indirect
|
||||||
|
github.com/geoffgarside/ber v1.1.0 // indirect
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
|
@ -92,6 +93,7 @@ require (
|
||||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||||
|
github.com/hirochachacha/go-smb2 v1.1.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
|
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
|
||||||
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
|
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
|
||||||
|
|
5
go.sum
5
go.sum
|
@ -204,6 +204,8 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk
|
||||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||||
github.com/gdamore/tcell/v2 v2.5.2 h1:tKzG29kO9p2V++3oBY2W9zUjYu7IK1MENFeY/BzJSVY=
|
github.com/gdamore/tcell/v2 v2.5.2 h1:tKzG29kO9p2V++3oBY2W9zUjYu7IK1MENFeY/BzJSVY=
|
||||||
github.com/gdamore/tcell/v2 v2.5.2/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
|
github.com/gdamore/tcell/v2 v2.5.2/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
|
||||||
|
github.com/geoffgarside/ber v1.1.0 h1:qTmFG4jJbwiSzSXoNJeHcOprVzZ8Ulde2Rrrifu5U9w=
|
||||||
|
github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||||
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||||
|
@ -353,6 +355,8 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C
|
||||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
|
||||||
|
github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
|
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
@ -676,6 +680,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
|
Loading…
Reference in New Issue
Block a user