From 735a649d57bae9bcfd9de660e2b1f212f4999f30 Mon Sep 17 00:00:00 2001 From: "evgeny.ezhov" Date: Tue, 17 Oct 2017 09:04:47 +0300 Subject: [PATCH] Fixing and refactoring of set/get property methods --- README.rst | 24 +- tests/{test_client.py => test_client_it.py} | 22 +- tests/test_client_unit.py | 81 ++++++ webdav3/client.py | 279 +++++++++++--------- 4 files changed, 274 insertions(+), 132 deletions(-) rename tests/{test_client.py => test_client_it.py} (88%) create mode 100644 tests/test_client_unit.py diff --git a/README.rst b/README.rst index 7bc61ad..f4b7396 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,25 @@ webdavclient3 -============ +============= Based on https://github.com/designerror/webdav-client-python -But uses `requests` instead of `PyCURL` \ No newline at end of file +But uses `requests` instead of `PyCURL` + + +Release Notes +============= +Version 0.3 - TBD +* Refactoring of WebDAV client and making it works in following methods: + - Getting of WebDAV resource property value + - Setting of WebDAV resource property value + +Version 0.2 - 11.09.2017 +* Refactoring of WebDAV client and making it works in following methods: + - Constructor with connecting to WebDAV + - Getting a list of resources on WebDAV + - Getting an information about free space on WebDAV + - Checking of existence of resource on WebDAV + - Making a directory on WebDAV + - Downloading of files and directories from WebDAV + - Asynchronously downloading of files and directories from WebDAV + - Uploading of files and directories to WebDAV + - Asynchronously uploading of files and directories to WebDAV \ No newline at end of file diff --git a/tests/test_client.py b/tests/test_client_it.py similarity index 88% rename from tests/test_client.py rename to tests/test_client_it.py index 1a7c066..decb5e9 100644 --- a/tests/test_client.py +++ b/tests/test_client_it.py @@ -11,7 +11,7 @@ class ClientTestCase(TestCase): remote_path_file = 'test_dir/test.txt' remote_path_dir = 'test_dir' local_path_file = 'test.txt' - local_path_dir = u'res/test_dir' + local_path_dir = 'res/test_dir' def setUp(self): options = { @@ -42,7 +42,7 @@ class ClientTestCase(TestCase): def test_download_to(self): buff = BytesIO() - self.client.download_to(buff=buff, remote_path=self.remote_path_file) + self.client.download_from(buff=buff, remote_path=self.remote_path_file) self.assertEquals(buff.getvalue(), 'test content for testing of webdav client') def test_download(self): @@ -90,7 +90,7 @@ class ClientTestCase(TestCase): def test_upload_from(self): self._prepare_for_uploading() buff = StringIO(u'test content for testing of webdav client') - self.client.upload_from(buff=buff, remote_path=self.remote_path_file) + self.client.upload_to(buff=buff, remote_path=self.remote_path_file) self.assertTrue(self.client.check(self.remote_path_file), 'Expected the file is uploaded.') self.test_download_to() @@ -123,6 +123,22 @@ class ClientTestCase(TestCase): self.client.upload(remote_path=self.remote_path_file, local_path=self.local_path_dir) + def test_get_property(self): + self._prepare_for_downloading() + result = self.client.get_property(remote_path=self.remote_path_file, option={'name': 'aProperty'}) + self.assertEquals(result, None) + + def test_set_property(self): + self._prepare_for_downloading() + self.client.set_property(remote_path=self.remote_path_file, option={ + 'namespace': 'test', + 'name': 'aProperty', + 'value': 'aValue' + }) + result = self.client.get_property(remote_path=self.remote_path_file, + option={'namespace': 'test', 'name': 'aProperty'}) + self.assertEquals(result, "aValue") + def _prepare_for_downloading(self): if not self.client.check(remote_path=self.remote_path_dir): self.client.mkdir(remote_path=self.remote_path_dir) diff --git a/tests/test_client_unit.py b/tests/test_client_unit.py new file mode 100644 index 0000000..15b7d26 --- /dev/null +++ b/tests/test_client_unit.py @@ -0,0 +1,81 @@ +from unittest import TestCase + +from lxml.etree import ElementTree, Element + +from webdav3.client import WebDavXmlUtils as utils + + +class ClientTestCase(TestCase): + def test_parse_get_list_response(self): + content = '/test_dir/' \ + 'HTTP/1.1 200 OK' \ + 'Mon, 16 Oct 2017 04:18:00 GMT' \ + 'test_dir2017-10-16T04:18:00Z' \ + '/test_dir/test.txt/' \ + 'HTTP/1.1 200 OK' \ + 'Mon, 16 Oct 2017 04:18:18 GMTtest.txt' \ + '2017-10-16T04:18:18Z' \ + '' + result = utils.parse_get_list_response(content) + self.assertEquals(result.__len__(), 2) + + def test_create_free_space_request_content(self): + result = utils.create_free_space_request_content() + self.assertEquals(result, '\n' + '') + + def test_parse_free_space_response(self): + content = '/' \ + 'HTTP/1.1 200 OK697' \ + '10737417543' \ + '' + result = utils.parse_free_space_response(content) + self.assertEquals(result, 10737417543) + + def test_create_get_property_request_content(self): + option = { + 'namespace': 'test', + 'name': 'aProperty' + } + result = utils.create_get_property_request_content(option=option) + self.assertEquals(result, '\n' + '') + + def test_create_get_property_request_content_name_only(self): + option = { + 'name': 'aProperty' + } + result = utils.create_get_property_request_content(option=option) + self.assertEquals(result, '\n' + '') + + def test_parse_get_property_response(self): + content = '' \ + '/test_dir/test.txtHTTP/1.1 200 OK' \ + 'aValue' + + result = utils.parse_get_property_response(content=content, name='aProperty') + self.assertEquals(result, 'aValue') + + def test_create_set_property_request_content(self): + option = { + 'namespace': 'test', + 'name': 'aProperty', + 'value': 'aValue' + } + result = utils.create_set_property_request_content(option=option) + self.assertEquals(result, '\n' + 'aValue') + + def test_create_set_property_request_content_name_only(self): + option = { + 'name': 'aProperty' + } + result = utils.create_set_property_request_content(option=option) + self.assertEquals(result, '\n' + '') + + def test_etree_to_string(self): + tree = ElementTree(Element('test')) + result = utils.etree_to_string(tree) + self.assertEquals(result, '\n') diff --git a/webdav3/client.py b/webdav3/client.py index f7475bb..71f3e25 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -94,8 +94,8 @@ class Client(object): 'clean': ["Accept: */*", "Connection: Keep-Alive"], 'check': ["Accept: */*"], 'info': ["Accept: */*", "Depth: 1"], - 'get_metadata': ["Accept: */*", "Depth: 1", "Content-Type: application/x-www-form-urlencoded"], - 'set_metadata': ["Accept: */*", "Depth: 1", "Content-Type: application/x-www-form-urlencoded"] + 'get_property': ["Accept: */*", "Depth: 1", "Content-Type: application/x-www-form-urlencoded"], + 'set_property': ["Accept: */*", "Depth: 1", "Content-Type: application/x-www-form-urlencoded"] } def get_header(self, action): @@ -166,8 +166,8 @@ class Client(object): 'publish': "PROPPATCH", 'unpublish': "PROPPATCH", 'published': "PROPPATCH", - 'get_metadata': "PROPFIND", - 'set_metadata': "PROPPATCH" + 'get_property': "PROPFIND", + 'set_property': "PROPPATCH" } meta_xmlns = { @@ -219,29 +219,13 @@ class Client(object): :param remote_path: path to remote directory. :return: list of nested file or directory names. """ - - def parse(list_response): - """Parses of response from WebDAV server and extract file and directory names. - - :param list_response: HTTP response from WebDAV server for getting list of files by remote path. - :return: list of extracted file or directory names. - """ - try: - response_str = list_response.content - tree = etree.fromstring(response_str) - hrees = [unquote(hree.text) for hree in tree.findall(".//{DAV:}href")] - return [Urn(hree) for hree in hrees] - except etree.XMLSyntaxError: - return list() - directory_urn = Urn(remote_path, directory=True) - if directory_urn.path() != Client.root: if not self.check(directory_urn.path()): raise RemoteResourceNotFound(directory_urn.path()) response = self.execute_request(action='list', path=directory_urn.quote()) - urns = parse(response) + urns = WebDavXmlUtils.parse_get_list_response(response.content) path = "{root}{path}".format(root=self.webdav.root, path=directory_urn.path()) return [urn.filename() for urn in urns if urn.path() != path and urn.path() != path[:-1]] @@ -252,39 +236,9 @@ class Client(object): :return: an amount of free space in bytes. """ - - def parse(free_response): - """Parses of response from WebDAV server and extract na amount of free space. - - :param free_response: HTTP response from WebDAV server for getting free space. - :return: an amount of free space in bytes. - """ - try: - response_str = free_response.content - tree = etree.fromstring(response_str) - node = tree.find('.//{DAV:}quota-available-bytes') - if node is not None: - return int(node.text) - else: - raise MethodNotSupported(name='free', server=self.webdav.hostname) - except TypeError: - raise MethodNotSupported(name='free', server=self.webdav.hostname) - except etree.XMLSyntaxError: - return str() - - def data(): - root = etree.Element("propfind", xmlns="DAV:") - prop = etree.SubElement(root, "prop") - etree.SubElement(prop, "quota-available-bytes") - etree.SubElement(prop, "quota-used-bytes") - tree = etree.ElementTree(root) - buff = BytesIO() - tree.write(buff) - return buff.getvalue() - - response = self.execute_request(action='free', path='', data=data()) - - return parse(response) + data = WebDavXmlUtils.create_free_space_request_content() + response = self.execute_request(action='free', path='', data=data) + return WebDavXmlUtils.parse_free_space_response(response.content, self.webdav.hostname) @wrap_connection_error def check(self, remote_path=root): @@ -321,7 +275,7 @@ class Client(object): return response.status_code == 200 @wrap_connection_error - def download_to(self, buff, remote_path): + def download_from(self, buff, remote_path): """Downloads file from WebDAV and writes it in buffer. :param buff: buffer object for writing of downloaded file content. @@ -423,7 +377,7 @@ class Client(object): threading.Thread(target=target).start() @wrap_connection_error - def upload_from(self, buff, remote_path): + def upload_to(self, buff, remote_path): """Uploads file from buffer to remote path on WebDAV server. :param buff: the buffer with content for file. @@ -544,6 +498,7 @@ class Client(object): :param remote_path_from: the path to resource which will be copied, :param remote_path_to: the path where resource will be copied. """ + def header(remote_path_to): path = Urn(remote_path_to).path() @@ -753,91 +708,48 @@ class Client(object): return parse(response, path) - def resource(self, remote_path): - urn = Urn(remote_path) - return Resource(self, urn.path()) - @wrap_connection_error def get_property(self, remote_path, option): + """ + Gets metadata property of remote resource on WebDAV server. + More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_PROPFIND - def parse(response, option): - response_str = response.content - tree = etree.fromstring(response_str) - xpath = "{xpath_prefix}{xpath_exp}".format(xpath_prefix=".//", xpath_exp=option['name']) - return tree.findtext(xpath) - - def data(option): - root = etree.Element("propfind", xmlns="DAV:") - prop = etree.SubElement(root, "prop") - etree.SubElement(prop, option.get('name', ""), xmlns=option.get('namespace', "")) - tree = etree.ElementTree(root) - - buff = BytesIO() - - tree.write(buff) - return buff.getvalue() - + :param remote_path: the path to remote resource. + :param option: the property attribute as dictionary with following keys: + `namespace`: (optional) the namespace for XML property which will be set, + `name`: the name of property which will be set. + :return: the value of property or None if property is not found. + """ urn = Urn(remote_path) - if not self.check(urn.path()): raise RemoteResourceNotFound(urn.path()) - url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} - options = { - 'URL': "{hostname}{root}{path}".format(**url), - 'CUSTOMREQUEST': Client.requests['get_metadata'], - 'HTTPHEADER': self.get_header('get_metadata'), - 'POSTFIELDS': data(option), - 'NOBODY': 0 - } - - response = requests.request( - options["CUSTOMREQUEST"], - options["URL"], - auth=(self.webdav.login, self.webdav.password), - headers=self.get_header('get_metadata'), - data=data(option) - ) - return parse(response, option) + data = WebDavXmlUtils.create_get_property_request_content(option) + response = self.execute_request(action='get_property', path=urn.quote(), data=data) + return WebDavXmlUtils.parse_get_property_response(response.content, option['name']) @wrap_connection_error def set_property(self, remote_path, option): + """ + Sets metadata property of remote resource on WebDAV server. + More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_PROPPATCH - def data(option): - root_node = etree.Element("propertyupdate", xmlns="DAV:") - root_node.set('xmlns:u', option.get('namespace', "")) - set_node = etree.SubElement(root_node, "set") - prop_node = etree.SubElement(set_node, "prop") - opt_node = etree.SubElement(prop_node, "{namespace}:{name}".format(namespace='u', name=option['name'])) - opt_node.text = option.get('value', "") - - tree = etree.ElementTree(root_node) - - buff = BytesIO() - tree.write(buff) - - return buff.getvalue() - + :param remote_path: the path to remote resource. + :param option: the property attribute as dictionary with following keys: + `namespace`: (optional) the namespace for XML property which will be set, + `name`: the name of property which will be set, + `value`: (optional) the value of property which will be set. Defaults is empty string. + """ urn = Urn(remote_path) - if not self.check(urn.path()): raise RemoteResourceNotFound(urn.path()) - url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} - options = { - 'URL': "{hostname}{root}{path}".format(**url), - 'CUSTOMREQUEST': Client.requests['set_metadata'], - 'HTTPHEADER': self.get_header('get_metadata'), - 'POSTFIELDS': data(option) - } + data = WebDavXmlUtils.create_set_property_request_content(option) + self.execute_request(action='set_property', path=urn.quote(), data=data) - response = requests.request( - options["CUSTOMREQUEST"], - options["URL"], - auth=(self.webdav.login, self.webdav.password), - headers=self.get_header('get_metadata'), - data=data(option) - ) + def resource(self, remote_path): + urn = Urn(remote_path) + return Resource(self, urn.path()) def push(self, remote_directory, local_directory): @@ -960,7 +872,7 @@ class Resource(object): return self.client.check(self.urn.path()) def read_from(self, buff): - self.client.upload_from(buff=buff, remote_path=self.urn.path()) + self.client.upload_to(buff=buff, remote_path=self.urn.path()) def read(self, local_path): return self.client.upload_sync(local_path=local_path, remote_path=self.urn.path()) @@ -969,7 +881,7 @@ class Resource(object): return self.client.upload_async(local_path=local_path, remote_path=self.urn.path(), callback=callback) def write_to(self, buff): - return self.client.download_to(buff=buff, remote_path=self.urn.path()) + return self.client.download_from(buff=buff, remote_path=self.urn.path()) def write(self, local_path): return self.client.download_sync(local_path=local_path, remote_path=self.urn.path()) @@ -991,3 +903,116 @@ class Resource(object): def property(self, option, value): option['value'] = value.__str__() self.client.set_property(remote_path=self.urn.path(), option=option) + + +class WebDavXmlUtils: + def __init__(self): + pass + + @staticmethod + def parse_get_list_response(content): + """ + Parses of response content XML from WebDAV server and extract file and directory names. + + :param content: the XML content of HTTP response from WebDAV server for getting list of files by remote path. + :return: list of extracted file or directory names. + """ + try: + tree = etree.fromstring(content) + hrees = [unquote(hree.text) for hree in tree.findall(".//{DAV:}href")] + return [Urn(hree) for hree in hrees] + except etree.XMLSyntaxError: + return list() + + @staticmethod + def create_free_space_request_content(): + """ + Creates an XML for requesting of free space on remote WebDAV server. + + :return: the XML string of request content. + """ + root = etree.Element("propfind", xmlns="DAV:") + prop = etree.SubElement(root, "prop") + etree.SubElement(prop, "quota-available-bytes") + etree.SubElement(prop, "quota-used-bytes") + tree = etree.ElementTree(root) + return WebDavXmlUtils.etree_to_string(tree) + + @staticmethod + def parse_free_space_response(content, hostname): + """ + Parses of response content XML from WebDAV server and extract na amount of free space. + + :param content: the XML content of HTTP response from WebDAV server for getting free space. + :return: an amount of free space in bytes. + """ + try: + tree = etree.fromstring(content) + node = tree.find('.//{DAV:}quota-available-bytes') + if node is not None: + return int(node.text) + else: + raise MethodNotSupported(name='free', server=hostname) + except TypeError: + raise MethodNotSupported(name='free', server=hostname) + except etree.XMLSyntaxError: + return str() + + @staticmethod + def create_get_property_request_content(option): + """ + Creates an XML for requesting of getting a property value of remote WebDAV resource. + + :param option: the property attributes as dictionary with following keys: + `namespace`: (optional) the namespace for XML property which will be get, + `name`: the name of property which will be get. + :return: the XML string of request content. + """ + root = etree.Element("propfind", xmlns="DAV:") + prop = etree.SubElement(root, "prop") + etree.SubElement(prop, option.get('name', ""), xmlns=option.get('namespace', "")) + tree = etree.ElementTree(root) + return WebDavXmlUtils.etree_to_string(tree) + + @staticmethod + def parse_get_property_response(content, name): + """ + Parses of response content XML from WebDAV server for getting metadata property value for some resource. + + :param content: the XML content of response as string. + :param name: the name of property for finding a value in response + :return: the value of property if it has been found or None otherwise. + """ + tree = etree.fromstring(content) + return tree.xpath('//*[local-name() = $name]', name=name)[0].text + + @staticmethod + def create_set_property_request_content(option): + """ + Creates an XML for requesting of setting a property value for remote WebDAV resource. + + :param option: the property attributes as dictionary with following keys: + `namespace`: (optional) the namespace for XML property which will be set, + `name`: the name of property which will be set, + `value`: (optional) the value of property which will be set. Defaults is empty string. + :return: the XML string of request content. + """ + root_node = etree.Element('propertyupdate', xmlns='DAV:') + set_node = etree.SubElement(root_node, 'set') + prop_node = etree.SubElement(set_node, 'prop') + opt_node = etree.SubElement(prop_node, option['name'], xmlns=option.get('namespace', '')) + opt_node.text = option.get('value', '') + tree = etree.ElementTree(root_node) + return WebDavXmlUtils.etree_to_string(tree) + + @staticmethod + def etree_to_string(tree): + """ + Creates string from lxml.etree.ElementTree with XML declaration and UTF-8 encoding. + + :param tree: the instance of ElementTree + :return: the string of XML. + """ + buff = BytesIO() + tree.write(buff, xml_declaration=True, encoding='UTF-8') + return buff.getvalue()