Files
2024-03-19 07:54:49 +11:00

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)
}
}
}
}