1""" 2fs.rpcfs 3======== 4 5This module provides the class 'RPCFS' to access a remote FS object over 6XML-RPC. You probably want to use this in conjunction with the 'RPCFSServer' 7class from the :mod:`fs.expose.xmlrpc` module. 8 9""" 10 11import xmlrpclib 12import socket 13import base64 14 15from fs.base import * 16from fs.errors import * 17from fs.path import * 18from fs import iotools 19 20from fs.filelike import StringIO 21 22import six 23from six import PY3, b 24 25 26def re_raise_faults(func): 27 """Decorator to re-raise XML-RPC faults as proper exceptions.""" 28 def wrapper(*args, **kwds): 29 try: 30 return func(*args, **kwds) 31 except (xmlrpclib.Fault), f: 32 #raise 33 # Make sure it's in a form we can handle 34 35 print f.faultString 36 bits = f.faultString.split(" ") 37 if bits[0] not in ["<type", "<class"]: 38 raise f 39 # Find the class/type object 40 bits = " ".join(bits[1:]).split(">:") 41 cls = bits[0] 42 msg = ">:".join(bits[1:]) 43 cls = cls.strip('\'') 44 print "-" + cls 45 cls = _object_by_name(cls) 46 # Re-raise using the remainder of the fault code as message 47 if cls: 48 if issubclass(cls, FSError): 49 raise cls('', msg=msg) 50 else: 51 raise cls(msg) 52 raise f 53 except socket.error, e: 54 raise RemoteConnectionError(str(e), details=e) 55 return wrapper 56 57 58def _object_by_name(name, root=None): 59 """Look up an object by dotted-name notation.""" 60 bits = name.split(".") 61 if root is None: 62 try: 63 obj = globals()[bits[0]] 64 except KeyError: 65 try: 66 obj = __builtins__[bits[0]] 67 except KeyError: 68 obj = __import__(bits[0], globals()) 69 else: 70 obj = getattr(root, bits[0]) 71 if len(bits) > 1: 72 return _object_by_name(".".join(bits[1:]), obj) 73 else: 74 return obj 75 76 77class ReRaiseFaults: 78 """XML-RPC proxy wrapper that re-raises Faults as proper Exceptions.""" 79 80 def __init__(self, obj): 81 self._obj = obj 82 83 def __getattr__(self, attr): 84 val = getattr(self._obj, attr) 85 if callable(val): 86 val = re_raise_faults(val) 87 self.__dict__[attr] = val 88 return val 89 90 91class RPCFS(FS): 92 """Access a filesystem exposed via XML-RPC. 93 94 This class provides the client-side logic for accessing a remote FS 95 object, and is dual to the RPCFSServer class defined in fs.expose.xmlrpc. 96 97 Example:: 98 99 fs = RPCFS("http://my.server.com/filesystem/location/") 100 101 """ 102 103 _meta = {'thread_safe' : True, 104 'virtual': False, 105 'network' : True, 106 } 107 108 def __init__(self, uri, transport=None): 109 """Constructor for RPCFS objects. 110 111 The only required argument is the URI of the server to connect 112 to. This will be passed to the underlying XML-RPC server proxy 113 object, along with the 'transport' argument if it is provided. 114 115 :param uri: address of the server 116 117 """ 118 super(RPCFS, self).__init__(thread_synchronize=True) 119 self.uri = uri 120 self._transport = transport 121 self.proxy = self._make_proxy() 122 self.isdir('/') 123 124 @synchronize 125 def _make_proxy(self): 126 kwds = dict(allow_none=True, use_datetime=True) 127 128 if self._transport is not None: 129 proxy = xmlrpclib.ServerProxy(self.uri, self._transport, **kwds) 130 else: 131 proxy = xmlrpclib.ServerProxy(self.uri, **kwds) 132 133 return ReRaiseFaults(proxy) 134 135 def __str__(self): 136 return '<RPCFS: %s>' % (self.uri,) 137 138 def __repr__(self): 139 return '<RPCFS: %s>' % (self.uri,) 140 141 @synchronize 142 def __getstate__(self): 143 state = super(RPCFS, self).__getstate__() 144 try: 145 del state['proxy'] 146 except KeyError: 147 pass 148 return state 149 150 def __setstate__(self, state): 151 super(RPCFS, self).__setstate__(state) 152 self.proxy = self._make_proxy() 153 154 def encode_path(self, path): 155 """Encode a filesystem path for sending over the wire. 156 157 Unfortunately XMLRPC only supports ASCII strings, so this method 158 must return something that can be represented in ASCII. The default 159 is base64-encoded UTF8. 160 """ 161 return six.text_type(base64.b64encode(path.encode("utf8")), 'ascii') 162 163 def decode_path(self, path): 164 """Decode paths arriving over the wire.""" 165 return six.text_type(base64.b64decode(path.encode('ascii')), 'utf8') 166 167 @synchronize 168 def getmeta(self, meta_name, default=NoDefaultMeta): 169 if default is NoDefaultMeta: 170 meta = self.proxy.getmeta(meta_name) 171 else: 172 meta = self.proxy.getmeta_default(meta_name, default) 173 if isinstance(meta, basestring): 174 # To allow transport of meta with invalid xml chars (like null) 175 meta = self.encode_path(meta) 176 return meta 177 178 @synchronize 179 def hasmeta(self, meta_name): 180 return self.proxy.hasmeta(meta_name) 181 182 @synchronize 183 @iotools.filelike_to_stream 184 def open(self, path, mode='r', buffering=-1, encoding=None, errors=None, newline=None, line_buffering=False, **kwargs): 185 # TODO: chunked transport of large files 186 epath = self.encode_path(path) 187 if "w" in mode: 188 self.proxy.set_contents(epath, xmlrpclib.Binary(b(""))) 189 if "r" in mode or "a" in mode or "+" in mode: 190 try: 191 data = self.proxy.get_contents(epath, "rb").data 192 except IOError: 193 if "w" not in mode and "a" not in mode: 194 raise ResourceNotFoundError(path) 195 if not self.isdir(dirname(path)): 196 raise ParentDirectoryMissingError(path) 197 self.proxy.set_contents(path, xmlrpclib.Binary(b(""))) 198 else: 199 data = b("") 200 f = StringIO(data) 201 if "a" not in mode: 202 f.seek(0, 0) 203 else: 204 f.seek(0, 2) 205 oldflush = f.flush 206 oldclose = f.close 207 oldtruncate = f.truncate 208 209 def newflush(): 210 self._lock.acquire() 211 try: 212 oldflush() 213 self.proxy.set_contents(epath, xmlrpclib.Binary(f.getvalue())) 214 finally: 215 self._lock.release() 216 217 def newclose(): 218 self._lock.acquire() 219 try: 220 f.flush() 221 oldclose() 222 finally: 223 self._lock.release() 224 225 def newtruncate(size=None): 226 self._lock.acquire() 227 try: 228 oldtruncate(size) 229 f.flush() 230 finally: 231 self._lock.release() 232 233 f.flush = newflush 234 f.close = newclose 235 f.truncate = newtruncate 236 return f 237 238 @synchronize 239 def exists(self, path): 240 path = self.encode_path(path) 241 return self.proxy.exists(path) 242 243 @synchronize 244 def isdir(self, path): 245 path = self.encode_path(path) 246 return self.proxy.isdir(path) 247 248 @synchronize 249 def isfile(self, path): 250 path = self.encode_path(path) 251 return self.proxy.isfile(path) 252 253 @synchronize 254 def listdir(self, path="./", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False): 255 enc_path = self.encode_path(path) 256 if not callable(wildcard): 257 entries = self.proxy.listdir(enc_path, 258 wildcard, 259 full, 260 absolute, 261 dirs_only, 262 files_only) 263 entries = [self.decode_path(e) for e in entries] 264 else: 265 entries = self.proxy.listdir(enc_path, 266 None, 267 False, 268 False, 269 dirs_only, 270 files_only) 271 entries = [self.decode_path(e) for e in entries] 272 entries = [e for e in entries if wildcard(e)] 273 if full: 274 entries = [relpath(pathjoin(path, e)) for e in entries] 275 elif absolute: 276 entries = [abspath(pathjoin(path, e)) for e in entries] 277 return entries 278 279 @synchronize 280 def makedir(self, path, recursive=False, allow_recreate=False): 281 path = self.encode_path(path) 282 return self.proxy.makedir(path, recursive, allow_recreate) 283 284 @synchronize 285 def remove(self, path): 286 path = self.encode_path(path) 287 return self.proxy.remove(path) 288 289 @synchronize 290 def removedir(self, path, recursive=False, force=False): 291 path = self.encode_path(path) 292 return self.proxy.removedir(path, recursive, force) 293 294 @synchronize 295 def rename(self, src, dst): 296 src = self.encode_path(src) 297 dst = self.encode_path(dst) 298 return self.proxy.rename(src, dst) 299 300 @synchronize 301 def settimes(self, path, accessed_time, modified_time): 302 path = self.encode_path(path) 303 return self.proxy.settimes(path, accessed_time, modified_time) 304 305 @synchronize 306 def getinfo(self, path): 307 path = self.encode_path(path) 308 info = self.proxy.getinfo(path) 309 return info 310 311 @synchronize 312 def desc(self, path): 313 path = self.encode_path(path) 314 return self.proxy.desc(path) 315 316 @synchronize 317 def getxattr(self, path, attr, default=None): 318 path = self.encode_path(path) 319 attr = self.encode_path(attr) 320 return self.fs.getxattr(path, attr, default) 321 322 @synchronize 323 def setxattr(self, path, attr, value): 324 path = self.encode_path(path) 325 attr = self.encode_path(attr) 326 return self.fs.setxattr(path, attr, value) 327 328 @synchronize 329 def delxattr(self, path, attr): 330 path = self.encode_path(path) 331 attr = self.encode_path(attr) 332 return self.fs.delxattr(path, attr) 333 334 @synchronize 335 def listxattrs(self, path): 336 path = self.encode_path(path) 337 return [self.decode_path(a) for a in self.fs.listxattrs(path)] 338 339 @synchronize 340 def copy(self, src, dst, overwrite=False, chunk_size=16384): 341 src = self.encode_path(src) 342 dst = self.encode_path(dst) 343 return self.proxy.copy(src, dst, overwrite, chunk_size) 344 345 @synchronize 346 def move(self, src, dst, overwrite=False, chunk_size=16384): 347 src = self.encode_path(src) 348 dst = self.encode_path(dst) 349 return self.proxy.move(src, dst, overwrite, chunk_size) 350 351 @synchronize 352 def movedir(self, src, dst, overwrite=False, ignore_errors=False, chunk_size=16384): 353 src = self.encode_path(src) 354 dst = self.encode_path(dst) 355 return self.proxy.movedir(src, dst, overwrite, ignore_errors, chunk_size) 356 357 @synchronize 358 def copydir(self, src, dst, overwrite=False, ignore_errors=False, chunk_size=16384): 359 src = self.encode_path(src) 360 dst = self.encode_path(dst) 361 return self.proxy.copydir(src, dst, overwrite, ignore_errors, chunk_size) 362