github.com/code-to-go/safepool.lib@v0.0.0-20221205180519-ee25e63c226e/transport/sftp.go (about)

     1  package transport
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/fs"
     8  	"time"
     9  
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"strings"
    14  
    15  	"github.com/code-to-go/safepool.lib/core"
    16  
    17  	"github.com/pkg/sftp"
    18  	"golang.org/x/crypto/ssh"
    19  )
    20  
    21  type SFTPConfig struct {
    22  	Addr     string `json:"addr" yaml:"addr"`
    23  	Username string `json:"username" yaml:"username"`
    24  	Password string `json:"password" yaml:"password"`
    25  	KeyPath  string `json:"keyPath" yaml:"keyPath"`
    26  	Base     string `json:"base" yaml:"base"`
    27  }
    28  
    29  type SFTP struct {
    30  	c     *sftp.Client
    31  	base  string
    32  	url   string
    33  	touch map[string]time.Time
    34  }
    35  
    36  func NewSFTP(config SFTPConfig) (Exchanger, error) {
    37  	addr := config.Addr
    38  	if !strings.ContainsRune(addr, ':') {
    39  		addr = fmt.Sprintf("%s:22", addr)
    40  	}
    41  
    42  	var url string
    43  	var auth []ssh.AuthMethod
    44  	if config.Password != "" {
    45  		auth = append(auth, ssh.Password(config.Password))
    46  		url = fmt.Sprintf("sftp://%s@%s/%s", config.Username, config.Addr, config.Base)
    47  	}
    48  	if config.KeyPath != "" {
    49  		key, err := os.ReadFile(config.KeyPath)
    50  		if err != nil {
    51  			return nil, fmt.Errorf("cannot load key file %s: %v", config.KeyPath, err)
    52  		}
    53  
    54  		signer, err := ssh.ParsePrivateKey(key)
    55  		if err != nil {
    56  			return nil, fmt.Errorf("invalid key file %s: %v", config.KeyPath, err)
    57  		}
    58  		auth = append(auth, ssh.PublicKeys(signer))
    59  		url = fmt.Sprintf("sftp://!%s@%s/%s", filepath.Base(config.KeyPath), config.Addr, config.Base)
    60  	}
    61  	if len(auth) == 0 {
    62  		return nil, fmt.Errorf("no auth method provided for sftp connection to %s", config.Addr)
    63  	}
    64  
    65  	cc := &ssh.ClientConfig{
    66  		User: config.Username,
    67  		Auth: []ssh.AuthMethod{
    68  			ssh.Password(config.Password),
    69  		},
    70  		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    71  	}
    72  
    73  	client, err := ssh.Dial("tcp", addr, cc)
    74  	if err != nil {
    75  		return nil, fmt.Errorf("cannot connect to %s: %v", addr, err)
    76  	}
    77  	c, err := sftp.NewClient(client)
    78  	if err != nil {
    79  		return nil, fmt.Errorf("cannot create a sftp client for %s: %v", addr, err)
    80  	}
    81  
    82  	base := config.Base
    83  	if base == "" {
    84  		base = "/"
    85  	}
    86  	return &SFTP{c, base, url, map[string]time.Time{}}, nil
    87  }
    88  
    89  func (s *SFTP) Touched(name string) bool {
    90  	touchFile := path.Join(s.base, fmt.Sprintf("%s.touch", name))
    91  	stat, err := s.Stat(touchFile)
    92  	touched := err != nil || stat.ModTime().After(s.touch[name])
    93  	if touched {
    94  		if !core.IsErr(s.Write(touchFile, &bytes.Buffer{}), "cannot write touch file: %v") {
    95  			s.touch[name] = stat.ModTime()
    96  		}
    97  	}
    98  	return touched
    99  }
   100  
   101  func (s *SFTP) Read(name string, rang *Range, dest io.Writer) error {
   102  	f, err := s.c.Open(path.Join(s.base, name))
   103  	if core.IsErr(err, "cannot open file on sftp server %v:%v", s) {
   104  		return err
   105  	}
   106  
   107  	if rang == nil {
   108  		_, err = io.Copy(dest, f)
   109  	} else {
   110  		left := rang.To - rang.From
   111  		f.Seek(rang.From, 0)
   112  		var b [4096]byte
   113  
   114  		for left > 0 && err == nil {
   115  			var sz int64
   116  			if rang.From-rang.To > 4096 {
   117  				sz = 4096
   118  			} else {
   119  				sz = rang.From - rang.To
   120  			}
   121  			_, err = f.Read(b[0:sz])
   122  			dest.Write(b[0:sz])
   123  			left -= sz
   124  		}
   125  	}
   126  	if core.IsErr(err, "cannot read from %s/%s:%v", s, name) {
   127  		return err
   128  	}
   129  
   130  	return nil
   131  }
   132  
   133  func (s *SFTP) Write(name string, source io.Reader) error {
   134  	return nil
   135  }
   136  
   137  func (s *SFTP) ReadDir(prefix string, opts ListOption) ([]fs.FileInfo, error) {
   138  	dir, prefix := path.Split(prefix)
   139  	result, err := s.c.ReadDir(dir)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	var infos []fs.FileInfo
   145  	for _, item := range result {
   146  		if strings.HasPrefix(item.Name(), prefix) {
   147  			infos = append(infos, item)
   148  		}
   149  	}
   150  
   151  	return infos, nil
   152  }
   153  
   154  func (s *SFTP) Stat(name string) (os.FileInfo, error) {
   155  	return s.c.Stat(path.Join(s.base, name))
   156  }
   157  
   158  func (s *SFTP) Rename(old, new string) error {
   159  	return s.c.Rename(path.Join(s.base, old), path.Join(s.base, new))
   160  }
   161  
   162  func (s *SFTP) Delete(name string) error {
   163  	return s.c.Remove(path.Join(s.base, name))
   164  }
   165  
   166  func (s *SFTP) Close() error {
   167  	return s.c.Close()
   168  }
   169  
   170  func (s *SFTP) String() string {
   171  	return s.url
   172  }