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 }