mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-27 19:53:41 +08:00
387 lines
9.1 KiB
Go
387 lines
9.1 KiB
Go
package plg_backend_ftp
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
. "github.com/mickael-kerjean/filestash/server/common"
|
|
//"github.com/secsy/goftp" <- FTP issue with microsoft FTP
|
|
"github.com/prasad83/goftp"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var FtpCache AppCache
|
|
|
|
type Ftp struct {
|
|
client *goftp.Client
|
|
p map[string]string
|
|
wg *sync.WaitGroup
|
|
ctx context.Context
|
|
}
|
|
|
|
func init() {
|
|
Backend.Register("ftp", Ftp{})
|
|
|
|
FtpCache = NewAppCache(2, 1)
|
|
FtpCache.OnEvict(func(key string, value interface{}) {
|
|
c := value.(*Ftp)
|
|
if c == nil {
|
|
Log.Warning("plg_backend_ftp::ftp is nil on close")
|
|
return
|
|
} else if c.wg == nil {
|
|
Log.Warning("plg_backend_ftp::wg is nil on close")
|
|
c.Close()
|
|
return
|
|
}
|
|
c.wg.Wait()
|
|
Log.Debug("plg_backend_ftp::vacuum")
|
|
c.Close()
|
|
})
|
|
}
|
|
|
|
func (f Ftp) Init(params map[string]string, app *App) (IBackend, error) {
|
|
if c := FtpCache.Get(params); c != nil {
|
|
d := c.(*Ftp)
|
|
if d == nil {
|
|
Log.Warning("plg_backend_ftp::ftp is nil on get")
|
|
return nil, ErrInternal
|
|
} else if d.wg == nil {
|
|
Log.Warning("plg_backend_ftp::wg is nil on get")
|
|
return nil, ErrInternal
|
|
}
|
|
d.wg.Add(1)
|
|
d.ctx = app.Context
|
|
go func() {
|
|
<-d.ctx.Done()
|
|
d.wg.Done()
|
|
}()
|
|
return d, nil
|
|
}
|
|
if params["hostname"] == "" {
|
|
params["hostname"] = "localhost"
|
|
}
|
|
|
|
if params["port"] == "" {
|
|
params["port"] = "21"
|
|
}
|
|
if params["username"] == "" {
|
|
params["acl"] = "r"
|
|
params["username"] = "anonymous"
|
|
}
|
|
if params["username"] == "anonymous" && params["password"] == "" {
|
|
params["password"] = "anonymous"
|
|
}
|
|
conn := 5
|
|
if params["conn"] != "" {
|
|
if i, err := strconv.Atoi(params["conn"]); err == nil && i > 0 {
|
|
conn = i
|
|
}
|
|
}
|
|
|
|
connectStrategy := []string{"ftp", "ftps::implicit", "ftps::explicit"}
|
|
if strings.HasPrefix(params["hostname"], "ftp://") {
|
|
connectStrategy = []string{"ftp"}
|
|
params["hostname"] = strings.TrimPrefix(params["hostname"], "ftp://")
|
|
} else if strings.HasPrefix(params["hostname"], "ftps://") {
|
|
connectStrategy = []string{"ftps::implicit", "ftps::explicit"}
|
|
params["hostname"] = strings.TrimPrefix(params["hostname"], "ftps://")
|
|
}
|
|
|
|
var backend *Ftp = nil
|
|
hostname := fmt.Sprintf("%s:%s", params["hostname"], params["port"])
|
|
cfgBuilder := func(timeout time.Duration, withTLS bool) goftp.Config {
|
|
cfg := goftp.Config{
|
|
User: params["username"],
|
|
Password: params["password"],
|
|
ConnectionsPerHost: conn,
|
|
Timeout: timeout * time.Second,
|
|
}
|
|
cfg.Timeout = timeout
|
|
if withTLS {
|
|
cfg.TLSConfig = &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
ClientSessionCache: tls.NewLRUClientSessionCache(0),
|
|
SessionTicketsDisabled: false,
|
|
ServerName: hostname,
|
|
}
|
|
}
|
|
return cfg
|
|
}
|
|
for i := 0; i < len(connectStrategy); i++ {
|
|
if connectStrategy[i] == "ftp" {
|
|
client, err := goftp.DialConfig(cfgBuilder(5*time.Second, false), hostname)
|
|
if err != nil {
|
|
Log.Debug("plg_backend_ftp::ftp dial %s", err.Error())
|
|
continue
|
|
} else if _, err := client.ReadDir("/"); err != nil {
|
|
client.Close()
|
|
Log.Debug("plg_backend_ftp::ftp verify %s", err.Error())
|
|
continue
|
|
}
|
|
client.Close()
|
|
client, err = goftp.DialConfig(cfgBuilder(60*time.Second, false), hostname)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
params["mode"] = connectStrategy[i]
|
|
backend = &Ftp{client, params, nil, app.Context}
|
|
break
|
|
} else if connectStrategy[i] == "ftps::implicit" {
|
|
cfg := cfgBuilder(60*time.Second, true)
|
|
cfg.TLSMode = goftp.TLSImplicit
|
|
client, err := goftp.DialConfig(cfg, hostname)
|
|
if err != nil {
|
|
Log.Debug("plg_backend_ftp::ftps::implicit dial %s", err.Error())
|
|
continue
|
|
} else if _, err := client.ReadDir("/"); err != nil {
|
|
Log.Debug("plg_backend_ftp::ftps::implicit verify %s", err.Error())
|
|
client.Close()
|
|
continue
|
|
}
|
|
params["mode"] = connectStrategy[i]
|
|
backend = &Ftp{client, params, nil, app.Context}
|
|
break
|
|
} else if connectStrategy[i] == "ftps::explicit" {
|
|
cfg := cfgBuilder(5*time.Second, true)
|
|
cfg.TLSMode = goftp.TLSExplicit
|
|
client, err := goftp.DialConfig(cfg, hostname)
|
|
if err != nil {
|
|
Log.Debug("plg_backend_ftp::ftps::explicit dial '%s'", err.Error())
|
|
continue
|
|
} else if _, err := client.ReadDir("/"); err != nil {
|
|
Log.Debug("plg_backend_ftp::ftps::explicit verify %s", err.Error())
|
|
client.Close()
|
|
continue
|
|
}
|
|
client.Close()
|
|
cfg = cfgBuilder(60*time.Second, true)
|
|
cfg.TLSMode = goftp.TLSExplicit
|
|
client, err = goftp.DialConfig(cfg, hostname)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
params["mode"] = connectStrategy[i]
|
|
backend = &Ftp{client, params, nil, app.Context}
|
|
break
|
|
}
|
|
}
|
|
if backend == nil {
|
|
return nil, ErrAuthenticationFailed
|
|
}
|
|
backend.wg = new(sync.WaitGroup)
|
|
backend.wg.Add(1)
|
|
backend.ctx = app.Context
|
|
go func() {
|
|
<-backend.ctx.Done()
|
|
backend.wg.Done()
|
|
}()
|
|
FtpCache.Set(params, backend)
|
|
return backend, nil
|
|
}
|
|
|
|
func (f Ftp) LoginForm() Form {
|
|
return Form{
|
|
Elmnts: []FormElement{
|
|
{
|
|
Name: "type",
|
|
Type: "hidden",
|
|
Value: "ftp",
|
|
},
|
|
{
|
|
Name: "hostname",
|
|
Type: "text",
|
|
Placeholder: "Hostname*",
|
|
},
|
|
{
|
|
Name: "username",
|
|
Type: "text",
|
|
Placeholder: "Username",
|
|
},
|
|
{
|
|
Name: "password",
|
|
Type: "password",
|
|
Placeholder: "Password",
|
|
},
|
|
{
|
|
Name: "advanced",
|
|
Type: "enable",
|
|
Placeholder: "Advanced",
|
|
Target: []string{"ftp_path", "ftp_port", "ftp_conn"},
|
|
},
|
|
{
|
|
Id: "ftp_path",
|
|
Name: "path",
|
|
Type: "text",
|
|
Placeholder: "Path",
|
|
},
|
|
{
|
|
Id: "ftp_port",
|
|
Name: "port",
|
|
Type: "number",
|
|
Placeholder: "Port",
|
|
},
|
|
{
|
|
Id: "ftp_conn",
|
|
Name: "conn",
|
|
Type: "number",
|
|
Placeholder: "Number of connections",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (f Ftp) Meta(path string) Metadata {
|
|
if f.p["acl"] == "r" {
|
|
return Metadata{
|
|
CanCreateFile: NewBool(false),
|
|
CanCreateDirectory: NewBool(false),
|
|
CanRename: NewBool(false),
|
|
CanMove: NewBool(false),
|
|
CanUpload: NewBool(false),
|
|
CanDelete: NewBool(false),
|
|
}
|
|
}
|
|
return Metadata{}
|
|
}
|
|
|
|
func (f Ftp) Home() (home string, err error) {
|
|
f.Execute(func(client *goftp.Client) error {
|
|
home, err = f.client.Getwd()
|
|
return err
|
|
})
|
|
return home, err
|
|
}
|
|
|
|
func (f Ftp) Ls(path string) (files []os.FileInfo, err error) {
|
|
f.Execute(func(client *goftp.Client) error {
|
|
files, err = client.ReadDir(path)
|
|
return err
|
|
})
|
|
return files, err
|
|
}
|
|
|
|
func (f Ftp) Cat(path string) (reader io.ReadCloser, err error) {
|
|
f.Execute(func(client *goftp.Client) error {
|
|
if _, err = client.Stat(path); err != nil {
|
|
return err
|
|
}
|
|
pr, pw := io.Pipe()
|
|
go func() {
|
|
err = client.Retrieve(path, pw)
|
|
if err != nil {
|
|
pr.CloseWithError(NewError("Problem", 409))
|
|
}
|
|
pw.Close()
|
|
}()
|
|
reader = pr
|
|
return nil
|
|
})
|
|
return reader, err
|
|
}
|
|
|
|
func (f Ftp) Mkdir(path string) (err error) {
|
|
f.Execute(func(client *goftp.Client) error {
|
|
_, err = client.Mkdir(path)
|
|
return err
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (f Ftp) Rm(path string) (err error) {
|
|
isDirectory := func(p string) bool {
|
|
return regexp.MustCompile(`\/$`).MatchString(p)
|
|
}
|
|
transformError := func(e error) error {
|
|
// For some reasons bsftp is struggling with the library
|
|
// sometimes returning a 200 OK
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
if obj, ok := e.(goftp.Error); ok {
|
|
if obj.Code() < 300 && obj.Code() > 0 {
|
|
return nil
|
|
}
|
|
}
|
|
return e
|
|
}
|
|
var recursiveDelete func(client *goftp.Client, _path string) error
|
|
recursiveDelete = func(client *goftp.Client, _path string) error {
|
|
if isDirectory(_path) {
|
|
entries, err := client.ReadDir(_path)
|
|
if transformError(err) != nil {
|
|
return err
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
err = recursiveDelete(client, _path+entry.Name()+"/")
|
|
if transformError(err) != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
err = recursiveDelete(client, _path+entry.Name())
|
|
if transformError(err) != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
err = client.Rmdir(_path)
|
|
return transformError(err)
|
|
}
|
|
err = client.Delete(_path)
|
|
return transformError(err)
|
|
}
|
|
f.Execute(func(client *goftp.Client) error {
|
|
err = recursiveDelete(client, path)
|
|
return err
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (f Ftp) Mv(from string, to string) (err error) {
|
|
f.Execute(func(client *goftp.Client) error {
|
|
err = client.Rename(from, to)
|
|
return err
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (f Ftp) Touch(path string) (err error) {
|
|
f.Execute(func(client *goftp.Client) error {
|
|
err = client.Store(path, strings.NewReader(""))
|
|
return err
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (f Ftp) Save(path string, file io.Reader) (err error) {
|
|
f.Execute(func(client *goftp.Client) error {
|
|
err = client.Store(path, file)
|
|
return err
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (f Ftp) Close() error {
|
|
return f.client.Close()
|
|
}
|
|
|
|
func (f Ftp) Execute(fn func(*goftp.Client) error) {
|
|
err := fn(f.client)
|
|
if ftpErr, ok := err.(goftp.Error); ok {
|
|
code := ftpErr.Code()
|
|
if code == 421 || (code == 0 && err.Error() == "error reading response: EOF") {
|
|
f.Close()
|
|
FtpCache.Set(f.p, nil)
|
|
if b, err := f.Init(f.p, &App{Context: f.ctx}); err == nil {
|
|
fn(b.(*Ftp).client)
|
|
}
|
|
}
|
|
}
|
|
}
|