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  }