1#!/usr/bin/env python3
  2
  3"""Implementation of Resource class."""
  4
  5import errno
  6import logging
  7from urllib.parse import urlparse
  8
  9from enum import Enum
 10
 11from chart.common.path import Path
 12from chart.common.ssh import SSHClient
 13
 14logger = logging.getLogger()
 15
 16
 17class Scheme(Enum):
 18    """Resource types."""
 19
 20    FILE = 'file'
 21    SSH = 'ssh'
 22    HTTP = 'http'
 23    FTP = 'ftp'
 24
 25
 26class Resource:
 27    """Identify a resource based on a string description so it can be treated as if a local
 28    file or dir.
 29
 30    Very similar but not identical to urlparse
 31
 32    Currently only local file and ssh have any implementation.
 33
 34    Examples:
 35
 36    Resource('a.txt') -> local file relative
 37    Resource('file://a.txt') -> local file relative
 38    Resource('file:///a/b.txt') -> local file abs
 39    Resource('http://example.com/a') -> http
 40    -> ftp
 41    -> ssh
 42    """
 43
 44    def __init__(self,
 45                 path=None,
 46                 _scheme=None,
 47                 _netloc=None,
 48                 _path=None,
 49                 _sftp=None):
 50        """
 51        Construct with `path` (str) to build new connection.
 52        `parent` is for internal construction by iterdir etc.
 53
 54        Members:
 55
 56            `scheme` (str|None): ssh, file, http, ftp
 57            `netloc` (str|None): for non-file, the hostname
 58            `path` (Path): rel or abs path
 59
 60        """
 61        # note that:
 62
 63        # >>> urlparse.urlparse('file://a')
 64        # ParseResult(scheme='file', netloc='a', path='', params='', query='', fragment='')
 65        # >>> urlparse.urlparse('file:///a')
 66        # ParseResult(scheme='file', netloc='', path='/a', params='', query='', fragment='')
 67
 68        if path is not None:
 69            match = urlparse(path)
 70            if match.scheme == '':  # is None:
 71                self.scheme = Scheme.FILE
 72
 73            else:
 74                self.scheme = Scheme(match.scheme)
 75
 76            self.netloc = match.netloc
 77            self.path = Path(match.path)
 78            self._sftp = None
 79
 80        elif _scheme is not None and _path is not None:
 81            assert isinstance(_path, Path)
 82            self.scheme = _scheme
 83            self.netloc = _netloc
 84            self.path = _path
 85            self._sftp = _sftp
 86
 87        else:
 88            raise NotImplementedError()
 89
 90    def __lt__(self, other):
 91        return self.path < other.path
 92
 93    def open(self, mode='r'):
 94        """Open ourselves and return a file descriptor."""
 95        if self.scheme is Scheme.FILE:
 96            return self.path.open(mode)
 97
 98        elif self.scheme is Scheme.SSH:
 99            return self.sftp().open(str(self.path), mode)
100
101        else:
102            raise NotImplementedError()
103
104    def relocate(self, config):
105        """Change the schema of our connection e.g.
106        >> obj.relocate('ssh://epsope')
107        to change a local file resource to an ssh resource."""
108        if 'ssh' in config:
109            logger.info('Relocating resource to ssh server {c}'.format(c=config))
110            self.scheme = Scheme.SSH
111            self.netloc = config
112
113        else:
114            logger.info('Ignoring request to relocate to {c} because "ssh" not specificed'.format(
115                c=config))
116
117
118    def joinpath(self, other):
119        """Return a new Resource pointing to a child of ourselves,
120        assuming we are a directory."""
121        return Resource(
122            _scheme=self.scheme,
123            _netloc=self.netloc,
124            _path=self.path.joinpath(other),
125            _sftp=self._sftp)
126
127    def sftp(self):
128        """For ssh resources create a paramiko sftp object."""
129        if self._sftp is None:
130            prefix = 'ssh://'
131            if self.netloc.startswith(prefix):
132                self._sftp = SSHClient(self.netloc[len(prefix):]).sftp
133
134            else:
135                raise ValueError('SSH connection string should start with ssh://')
136
137        return self._sftp
138
139    def exists(self):
140        """Test if the name we point to exists."""
141        if self.scheme is Scheme.FILE:
142            return self.path.exists()
143
144        elif self.scheme is Scheme.SSH:
145            # ...
146            try:
147                stat = self.sftp().stat(str(self.path))
148            except IOError as e:
149                if e.errno == errno.ENOENT:
150                    return False
151
152                else:
153                    raise
154
155            return True
156
157        else:
158            raise NotImplementedError()
159
160    def stat(self):
161        """Return metadata for our object."""
162        if self.scheme is Scheme.FILE:
163            return self.path.stat()
164
165        elif self.scheme is Scheme.SSH:
166            return self.sftp().stat(str(self.path))
167
168    @property
169    def suffix(self):
170        """Return the file extension including '.' for our resource."""
171        a = self.path
172        return self.path.suffix
173
174    def _new(self, _path):
175        """Create a copy of ourselves with a new _path."""
176        return Resource(
177            _scheme=self.scheme,
178            _netloc=self.netloc,
179            _path=_path,
180            _sftp=self._sftp)
181
182    def iterdir(self):
183        """Assuming we are a directory, iterate through contents.
184        Returned values are fresh Resource objects."""
185        if self.scheme is Scheme.FILE:
186            for p in self.path.iterdir():
187                yield self._new(p)
188
189        elif self.scheme is Scheme.SSH:
190            for p in self.sftp().listdir(str(self.path)):
191                yield Resource(
192                    _scheme=self.scheme,
193                    _netloc=self.netloc,
194                    _path=self.path.joinpath(p),
195                    _sftp=self.sftp())
196
197        else:
198            raise NotImplementedError()
199
200    def read(self):
201        """Return contents of our resource."""
202        if self.scheme is Scheme.FILE:
203            return self.path.open('rb').read()
204
205        elif self.scheme is Scheme.SSH:
206            handle = self.sftp().open(str(self.path))
207            return handle.read()
208
209        else:
210            raise NotImplementedError()