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()