Skip to content
This repository has been archived by the owner on Dec 27, 2018. It is now read-only.

XMLRPC Filesystem + expose Module #2

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ after_script:

notifications:
email:
- althonosdev@gmail.com
- M3RLINK@gmx.de
18 changes: 18 additions & 0 deletions fs/expose/xmlrpc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# coding: utf-8
from __future__ import absolute_import
from __future__ import unicode_literals

import threading

from .xmlrpc import RPCFSServer
# ~ from six.moves.xmlrpc_server import SimpleXMLRPCRequestHandler

def serve(filesystem, host='127.0.0.1', port=8000, encoding='utf-8',debug=True):
xmlrpcserver = RPCFSServer(filesystem,(host,port))

server_thread = threading.Thread(target=xmlrpcserver.serve_forever)
server_thread.daemon = False
server_thread.shutdown = xmlrpcserver.shutdown
server_thread.start()

return server_thread
194 changes: 194 additions & 0 deletions fs/expose/xmlrpc/xmlrpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
"""
fs.expose.xmlrpc
================

Server to expose an FS via XML-RPC

This module provides the necessary infrastructure to expose an FS object
over XML-RPC. The main class is 'RPCFSServer', a SimpleXMLRPCServer subclass
designed to expose an underlying FS.

If you need to use a more powerful server than SimpleXMLRPCServer, you can
use the RPCFSInterface class to provide an XML-RPC-compatible wrapper around
an FS object, which can then be exposed using whatever server you choose
(e.g. Twisted's XML-RPC server).

"""
from __future__ import absolute_import
from __future__ import unicode_literals


import six
from six import PY3

from six.moves.xmlrpc_server import SimpleXMLRPCServer
import six.moves.xmlrpc_client as xmlrpclib
import six.moves.cPickle as pickle

from datetime import datetime
import base64

from ... import errors
from ...path import normpath
from ...opener import open_fs

class RPCFSInterface(object):
"""Wrapper to expose an FS via a XML-RPC compatible interface.

The only real trick is using xmlrpclib.Binary objects to transport
the contents of files.
"""

# info keys are restricted to a subset known to work over xmlrpc
# This fixes an issue with transporting Longs on Py3
_allowed_methods = [
"listdir",
"isdir",
"isfile",
"exists",
"getinfo",
"setbytes",
"makedir",
"makedirs",
"remove",
"create",
"touch",
"validatepath",
"islink",
"removetree",
"removedir",
"getbytes",
"getsize",
"isempty",
"move",
"movedir",
"scandir",
"settimes",
"settext",
"setinfo",
"match",
"gettext",
"copy",
"copydir",
"desc",
"appendbytes",
"appendtext",
"getmeta",
"gettype",
"getsyspath",
"hassyspath",
"geturl",
"hasurl",
"getdetails",
]


def __init__(self, fs):
super(RPCFSInterface, self).__init__()
self.fs = fs

def _dispatch(self, method, params):


if not method in self._allowed_methods:
# ~ print('Server',method,params,'-->','Unsupported')
raise errors.Unsupported


# ~ return func(*params)

try:
func = getattr(self.fs, method)
params = list(params)

if six.PY2:
if method in ['match']:
params[1] = params[1].decode('utf-8')
else:
params[0] = params[0].decode('utf-8')


if method in ['appendtext','settext']:
#Ugly Hack to let the Tests run through
try:
params[1] = params[1].decode('utf-8')
except:
pass

if method in ['copy','move','copydir','movedir']:
params[1] = params[1].decode('utf-8')


if method in ['setbytes','appendbytes']:
try:
params[1] = params[1].data
except:
# ~ print('Server',method,params,'-->','TypeError: Need an xmlrpc.Binary object')
raise TypeError('Need an xmlrpc.Binary object')

if method in ['settimes']:
if isinstance(params[1], xmlrpclib.DateTime):
params[1] = datetime.strptime(params[1].value, "%Y%m%dT%H:%M:%S")
if len(params) > 2:
if isinstance(params[2], xmlrpclib.DateTime):
params[2] = datetime.strptime(params[2].value, "%Y%m%dT%H:%M:%S")

returndata = func(*params)

if method in ['makedir',"makedirs"]:
returndata = True

if method in ['getinfo','getdetails']:
returndata = returndata.raw

if method in ['getbytes']:
returndata = xmlrpclib.Binary(returndata)

