From 18c82e79b558885d39f44cfdc71bb7b271e4a610 Mon Sep 17 00:00:00 2001 From: Xhofe Date: Sat, 2 Apr 2022 19:28:43 +0800 Subject: [PATCH] feat: sftp support --- drivers/all.go | 1 + drivers/sftp/driver.go | 220 +++++++++++++++++++++++++++++++++++++++++ drivers/sftp/sftp.go | 109 ++++++++++++++++++++ drivers/sftp/types.go | 18 ++++ drivers/sftp/util.go | 3 + go.mod | 7 +- go.sum | 15 ++- 7 files changed, 367 insertions(+), 6 deletions(-) create mode 100644 drivers/sftp/driver.go create mode 100644 drivers/sftp/sftp.go create mode 100644 drivers/sftp/types.go create mode 100644 drivers/sftp/util.go diff --git a/drivers/all.go b/drivers/all.go index a50357fd..b9275931 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -18,6 +18,7 @@ import ( _ "github.com/Xhofe/alist/drivers/pikpak" _ "github.com/Xhofe/alist/drivers/quark" _ "github.com/Xhofe/alist/drivers/s3" + _ "github.com/Xhofe/alist/drivers/sftp" _ "github.com/Xhofe/alist/drivers/shandian" _ "github.com/Xhofe/alist/drivers/teambition" _ "github.com/Xhofe/alist/drivers/uss" diff --git a/drivers/sftp/driver.go b/drivers/sftp/driver.go new file mode 100644 index 00000000..1b61b536 --- /dev/null +++ b/drivers/sftp/driver.go @@ -0,0 +1,220 @@ +package template + +import ( + "github.com/Xhofe/alist/conf" + "github.com/Xhofe/alist/drivers/base" + "github.com/Xhofe/alist/model" + "github.com/Xhofe/alist/utils" + "io" + "path" + "path/filepath" +) + +type SFTP struct { +} + +func (driver SFTP) Config() base.DriverConfig { + return base.DriverConfig{ + Name: "SFTP", + OnlyProxy: true, + OnlyLocal: true, + LocalSort: true, + } +} + +func (driver SFTP) Items() []base.Item { + // TODO fill need info + return []base.Item{ + { + Name: "site_url", + Label: "ip/host", + Type: base.TypeString, + Required: true, + }, + { + Name: "limit", + Label: "port", + Type: base.TypeNumber, + Required: true, + Default: "22", + }, + { + Name: "username", + Label: "username", + Type: base.TypeString, + Required: true, + }, + { + Name: "password", + Label: "password", + Type: base.TypeString, + Required: true, + }, + { + Name: "root_folder", + Label: "root folder path", + Type: base.TypeString, + Default: "/", + Required: true, + }, + } +} + +func (driver SFTP) Save(account *model.Account, old *model.Account) error { + if old != nil { + clientsMap.Lock() + defer clientsMap.Unlock() + delete(clientsMap.clients, old.Name) + } + if account == nil { + return nil + } + _, err := GetClient(account) + if err != nil { + account.Status = err.Error() + } else { + account.Status = "work" + } + _ = model.SaveAccount(account) + return err +} + +func (driver SFTP) File(path string, account *model.Account) (*model.File, error) { + path = utils.ParsePath(path) + if path == "/" { + return &model.File{ + Id: account.RootFolder, + Name: account.Name, + Size: 0, + Type: conf.FOLDER, + Driver: driver.Config().Name, + UpdatedAt: account.UpdatedAt, + }, nil + } + dir, name := filepath.Split(path) + files, err := driver.Files(dir, account) + if err != nil { + return nil, err + } + for _, file := range files { + if file.Name == name { + return &file, nil + } + } + return nil, base.ErrPathNotFound +} + +func (driver SFTP) Files(path string, account *model.Account) ([]model.File, error) { + path = utils.ParsePath(path) + remotePath := utils.Join(account.RootFolder, path) + cache, err := base.GetCache(path, account) + if err == nil { + files, _ := cache.([]model.File) + return files, nil + } + client, err := GetClient(account) + if err != nil { + return nil, err + } + var files []model.File + rawFiles, err := client.Files(remotePath) + if err != nil { + return nil, err + } + for i := 0; i < len(rawFiles); i++ { + files = append(files, driver.formatFile(rawFiles[i])) + } + if len(files) > 0 { + _ = base.SetCache(path, files, account) + } + return files, nil +} + +func (driver SFTP) Link(args base.Args, account *model.Account) (*base.Link, error) { + client, err := GetClient(account) + if err != nil { + return nil, err + } + remoteFileName := utils.Join(account.RootFolder, args.Path) + remoteFile, err := client.Open(remoteFileName) + if err != nil { + return nil, err + } + return &base.Link{ + Data: remoteFile, + }, nil +} + +func (driver SFTP) Path(path string, account *model.Account) (*model.File, []model.File, error) { + path = utils.ParsePath(path) + file, err := driver.File(path, account) + if err != nil { + return nil, nil, err + } + if !file.IsDir() { + return file, nil, nil + } + files, err := driver.Files(path, account) + if err != nil { + return nil, nil, err + } + return nil, files, nil +} + +func (driver SFTP) Preview(path string, account *model.Account) (interface{}, error) { + //TODO preview interface if driver support + return nil, base.ErrNotImplement +} + +func (driver SFTP) MakeDir(path string, account *model.Account) error { + client, err := GetClient(account) + if err != nil { + return err + } + return client.MkdirAll(utils.Join(account.RootFolder, path)) +} + +func (driver SFTP) Move(src string, dst string, account *model.Account) error { + return driver.Rename(src, dst, account) +} + +func (driver SFTP) Rename(src string, dst string, account *model.Account) error { + client, err := GetClient(account) + if err != nil { + return err + } + return client.Rename(utils.Join(account.RootFolder, src), utils.Join(account.RootFolder, dst)) +} + +func (driver SFTP) Copy(src string, dst string, account *model.Account) error { + return base.ErrNotSupport +} + +func (driver SFTP) Delete(path string, account *model.Account) error { + client, err := GetClient(account) + if err != nil { + return err + } + return client.Remove(utils.Join(account.RootFolder, path)) +} + +func (driver SFTP) Upload(file *model.FileStream, account *model.Account) error { + if file == nil { + return base.ErrEmptyFile + } + client, err := GetClient(account) + if err != nil { + return err + } + dstFile, err := client.Create(path.Join(account.RootFolder, file.ParentPath, file.Name)) + if err != nil { + return err + } + defer func() { + _ = dstFile.Close() + }() + _, err = io.Copy(dstFile, file) + return err +} + +var _ base.Driver = (*SFTP)(nil) diff --git a/drivers/sftp/sftp.go b/drivers/sftp/sftp.go new file mode 100644 index 00000000..eb9b0acb --- /dev/null +++ b/drivers/sftp/sftp.go @@ -0,0 +1,109 @@ +package template + +import ( + "github.com/Xhofe/alist/conf" + "github.com/Xhofe/alist/drivers/base" + "github.com/Xhofe/alist/model" + "github.com/Xhofe/alist/utils" + "github.com/pkg/sftp" + "golang.org/x/crypto/ssh" + "os" + "path" + "sync" +) + +var clientsMap = struct { + sync.Mutex + clients map[string]*Client +}{clients: make(map[string]*Client)} + +func GetClient(account *model.Account) (*Client, error) { + clientsMap.Lock() + defer clientsMap.Unlock() + if v, ok := clientsMap.clients[account.Name]; ok { + return v, nil + } + conn, err := ssh.Dial("tcp", account.SiteUrl, &ssh.ClientConfig{ + User: account.Username, + Auth: []ssh.AuthMethod{ssh.Password(account.Password)}, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + }) + if err != nil { + return nil, err + } + client, err := sftp.NewClient(conn) + if err != nil { + return nil, err + } + c := &Client{client} + clientsMap.clients[account.Name] = c + return c, nil +} + +type Client struct { + *sftp.Client +} + +func (client *Client) Files(remotePath string) ([]os.FileInfo, error) { + return client.ReadDir(remotePath) +} + +func (client *Client) Remove(remotePath string) error { + f, err := client.Stat(remotePath) + if err != nil { + return nil + } + if f.IsDir() { + return client.removeDirectory(remotePath) + } else { + return client.removeFile(remotePath) + } +} + +func (client *Client) removeDirectory(remotePath string) error { + //打不开,说明要么文件路径错误了,要么是第一次部署 + remoteFiles, err := client.ReadDir(remotePath) + if err != nil { + return err + } + for _, backupDir := range remoteFiles { + remoteFilePath := path.Join(remotePath, backupDir.Name()) + if backupDir.IsDir() { + err := client.removeDirectory(remoteFilePath) + if err != nil { + return err + } + } else { + err := client.Remove(path.Join(remoteFilePath)) + if err != nil { + return err + } + } + } + return client.RemoveDirectory(remotePath) +} + +func (client *Client) removeFile(remotePath string) error { + return client.Remove(utils.Join(remotePath)) +} + +func (driver SFTP) formatFile(f os.FileInfo) model.File { + t := f.ModTime() + file := model.File{ + //Id: f.Id, + Name: f.Name(), + Size: f.Size(), + Driver: driver.Config().Name, + UpdatedAt: &t, + } + if f.IsDir() { + file.Type = conf.FOLDER + } else { + file.Type = utils.GetFileType(path.Ext(f.Name())) + } + return file +} + +func init() { + base.RegisterDriver(&SFTP{}) +} diff --git a/drivers/sftp/types.go b/drivers/sftp/types.go new file mode 100644 index 00000000..fbcc8268 --- /dev/null +++ b/drivers/sftp/types.go @@ -0,0 +1,18 @@ +package template + +import "time" + +// write all struct here + +type Resp struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type File struct { + Id string `json:"id"` + FileName string `json:"file_name"` + Size int64 `json:"size"` + File bool `json:"file"` + UpdatedAt *time.Time `json:"updated_at"` +} diff --git a/drivers/sftp/util.go b/drivers/sftp/util.go new file mode 100644 index 00000000..914c349f --- /dev/null +++ b/drivers/sftp/util.go @@ -0,0 +1,3 @@ +package template + +// write util func here, such as cal sign diff --git a/go.mod b/go.mod index 3bb04aa6..1f4df15a 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/jlaffaye/ftp v0.0.0-20211117213618-11820403398b github.com/json-iterator/go v1.1.12 github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/pkg/sftp v1.13.4 github.com/robfig/cron/v3 v3.0.0 github.com/sirupsen/logrus v1.8.1 github.com/studio-b12/gowebdav v0.0.0-20211109083228-3f8721cd4b6f @@ -24,6 +25,8 @@ require ( gorm.io/gorm v1.23.1 ) +require github.com/kr/fs v0.1.0 // indirect + require ( github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect github.com/fatih/color v1.13.0 @@ -73,9 +76,9 @@ require ( go.opentelemetry.io/otel v0.20.0 // indirect go.opentelemetry.io/otel/metric v0.20.0 // indirect go.opentelemetry.io/otel/trace v0.20.0 // indirect - golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect + golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect - golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70 // indirect + golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect diff --git a/go.sum b/go.sum index 7e63ced7..51987c38 100644 --- a/go.sum +++ b/go.sum @@ -307,6 +307,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -420,6 +422,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg= +github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -556,11 +560,12 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U 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-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 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= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -645,12 +650,14 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70 h1:SeSEfdIxyvwGJliREIJhRPPXvW6sDlLT+UQ3B0hD0NA= -golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f h1:rlezHXNlxYWvBCzNses9Dlc7nGFaNMJeqLolcmQSSZY= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=