github.com/viant/toolbox@v0.34.5/storage/scp/service.go (about) 1 package scp 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "github.com/lunixbochs/vtclean" 8 "github.com/viant/toolbox" 9 "github.com/viant/toolbox/cred" 10 "github.com/viant/toolbox/ssh" 11 "github.com/viant/toolbox/storage" 12 "io" 13 "io/ioutil" 14 "net/url" 15 "os" 16 "path" 17 "strings" 18 "sync" 19 ) 20 21 const ( 22 defaultSSHPort = 22 23 verificationSizeThreshold = 1024 * 1024 24 ) 25 26 //NoSuchFileOrDirectoryError represents no such file or directory error 27 var NoSuchFileOrDirectoryError = errors.New("No such file or directory") 28 29 const unrecognizedOption = "unrecognized option" 30 31 type service struct { 32 fileService storage.Service 33 config *cred.Config 34 services map[string]ssh.Service 35 multiSessions map[string]ssh.MultiCommandSession 36 mutex *sync.Mutex 37 } 38 39 func (s *service) runCommand(session ssh.MultiCommandSession, URL string, command string) (string, error) { 40 s.mutex.Lock() 41 defer s.mutex.Unlock() 42 output, _ := session.Run(command, nil, 5000) 43 var stdout = s.stdout(output) 44 return stdout, nil 45 } 46 47 func (s *service) stdout(output string) string { 48 var result = make([]string, 0) 49 lines := strings.Split(output, "\n") 50 for _, line := range lines { 51 result = append(result, vtclean.Clean(line, false)) 52 } 53 return strings.Join(result, "\n") 54 } 55 56 func (s *service) getMultiSession(parsedURL *url.URL) ssh.MultiCommandSession { 57 s.mutex.Lock() 58 defer s.mutex.Unlock() 59 return s.multiSessions[parsedURL.Host] 60 } 61 62 func (s *service) getService(parsedURL *url.URL) (ssh.Service, error) { 63 port := toolbox.AsInt(parsedURL.Port()) 64 if port == 0 { 65 port = 22 66 } 67 key := parsedURL.Host 68 s.mutex.Lock() 69 defer s.mutex.Unlock() 70 if service, ok := s.services[key]; ok { 71 return service, nil 72 } 73 service, err := ssh.NewService(parsedURL.Hostname(), toolbox.AsInt(port), s.config) 74 if err != nil { 75 return nil, err 76 } 77 s.services[key] = service 78 s.multiSessions[key], err = service.OpenMultiCommandSession(nil) 79 if err != nil { 80 return nil, err 81 } 82 return service, nil 83 } 84 85 //List returns a list of object for supplied URL 86 func (s *service) List(URL string) ([]storage.Object, error) { 87 parsedURL, err := url.Parse(URL) 88 if err != nil { 89 return nil, err 90 } 91 if parsedURL.Host == "127.0.0.1" || parsedURL.Host == "127.0.0.1:22" { 92 var fileURL = toolbox.FileSchema + parsedURL.Path 93 return s.fileService.List(fileURL) 94 } 95 _, err = s.getService(parsedURL) 96 if err != nil { 97 return nil, err 98 } 99 commandSession := s.getMultiSession(parsedURL) 100 canListWithTimeStyle := commandSession.System() != "darwin" 101 var parser = &Parser{IsoTimeStyle: canListWithTimeStyle} 102 var URLPath = parsedURL.Path 103 var result = make([]storage.Object, 0) 104 var lsCommand = "" 105 106 if canListWithTimeStyle { 107 lsCommand += "ls -dltr --time-style=full-iso " + URLPath 108 } else { 109 lsCommand += "ls -dltrT " + URLPath 110 } 111 output, _ := s.runCommand(commandSession, URL, lsCommand) 112 var stdout = vtclean.Clean(string(output), false) 113 if strings.Contains(stdout, "unrecognized option") { 114 if canListWithTimeStyle { 115 lsCommand = "ls -dltr --full-time " + URLPath 116 output, _ = s.runCommand(commandSession, URL, lsCommand) 117 stdout = vtclean.Clean(string(output), false) 118 } 119 } 120 121 if strings.Contains(stdout, unrecognizedOption) { 122 return nil, fmt.Errorf("unable to list files with: %v, %v", lsCommand, stdout) 123 } 124 125 if strings.Contains(stdout, "No such file or directory") { 126 return result, NoSuchFileOrDirectoryError 127 } 128 objects, err := parser.Parse(parsedURL, stdout, false) 129 if err != nil { 130 return nil, err 131 } 132 if len(objects) == 1 && objects[0].FileInfo().IsDir() { 133 output, _ = s.runCommand(commandSession, URL, lsCommand+" "+path.Join(URLPath, "*")) 134 stdout = vtclean.Clean(string(output), false) 135 directoryObjects, err := parser.Parse(parsedURL, stdout, true) 136 if err != nil { 137 return nil, err 138 } 139 if len(directoryObjects) > 0 { 140 objects = append(objects, directoryObjects...) 141 } 142 } 143 return objects, nil 144 } 145 146 func (s *service) Exists(URL string) (bool, error) { 147 parsedURL, err := url.Parse(URL) 148 if err != nil { 149 return false, err 150 } 151 152 if parsedURL.Host == "127.0.0.1" || parsedURL.Host == "127.0.0.1:22" { 153 var fileURL = toolbox.FileSchema + parsedURL.Path 154 return s.fileService.Exists(fileURL) 155 } 156 _, err = s.getService(parsedURL) 157 if err != nil { 158 return false, err 159 } 160 commandSession := s.getMultiSession(parsedURL) 161 output, _ := s.runCommand(commandSession, URL, "ls -dltr "+parsedURL.Path) 162 if strings.Contains(string(output), "No such file or directory") { 163 return false, nil 164 } 165 return true, nil 166 167 } 168 169 func (s *service) StorageObject(URL string) (storage.Object, error) { 170 objects, err := s.List(URL) 171 if err != nil { 172 return nil, err 173 } 174 if len(objects) == 0 { 175 return nil, NoSuchFileOrDirectoryError 176 } 177 return objects[0], nil 178 } 179 180 //Download returns reader for downloaded storage object 181 func (s *service) Download(object storage.Object) (io.ReadCloser, error) { 182 if object == nil { 183 return nil, fmt.Errorf("object was nil") 184 } 185 parsedURL, err := url.Parse(object.URL()) 186 if err != nil { 187 return nil, err 188 } 189 if parsedURL.Host == "127.0.0.1" || parsedURL.Host == "127.0.0.1:22" { 190 var fileURL = toolbox.FileSchema + parsedURL.Path 191 storageObject, err := s.fileService.StorageObject(fileURL) 192 if err != nil { 193 return nil, err 194 } 195 return s.fileService.Download(storageObject) 196 } 197 port := toolbox.AsInt(parsedURL.Port()) 198 if port == 0 { 199 port = defaultSSHPort 200 } 201 service, err := s.getService(parsedURL) 202 if err != nil { 203 return nil, err 204 } 205 content, err := service.Download(parsedURL.Path) 206 if err != nil { 207 return nil, err 208 } 209 return ioutil.NopCloser(bytes.NewReader(content)), nil 210 } 211 212 //Upload uploads provided reader content for supplied URL. 213 func (s *service) Upload(URL string, reader io.Reader) error { 214 return s.UploadWithMode(URL, storage.DefaultFileMode, reader) 215 } 216 217 //Upload uploads provided reader content for supplied URL. 218 func (s *service) UploadWithMode(URL string, mode os.FileMode, reader io.Reader) error { 219 if mode == 0 { 220 mode = storage.DefaultFileMode 221 } 222 parsedURL, err := url.Parse(URL) 223 if err != nil { 224 return err 225 } 226 227 if parsedURL.Host == "127.0.0.1" || parsedURL.Host == "127.0.0.1:22" { 228 var fileURL = toolbox.FileSchema + parsedURL.Path 229 return s.fileService.UploadWithMode(fileURL, mode, reader) 230 } 231 232 port := toolbox.AsInt(parsedURL.Port()) 233 if port == 0 { 234 port = defaultSSHPort 235 } 236 237 //service, err := ssh.NewService(parsedURL.Hostname(), toolbox.AsInt(port), s.config) 238 service, err := s.getService(parsedURL) 239 if err != nil { 240 return err 241 } 242 243 //defer service.Close() 244 content, err := ioutil.ReadAll(reader) 245 if err != nil { 246 return fmt.Errorf("failed to upload - unable read: %v", err) 247 } 248 249 err = service.Upload(parsedURL.Path, mode, content) 250 if err != nil { 251 return fmt.Errorf("failed to upload: %v %v", URL, err) 252 } 253 return err 254 } 255 256 func (s *service) Register(schema string, service storage.Service) error { 257 return errors.New("unsupported") 258 } 259 260 func (s *service) Close() error { 261 for _, service := range s.services { 262 service.Close() 263 } 264 for _, session := range s.multiSessions { 265 session.Close() 266 } 267 return nil 268 } 269 270 //Delete removes passed in storage object 271 func (s *service) Delete(object storage.Object) error { 272 parsedURL, err := url.Parse(object.URL()) 273 if err != nil { 274 return err 275 } 276 if parsedURL.Host == "127.0.0.1" || parsedURL.Host == "127.0.0.1:22" { 277 var fileURL = toolbox.FileSchema + parsedURL.Path 278 storageObject, err := s.fileService.StorageObject(fileURL) 279 if err != nil { 280 return err 281 } 282 return s.fileService.Delete(storageObject) 283 } 284 285 port := toolbox.AsInt(parsedURL.Port()) 286 if port == 0 { 287 port = defaultSSHPort 288 } 289 service, err := ssh.NewService(parsedURL.Hostname(), toolbox.AsInt(port), s.config) 290 if err != nil { 291 return err 292 } 293 294 //defer service.Close() 295 session, err := service.NewSession() 296 if err != nil { 297 return err 298 } 299 defer session.Close() 300 301 if parsedURL.Path == "/" { 302 return fmt.Errorf("invalid removal path: %v", parsedURL.Path) 303 } 304 _, err = session.Output("rm -rf " + parsedURL.Path) 305 return err 306 } 307 308 //DownloadWithURL downloads content for passed in object URL 309 func (s *service) DownloadWithURL(URL string) (io.ReadCloser, error) { 310 object, err := s.StorageObject(URL) 311 if err != nil { 312 return nil, err 313 } 314 return s.Download(object) 315 } 316 317 //NewService create a new gc storage service 318 func NewService(config *cred.Config) *service { 319 return &service{ 320 services: make(map[string]ssh.Service), 321 config: config, 322 multiSessions: make(map[string]ssh.MultiCommandSession), 323 mutex: &sync.Mutex{}, 324 fileService: storage.NewFileStorage(), 325 } 326 }