Skip to content

Commit

Permalink
sftp support
Browse files Browse the repository at this point in the history
  • Loading branch information
balazsgrill committed Aug 13, 2024
1 parent a1592b3 commit 3e9150c
Show file tree
Hide file tree
Showing 16 changed files with 488 additions and 73 deletions.
22 changes: 22 additions & 0 deletions bindings/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"syscall"
"time"

"github.com/balazsgrill/potatodrive/bindings/s3"
"github.com/balazsgrill/potatodrive/bindings/sftp"
"github.com/balazsgrill/potatodrive/win"
cfapi "github.com/balazsgrill/potatodrive/win/cfapi/filesystem"
prjfs "github.com/balazsgrill/potatodrive/win/projfs/filesystem"
Expand All @@ -20,6 +22,16 @@ import (

const UseCFAPI bool = true

type BindingConfig interface {
Validate() error
ToFileSystem() (afero.Fs, error)
}

type BaseConfig struct {
LocalPath string `flag:"localpath,Local folder" reg:"LocalPath"`
Type string `flag:"type,Type of binding" reg:"Type"`
}

func ConfigToFlags(config any) {
structPtrValue := reflect.ValueOf(config)
structValue := structPtrValue.Elem()
Expand All @@ -42,6 +54,16 @@ func ConfigToFlags(config any) {
}
}

func CreateConfigByType(typestr string) BindingConfig {
switch typestr {
case "afero-s3":
return &s3.Config{}
case "afero-sftp":
return &sftp.Config{}
}
return nil
}

func ReadConfigFromRegistry(key registry.Key, config any) error {
structPtrValue := reflect.ValueOf(config)
structValue := structPtrValue.Elem()
Expand Down
8 changes: 2 additions & 6 deletions bindings/s3/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/balazsgrill/potatodrive/bindings"
"github.com/balazsgrill/potatodrive/bindings/utils"
s3 "github.com/fclairamb/afero-s3"
"github.com/spf13/afero"
)

type Config struct {
LocalPath string `flag:"localpath,Local folder" reg:"LocalPath"`
Endpoint string `flag:"endpoint,S3 endpoint" reg:"Endpoint"`
Region string `flag:"region,Region" reg:"Region"`
Bucket string `flag:"bucket,Bucket" reg:"Bucket"`
Expand All @@ -25,9 +24,6 @@ func (c *Config) Validate() error {
if c.Endpoint == "" {
return errors.New("endpoint is mandatory")
}
if c.LocalPath == "" {
return errors.New("localpath is mandatory")
}
if c.Region == "" {
return errors.New("region is mandatory")
}
Expand Down Expand Up @@ -57,6 +53,6 @@ func (c *Config) ToFileSystem() (afero.Fs, error) {

fs := s3.NewFs(c.Bucket, sess)
fs.MkdirAll("root", 0777)
rootfs := bindings.NewBasePathFs(fs, "root")
rootfs := utils.NewBasePathFs(fs, "root")
return rootfs, nil
}
70 changes: 70 additions & 0 deletions bindings/sftp/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package sftp

import (
"errors"

"github.com/balazsgrill/potatodrive/bindings/utils"
sftpclient "github.com/pkg/sftp"
"github.com/spf13/afero"
"github.com/spf13/afero/sftpfs"
"golang.org/x/crypto/ssh"
)

type Config struct {
User string `flag:"user,User name" reg:"User"`
Password string `flag:"password,Password" reg:"Password"`
Host string `flag:"host,Host:port" reg:"Host"`
Basepath string `flag:"basepath,Base path on remote server" reg:"Basepath"`
}

func (c *Config) Validate() error {
if c.Host == "" {
return errors.New("host is mandatory")
}
if c.User == "" {
return errors.New("user is mandatory")
}
if c.Password == "" {
return errors.New("password is mandatory")
}
return nil
}

func (c *Config) Connect(onDisconnect func(error)) (afero.Fs, error) {
config := ssh.ClientConfig{
User: c.User,
Auth: []ssh.AuthMethod{
ssh.Password(c.Password),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
conn, err := ssh.Dial("tcp", c.Host, &config)
if err != nil {
return nil, err
}

client, err := sftpclient.NewClient(conn)
if err != nil {
conn.Close()
return nil, err
}

go func() {
err := conn.Wait()
client.Close()
onDisconnect(err)
}()

return sftpfs.New(client), nil
}

func (c *Config) ToFileSystem() (afero.Fs, error) {
var remote afero.Fs
remote = &utils.ConnectingFs{
Connect: c.Connect,
}
if c.Basepath != "" {
remote = utils.NewBasePathFs(remote, c.Basepath)
}
return remote, nil
}
2 changes: 1 addition & 1 deletion bindings/basepathfs.go → bindings/utils/basepathfs.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
This file is a fork of https://github.com/spf13/afero/blob/master/basepath.go modified to always use "/" as separator regardless of the host system
*/
package bindings
package utils

import (
"io/fs"
Expand Down
116 changes: 116 additions & 0 deletions bindings/utils/connectingfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package utils

import (
"os"
"sync"
"time"

"github.com/spf13/afero"
)

type ConnectingFs struct {
Connect func(onDisconnect func(error)) (afero.Fs, error)

lock sync.Mutex
currentFs afero.Fs
}

var _ afero.Fs = (*ConnectingFs)(nil)

func (cfs *ConnectingFs) Chmod(name string, mode os.FileMode) error {
return cfs.withFs(func(fs afero.Fs) error {
return fs.Chmod(name, mode)
})
}

func (cfs *ConnectingFs) MkdirAll(path string, perm os.FileMode) error {
return cfs.withFs(func(fs afero.Fs) error {
return fs.MkdirAll(path, perm)
})
}

func (cfs *ConnectingFs) Stat(name string) (os.FileInfo, error) {
var fileInfo os.FileInfo
err := cfs.withFs(func(fs afero.Fs) error {
var err error
fileInfo, err = fs.Stat(name)
return err
})
return fileInfo, err
}
func (cfs *ConnectingFs) Rename(oldname, newname string) error {
return cfs.withFs(func(fs afero.Fs) error {
return fs.Rename(oldname, newname)
})
}
func (cfs *ConnectingFs) RemoveAll(path string) error {
return cfs.withFs(func(fs afero.Fs) error {
return fs.RemoveAll(path)
})
}
func (cfs *ConnectingFs) Remove(name string) error {
return cfs.withFs(func(fs afero.Fs) error {
return fs.Remove(name)
})
}
func (cfs *ConnectingFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
var file afero.File
err := cfs.withFs(func(fs afero.Fs) error {
var err error
file, err = fs.OpenFile(name, flag, perm)
return err
})
return file, err
}
func (cfs *ConnectingFs) Open(name string) (afero.File, error) {
var file afero.File
err := cfs.withFs(func(fs afero.Fs) error {
var err error
file, err = fs.Open(name)
return err
})
return file, err
}
func (cfs *ConnectingFs) Name() string {
return "ConnectingFs"
}
func (cfs *ConnectingFs) Mkdir(name string, perm os.FileMode) error {
return cfs.withFs(func(fs afero.Fs) error {
return fs.Mkdir(name, perm)
})
}
func (cfs *ConnectingFs) Create(name string) (afero.File, error) {
var file afero.File
err := cfs.withFs(func(fs afero.Fs) error {
var err error
file, err = fs.Create(name)
return err
})
return file, err
}
func (cfs *ConnectingFs) Chtimes(name string, atime, mtime time.Time) error {
return cfs.withFs(func(fs afero.Fs) error {
return fs.Chtimes(name, atime, mtime)
})
}
func (cfs *ConnectingFs) Chown(name string, uid, gid int) error {
return cfs.withFs(func(fs afero.Fs) error {
return fs.Chown(name, uid, gid)
})
}
func (cfs *ConnectingFs) withFs(f func(fs afero.Fs) error) error {
cfs.lock.Lock()
defer cfs.lock.Unlock()
if cfs.currentFs == nil {
fs, err := cfs.Connect(func(error) {
cfs.lock.Lock()
defer cfs.lock.Unlock()
cfs.currentFs = nil
})
if err != nil {
return err
}
cfs.currentFs = fs
}
return f(cfs.currentFs)
}
92 changes: 92 additions & 0 deletions bindings/utils/walk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Forked from afero, changed to join path segments by path.Join instead of filepath.Join

package utils

import (
"os"
fpath "path"
"path/filepath"
"sort"

"github.com/spf13/afero"
)

// readDirNames reads the directory named by dirname and returns
// a sorted list of directory entries.
// adapted from https://golang.org/src/path/filepath/path.go
func readDirNames(fs afero.Fs, dirname string) ([]string, error) {
f, err := fs.Open(dirname)
if err != nil {
return nil, err
}
names, err := f.Readdirnames(-1)
f.Close()
if err != nil {
return nil, err
}
sort.Strings(names)
return names, nil
}

// walk recursively descends path, calling walkFn
// adapted from https://golang.org/src/path/filepath/path.go
func walk(fs afero.Fs, path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
err := walkFn(path, info, nil)
if err != nil {
if info.IsDir() && err == filepath.SkipDir {
return nil
}
return err
}

if !info.IsDir() {
return nil
}

names, err := readDirNames(fs, path)
if err != nil {
return walkFn(path, info, err)
}

for _, name := range names {
filename := fpath.Join(path, name)
fileInfo, err := lstatIfPossible(fs, filename)
if err != nil {
if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
return err
}
} else {
err = walk(fs, filename, fileInfo, walkFn)
if err != nil {
if !fileInfo.IsDir() || err != filepath.SkipDir {
return err
}
}
}
}
return nil
}

// if the filesystem supports it, use Lstat, else use fs.Stat
func lstatIfPossible(fs afero.Fs, path string) (os.FileInfo, error) {
if lfs, ok := fs.(afero.Lstater); ok {
fi, _, err := lfs.LstatIfPossible(path)
return fi, err
}
return fs.Stat(path)
}

// Walk walks the file tree rooted at root, calling walkFn for each file or
// directory in the tree, including root. All errors that arise visiting files
// and directories are filtered by walkFn. The files are walked in lexical
// order, which makes the output deterministic but means that for very
// large directories Walk can be inefficient.
// Walk does not follow symbolic links.

func Walk(fs afero.Fs, root string, walkFn filepath.WalkFunc) error {
info, err := lstatIfPossible(fs, root)
if err != nil {
return walkFn(root, nil, err)
}
return walk(fs, root, info, walkFn)
}
Loading

0 comments on commit 3e9150c

Please sign in to comment.