1"""
2fs.expose.xmlrpc
3================
4
5Server to expose an FS via XML-RPC
6
7This module provides the necessary infrastructure to expose an FS object
8over XML-RPC.  The main class is 'RPCFSServer', a SimpleXMLRPCServer subclass
9designed to expose an underlying FS.
10
11If you need to use a more powerful server than SimpleXMLRPCServer, you can
12use the RPCFSInterface class to provide an XML-RPC-compatible wrapper around
13an FS object, which can then be exposed using whatever server you choose
14(e.g. Twisted's XML-RPC server).
15
16"""
17
18import xmlrpclib
19from SimpleXMLRPCServer import SimpleXMLRPCServer
20from datetime import datetime
21import base64
22
23import six
24from six import PY3
25
26
27class RPCFSInterface(object):
28    """Wrapper to expose an FS via a XML-RPC compatible interface.
29
30    The only real trick is using xmlrpclib.Binary objects to transport
31    the contents of files.
32    """
33
34    # info keys are restricted to a subset known to work over xmlrpc
35    # This fixes an issue with transporting Longs on Py3
36    _allowed_info = ["size",
37                     "created_time",
38                     "modified_time",
39                     "accessed_time",
40                     "st_size",
41                     "st_mode",
42                     "type"]
43
44    def __init__(self, fs):
45        super(RPCFSInterface, self).__init__()
46        self.fs = fs
47
48    def encode_path(self, path):
49        """Encode a filesystem path for sending over the wire.
50
51        Unfortunately XMLRPC only supports ASCII strings, so this method
52        must return something that can be represented in ASCII.  The default
53        is base64-encoded UTF-8.
54        """
55        #return path
56        return six.text_type(base64.b64encode(path.encode("utf8")), 'ascii')
57
58    def decode_path(self, path):
59        """Decode paths arriving over the wire."""
60        return six.text_type(base64.b64decode(path.encode('ascii')), 'utf8')
61
62    def getmeta(self, meta_name):
63        meta = self.fs.getmeta(meta_name)
64        if isinstance(meta, basestring):
65            meta = self.decode_path(meta)
66        return meta
67
68    def getmeta_default(self, meta_name, default):
69        meta = self.fs.getmeta(meta_name, default)
70        if isinstance(meta, basestring):
71            meta = self.decode_path(meta)
72        return meta
73
74    def hasmeta(self, meta_name):
75        return self.fs.hasmeta(meta_name)
76
77    def get_contents(self, path, mode="rb"):
78        path = self.decode_path(path)
79        data = self.fs.getcontents(path, mode)
80        return xmlrpclib.Binary(data)
81
82    def set_contents(self, path, data):
83        path = self.decode_path(path)
84        self.fs.setcontents(path, data.data)
85
86    def exists(self, path):
87        path = self.decode_path(path)
88        return self.fs.exists(path)
89
90    def isdir(self, path):
91        path = self.decode_path(path)
92        return self.fs.isdir(path)
93
94    def isfile(self, path):
95        path = self.decode_path(path)
96        return self.fs.isfile(path)
97
98    def listdir(self, path="./", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False):
99        path = self.decode_path(path)
100        entries = self.fs.listdir(path, wildcard, full, absolute, dirs_only, files_only)
101        return [self.encode_path(e) for e in entries]
102
103    def makedir(self, path, recursive=False, allow_recreate=False):
104        path = self.decode_path(path)
105        return self.fs.makedir(path, recursive, allow_recreate)
106
107    def remove(self, path):
108        path = self.decode_path(path)
109        return self.fs.remove(path)
110
111    def removedir(self, path, recursive=False, force=False):
112        path = self.decode_path(path)
113        return self.fs.removedir(path, recursive, force)
114
115    def rename(self, src, dst):
116        src = self.decode_path(src)
117        dst = self.decode_path(dst)
118        return self.fs.rename(src, dst)
119
120    def settimes(self, path, accessed_time, modified_time):
121        path = self.decode_path(path)
122        if isinstance(accessed_time, xmlrpclib.DateTime):
123            accessed_time = datetime.strptime(accessed_time.value, "%Y%m%dT%H:%M:%S")
124        if isinstance(modified_time, xmlrpclib.DateTime):
125            modified_time = datetime.strptime(modified_time.value, "%Y%m%dT%H:%M:%S")
126        return self.fs.settimes(path, accessed_time, modified_time)
127
128    def getinfo(self, path):
129        path = self.decode_path(path)
130        info = self.fs.getinfo(path)
131        info = dict((k, v) for k, v in info.iteritems()
132                    if k in self._allowed_info)
133        return info
134
135    def desc(self, path):
136        path = self.decode_path(path)
137        return self.fs.desc(path)
138
139    def getxattr(self, path, attr, default=None):
140        path = self.decode_path(path)
141        attr = self.decode_path(attr)
142        return self.fs.getxattr(path, attr, default)
143
144    def setxattr(self, path, attr, value):
145        path = self.decode_path(path)
146        attr = self.decode_path(attr)
147        return self.fs.setxattr(path, attr, value)
148
149    def delxattr(self, path, attr):
150        path = self.decode_path(path)
151        attr = self.decode_path(attr)
152        return self.fs.delxattr(path, attr)
153
154    def listxattrs(self, path):
155        path = self.decode_path(path)
156        return [self.encode_path(a) for a in self.fs.listxattrs(path)]
157
158    def copy(self, src, dst, overwrite=False, chunk_size=16384):
159        src = self.decode_path(src)
160        dst = self.decode_path(dst)
161        return self.fs.copy(src, dst, overwrite, chunk_size)
162
163    def move(self, src, dst, overwrite=False, chunk_size=16384):
164        src = self.decode_path(src)
165        dst = self.decode_path(dst)
166        return self.fs.move(src, dst, overwrite, chunk_size)
167
168    def movedir(self, src, dst, overwrite=False, ignore_errors=False, chunk_size=16384):
169        src = self.decode_path(src)
170        dst = self.decode_path(dst)
171        return self.fs.movedir(src, dst, overwrite, ignore_errors, chunk_size)
172
173    def copydir(self, src, dst, overwrite=False, ignore_errors=False, chunk_size=16384):
174        src = self.decode_path(src)
175        dst = self.decode_path(dst)
176        return self.fs.copydir(src, dst, overwrite, ignore_errors, chunk_size)
177
178
179class RPCFSServer(SimpleXMLRPCServer):
180    """Server to expose an FS object via XML-RPC.
181
182    This class takes as its first argument an FS instance, and as its second
183    argument a (hostname,port) tuple on which to listen for XML-RPC requests.
184    Example::
185
186        fs = OSFS('/var/srv/myfiles')
187        s = RPCFSServer(fs,("",8080))
188        s.serve_forever()
189
190    To cleanly shut down the server after calling serve_forever, set the
191    attribute "serve_more_requests" to False.
192    """
193
194    def __init__(self, fs, addr, requestHandler=None, logRequests=None):
195        kwds = dict(allow_none=True)
196        if requestHandler is not None:
197            kwds['requestHandler'] = requestHandler
198        if logRequests is not None:
199            kwds['logRequests'] = logRequests
200        self.serve_more_requests = True
201        SimpleXMLRPCServer.__init__(self, addr, **kwds)
202        self.register_instance(RPCFSInterface(fs))
203
204    def serve_forever(self):
205        """Override serve_forever to allow graceful shutdown."""
206        while self.serve_more_requests:
207            self.handle_request()
208