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