Repository: AlistGo/alist (language: go)

Issue:
希望v3能挂载电脑共享smb
### Please make sure of the following things

- [ ] I have read the [documentation](https://alist.nn.ci).
- [ ] I'm sure there are no duplicate issues or discussions.
- [ ] I'm sure this feature is not implemented.
- [ ] I'm sure it's a reasonable and popular requirement.

### Description of the feature / 需求描述

希望v3能挂载电脑共享smb

### Suggested solution / 实现思路

_No response_

### Additional context / 附件

_No response_


Discussion / hints:
支持，非常需要！
这个指的是可以直接在Windows文件资源管理器里添加映射网络驱动器吗？
> 这个指的是可以直接在Windows文件资源管理器里添加映射网络驱动器吗？

不。希望Alist可以增加局域网内SMB映射，来访问SMB里内容，特殊场景有需要，比如emby

Fix patch:
diff --git a/drivers/all.go b/drivers/all.go
index 1a17130601f..bbfbf537b35 100644
--- a/drivers/all.go
+++ b/drivers/all.go
@@ -21,6 +21,7 @@ import (
 	_ "github.com/alist-org/alist/v3/drivers/quark"
 	_ "github.com/alist-org/alist/v3/drivers/s3"
 	_ "github.com/alist-org/alist/v3/drivers/sftp"
+	_ "github.com/alist-org/alist/v3/drivers/smb"
 	_ "github.com/alist-org/alist/v3/drivers/teambition"
 	_ "github.com/alist-org/alist/v3/drivers/thunder"
 	_ "github.com/alist-org/alist/v3/drivers/uss"
diff --git a/drivers/smb/driver.go b/drivers/smb/driver.go
new file mode 100644
index 00000000000..4589d5a42a4
--- /dev/null
+++ b/drivers/smb/driver.go
@@ -0,0 +1,163 @@
+package smb
+
+import (
+	"context"
+	"errors"
+	"path/filepath"
+
+	"github.com/alist-org/alist/v3/internal/driver"
+	"github.com/alist-org/alist/v3/internal/model"
+	"github.com/alist-org/alist/v3/pkg/utils"
+
+	"github.com/hirochachacha/go-smb2"
+)
+
+type SMB struct {
+	model.Storage
+	Addition
+	fs *smb2.Share
+}
+
+func (d *SMB) Config() driver.Config {
+	return config
+}
+
+func (d *SMB) GetAddition() driver.Additional {
+	return d.Addition
+}
+
+func (d *SMB) Init(ctx context.Context, storage model.Storage) error {
+	d.Storage = storage
+	err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
+	if err != nil {
+		return err
+	}
+	return d.initFS()
+}
+
+func (d *SMB) Drop(ctx context.Context) error {
+	if d.fs != nil {
+		_ = d.fs.Umount()
+	}
+	return nil
+}
+
+func (d *SMB) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
+	fullPath := d.getSMBPath(dir)
+	rawFiles, err := d.fs.ReadDir(fullPath)
+	if err != nil {
+		return nil, err
+	}
+	var files []model.Obj
+	for _, f := range rawFiles {
+		file := model.ObjThumb{
+			Object: model.Object{
+				Name:     f.Name(),
+				Modified: f.ModTime(),
+				Size:     f.Size(),
+				IsFolder: f.IsDir(),
+			},
+		}
+		files = append(files, &file)
+	}
+	return files, nil
+}
+
+//func (d *SMB) Get(ctx context.Context, path string) (model.Obj, error) {
+//	// this is optional
+//	return nil, errs.NotImplement
+//}
+
+func (d *SMB) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
+	fullPath := d.getSMBPath(file)
+	remoteFile, err := d.fs.Open(fullPath)
+	if err != nil {
+		return nil, err
+	}
+	return &model.Link{
+		Data: remoteFile,
+	}, nil
+}
+
+func (d *SMB) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
+	fullPath := filepath.Join(d.getSMBPath(parentDir), dirName)
+	err := d.fs.MkdirAll(fullPath, 0700)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (d *SMB) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
+	srcPath := d.getSMBPath(srcObj)
+	dstPath := filepath.Join(d.getSMBPath(dstDir), srcObj.GetName())
+	err := d.fs.Rename(srcPath, dstPath)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (d *SMB) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
+	srcPath := d.getSMBPath(srcObj)
+	dstPath := filepath.Join(filepath.Dir(srcPath), newName)
+	err := d.fs.Rename(srcPath, dstPath)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (d *SMB) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
+	srcPath := d.getSMBPath(srcObj)
+	dstPath := filepath.Join(d.getSMBPath(dstDir), srcObj.GetName())
+	var err error
+	if srcObj.IsDir() {
+		err = d.CopyDir(srcPath, dstPath)
+	} else {
+		err = d.CopyFile(srcPath, dstPath)
+	}
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (d *SMB) Remove(ctx context.Context, obj model.Obj) error {
+	var err error
+	fullPath := d.getSMBPath(obj)
+	if obj.IsDir() {
+		err = d.fs.RemoveAll(fullPath)
+	} else {
+		err = d.fs.Remove(fullPath)
+	}
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (d *SMB) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
+	fullPath := filepath.Join(d.getSMBPath(dstDir), stream.GetName())
+	out, err := d.fs.Create(fullPath)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		_ = out.Close()
+		if errors.Is(err, context.Canceled) {
+			_ = d.fs.Remove(fullPath)
+		}
+	}()
+	err = utils.CopyWithCtx(ctx, out, stream, stream.GetSize(), up)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+//func (d *SMB) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
+//	return nil, errs.NotSupport
+//}
+
+var _ driver.Driver = (*SMB)(nil)
diff --git a/drivers/smb/meta.go b/drivers/smb/meta.go
new file mode 100644
index 00000000000..35d88911217
--- /dev/null
+++ b/drivers/smb/meta.go
@@ -0,0 +1,28 @@
+package smb
+
+import (
+	"github.com/alist-org/alist/v3/internal/driver"
+	"github.com/alist-org/alist/v3/internal/op"
+)
+
+type Addition struct {
+	driver.RootPath
+	Address   string `json:"address" required:"true"`
+	Username  string `json:"username" required:"true"`
+	Password  string `json:"password"`
+	ShareName string `json:"share_name" required:"true"`
+}
+
+var config = driver.Config{
+	Name:        "SMB",
+	LocalSort:   true,
+	OnlyLocal:   true,
+	DefaultRoot: ".",
+	NoCache:     true,
+}
+
+func init() {
+	op.RegisterDriver(config, func() driver.Driver {
+		return &SMB{}
+	})
+}
diff --git a/drivers/smb/types.go b/drivers/smb/types.go
new file mode 100644
index 00000000000..161798ade6d
--- /dev/null
+++ b/drivers/smb/types.go
@@ -0,0 +1,1 @@
+package smb
diff --git a/drivers/smb/util.go b/drivers/smb/util.go
new file mode 100644
index 00000000000..144ec171e67
--- /dev/null
+++ b/drivers/smb/util.go
@@ -0,0 +1,122 @@
+package smb
+
+import (
+	"io"
+	"io/fs"
+	"net"
+	"os"
+	"path/filepath"
+
+	"github.com/alist-org/alist/v3/internal/model"
+	"github.com/hirochachacha/go-smb2"
+)
+
+func (d *SMB) initFS() error {
+	conn, err := net.Dial("tcp", d.Address)
+	if err != nil {
+		return err
+	}
+	dialer := &smb2.Dialer{
+		Initiator: &smb2.NTLMInitiator{
+			User:     d.Username,
+			Password: d.Password,
+		},
+	}
+	s, err := dialer.Dial(conn)
+	if err != nil {
+		return err
+	}
+	d.fs, err = s.Mount(d.ShareName)
+	if err != nil {
+		return err
+	}
+	return err
+}
+
+func (d *SMB) getSMBPath(dir model.Obj) string {
+	fullPath := dir.GetPath()
+	if fullPath[0:1] != "." {
+		fullPath = "." + fullPath
+	}
+	return fullPath
+}
+
+// CopyFile File copies a single file from src to dst
+func (d *SMB) CopyFile(src, dst string) error {
+	var err error
+	var srcfd *smb2.File
+	var dstfd *smb2.File
+	var srcinfo fs.FileInfo
+
+	if srcfd, err = d.fs.Open(src); err != nil {
+		return err
+	}
+	defer srcfd.Close()
+
+	if dstfd, err = d.CreateNestedFile(dst); err != nil {
+		return err
+	}
+	defer dstfd.Close()
+
+	if _, err = io.Copy(dstfd, srcfd); err != nil {
+		return err
+	}
+	if srcinfo, err = d.fs.Stat(src); err != nil {
+		return err
+	}
+	return d.fs.Chmod(dst, srcinfo.Mode())
+}
+
+// CopyDir Dir copies a whole directory recursively
+func (d *SMB) CopyDir(src string, dst string) error {
+	var err error
+	var fds []fs.FileInfo
+	var srcinfo fs.FileInfo
+
+	if srcinfo, err = d.fs.Stat(src); err != nil {
+		return err
+	}
+	if err = d.fs.MkdirAll(dst, srcinfo.Mode()); err != nil {
+		return err
+	}
+	if fds, err = d.fs.ReadDir(src); err != nil {
+		return err
+	}
+	for _, fd := range fds {
+		srcfp := filepath.Join(src, fd.Name())
+		dstfp := filepath.Join(dst, fd.Name())
+
+		if fd.IsDir() {
+			if err = d.CopyDir(srcfp, dstfp); err != nil {
+				return err
+			}
+		} else {
+			if err = d.CopyFile(srcfp, dstfp); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+// Exists determine whether the file exists
+func (d *SMB) Exists(name string) bool {
+	if _, err := d.fs.Stat(name); err != nil {
+		if os.IsNotExist(err) {
+			return false
+		}
+	}
+	return true
+}
+
+// CreateNestedFile create nested file
+func (d *SMB) CreateNestedFile(path string) (*smb2.File, error) {
+	basePath := filepath.Dir(path)
+	if !d.Exists(basePath) {
+		err := d.fs.MkdirAll(basePath, 0700)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return d.fs.Create(path)
+}
diff --git a/go.mod b/go.mod
index 25bc96b0537..af38c0cc98c 100644
--- a/go.mod
+++ b/go.mod
@@ -34,6 +34,7 @@ require (
 
 require (
 	github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
+	github.com/geoffgarside/ber v1.1.0 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-playground/locales v0.14.0 // indirect
 	github.com/go-playground/universal-translator v0.18.0 // indirect
@@ -42,6 +43,7 @@ require (
 	github.com/goccy/go-json v0.9.7 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
+	github.com/hirochachacha/go-smb2 v1.1.0 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/jackc/chunkreader/v2 v2.0.1 // indirect
 	github.com/jackc/pgconn v1.12.1 // indirect
diff --git a/go.sum b/go.sum
index 1be3c68490d..fedd6037541 100644
--- a/go.sum
+++ b/go.sum
@@ -22,6 +22,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
 github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
+github.com/geoffgarside/ber v1.1.0 h1:qTmFG4jJbwiSzSXoNJeHcOprVzZ8Ulde2Rrrifu5U9w=
+github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
 github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA=
 github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk=
 github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -67,6 +69,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
+github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
@@ -244,6 +248,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 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-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=


Test patch: