Files
Mickael Kerjean 39f8fc66aa fix (ftp): recreate connection if closed
An issue would araise when the connection is closed before we had time
to do the vacuum cleaning on servers like on infinity free where "Our server has quite aggressive inactivity timeouts and will kill the connection after only 20 seconds of inactivity."

reference: https://forum.infinityfree.net/t/good-online-ftp/69285/18
2023-04-10 01:28:39 +10:00

384 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
}
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)
go func() {
<-app.Context.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}
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}
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}
break
}
}
if backend == nil {
return nil, ErrAuthenticationFailed
}
backend.wg = new(sync.WaitGroup)
backend.wg.Add(1)
go func() {
<-app.Context.Done()
backend.wg.Done()
}()
FtpCache.Set(params, backend)
return backend, nil
}
func (f Ftp) LoginForm() Form {
return Form{
Elmnts: []FormElement{
FormElement{
Name: "type",
Type: "hidden",
Value: "ftp",
},
FormElement{
Name: "hostname",
Type: "text",
Placeholder: "Hostname*",
},
FormElement{
Name: "username",
Type: "text",
Placeholder: "Username",
},
FormElement{
Name: "password",
Type: "password",
Placeholder: "Password",
},
FormElement{
Name: "advanced",
Type: "enable",
Placeholder: "Advanced",
Target: []string{"ftp_path", "ftp_port", "ftp_conn"},
},
FormElement{
Id: "ftp_path",
Name: "path",
Type: "text",
Placeholder: "Path",
},
FormElement{
Id: "ftp_port",
Name: "port",
Type: "number",
Placeholder: "Port",
},
FormElement{
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: context.Background()}); err == nil {
fn(b.(*Ftp).client)
}
}
}
}