if method in ['getmeta']:
if 'invalid_path_chars' in returndata:
returndata['invalid_path_chars'] = xmlrpclib.Binary(returndata['invalid_path_chars'].encode('utf-8'))
# ~ try:
# ~ print('Server',method,params,'-->',returndata)
# ~ except:
# ~ pass
return returndata
except:
# ~ import traceback
# ~ print('############## Traceback from Server ####################')
# ~ print('Server',method,params,'-->','Error')
# ~ traceback.print_exc()
# ~ print('############## Traceback from Server ####################')
raise


class RPCFSServer(SimpleXMLRPCServer):
"""Server to expose an FS object via XML-RPC.

This class takes as its first argument an FS instance, and as its second
argument a (hostname,port) tuple on which to listen for XML-RPC requests.
Example::

fs = OSFS('/var/srv/myfiles')
s = RPCFSServer(fs,("",8080))
s.serve_forever()

To cleanly shut down the server after calling serve_forever, set the
attribute "serve_more_requests" to False.
"""

def __init__(self, fs, addr, requestHandler=None, logRequests=True):
kwds = dict(allow_none=True)
if requestHandler is not None:
kwds['requestHandler'] = requestHandler
if logRequests is not None:
kwds['logRequests'] = logRequests
self.serve_more_requests = True
SimpleXMLRPCServer.__init__(self, addr, **kwds)
self.register_instance(RPCFSInterface(fs))


def serve(self):
"""Override serve_forever to allow graceful shutdown."""
while self.serve_more_requests:
self.handle_request()

115 changes: 115 additions & 0 deletions fs/xmlrpcfs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# coding: utf-8
from __future__ import absolute_import
from __future__ import unicode_literals

from contextlib import closing
from xml.parsers.expat import ExpatError

import six
from six.moves import xmlrpc_client
import six.moves.xmlrpc_client as xmlrpclib
from six.moves.xmlrpc_client import Fault
from fs.errors import *
from fs import errors, info

class XMLRPC_FS(object):
def __init__(self, *args,**kwargs):
self.proxy = xmlrpc_client.ServerProxy(*args,**kwargs)

def __getattr__(self, name):
# magic method dispatcher
return xmlrpclib._Method(self.__request, name)

def __call__(self, attr):
"""A workaround to get special attributes on the ServerProxy
without interfering with the magic __getattr__
"""
if attr == "close":
return self.__close
elif attr == "transport":
return self.__transport
raise AttributeError("Attribute %r not found" % (attr,))

def __enter__(self):
return self

def __exit__(self, *args):
self.__close()



def __request(self, methodname, params):

#Presendfunctions
if methodname in ['getmeta']:
if len(params) == 0:
params = ('',)

if methodname in ['settext','appendtext']:
if type(params[1]) == bytes:
raise TypeError('Bytes not allowed')

if six.PY2:
if methodname in ['setbytes','appendbytes']:

if type(params[1]) == unicode:
raise TypeError('Unicode not allowed')

if methodname in ['setbytes','appendbytes']:
params = (params[0],xmlrpclib.Binary(params[1]))

#Send
func = getattr(self.proxy, methodname)

try:
data = func(*params)
# ~ try:
# ~ print(methodname, params,'-->',data)
# ~ except:
# ~ pass

except ExpatError as err:
raise errors.InvalidCharsInPath(err)

except Fault as err:
err = str(err)
if 'fs.errors' in err:
x = err.split('fs.errors.')[1].split("'")[0]
errorobj = getattr(errors, x)
raise errorobj(err,'')
elif 'exceptions.TypeError' in err:
raise TypeError(err)
elif 'ExpatError' in err:
raise errors.InvalidCharsInPath(err)
else:
# ~ print(err)
raise


#Postsendfunctions
if methodname in ['getbytes']:
data = data.data

if methodname in ['getmeta']:
if 'invalid_path_chars' in data:
data['invalid_path_chars'] = data['invalid_path_chars'].data.decode('utf-8')

if six.PY2:
if methodname in ['getinfo','getdetails']:
data['basic']['name'] = data['basic']['name'].decode('utf-8')
if methodname in ['listdir']:
outlist = []
for entry in data:
outlist.append(entry.decode('utf-8'))
data = outlist

if methodname in ['gettext']:
#Ugly Hack to let the Tests run through
try:
data = data.decode('utf-8')
except:
pass
if methodname in ['getinfo','getdetails']:
data = info.Info(data)

return data
6 changes: 2 additions & 4 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,8 @@ python_requires = >= 2.7, != 3.0.*, != 3.1.*, != 3.2.*
packages = fs.expose, fs.expose.http
test_suite = tests
setup_requires = setuptools
install_requires =
fs ~=2.0.7
tests_require =
fs.expose[test]
install_requires = fs ~=2.0.7
tests_require = fs.expose[test]

[bdist_wheel]
universal = 1
Expand Down
Loading