From 17fae10abc26c8da90b4171f74da1bc821975f9d Mon Sep 17 00:00:00 2001 From: "Zimmermann, Stefan" Date: Thu, 20 Dec 2018 12:57:45 +0100 Subject: [PATCH 01/63] Fixing problems of integration and unit tests - AssertEquals deprecation warnings - Problems with byte/UTF strings and xml library --- .gitignore | 4 +++ tests/test_client_it.py | 17 +++++------ tests/test_client_unit.py | 60 +++++++++++++++++++-------------------- 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index 24b43f6..bf2fda7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ /**/*.pyc /dist /*.egg-info +/.eggs/ +.project +.pydevproject +/.settings/ diff --git a/tests/test_client_it.py b/tests/test_client_it.py index 52c5a60..cc4838b 100644 --- a/tests/test_client_it.py +++ b/tests/test_client_it.py @@ -60,7 +60,7 @@ class ClientTestCase(TestCase): self._prepare_for_downloading() buff = BytesIO() self.client.download_from(buff=buff, remote_path=self.remote_path_file) - self.assertEquals(buff.getvalue(), 'test content for testing of webdav client') + self.assertEqual(buff.getvalue(), 'test content for testing of webdav client') @unittest.skip("Yandex brakes response for file it contains property resourcetype as collection but it should " "be empty for file") @@ -110,7 +110,8 @@ class ClientTestCase(TestCase): def test_upload_from(self): self._prepare_for_uploading() - buff = StringIO(u'test content for testing of webdav client') + buff = StringIO() + buff.write(u'test content for testing of webdav client') 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.') @@ -165,8 +166,8 @@ class ClientTestCase(TestCase): def test_info(self): self._prepare_for_downloading() result = self.client.info(remote_path=self.remote_path_file) - self.assertEquals(result['name'], 'test.txt') - self.assertEquals(result['size'], '41') + self.assertEqual(result['name'], 'test.txt') + self.assertEqual(result['size'], '41') self.assertTrue('created' in result) self.assertTrue('modified' in result) @@ -181,7 +182,7 @@ class ClientTestCase(TestCase): def test_get_property_of_non_exist(self): self._prepare_for_downloading() result = self.client.get_property(remote_path=self.remote_path_file, option={'name': 'aProperty'}) - self.assertEquals(result, None, 'For not found property should return value as None') + self.assertEqual(result, None, 'For not found property should return value as None') def test_set_property(self): self._prepare_for_downloading() @@ -192,7 +193,7 @@ class ClientTestCase(TestCase): }) result = self.client.get_property(remote_path=self.remote_path_file, option={'namespace': 'test', 'name': 'aProperty'}) - self.assertEquals(result, 'aValue', 'Property value should be set') + self.assertEqual(result, 'aValue', 'Property value should be set') def test_set_property_batch(self): self._prepare_for_downloading() @@ -210,10 +211,10 @@ class ClientTestCase(TestCase): ]) result = self.client.get_property(remote_path=self.remote_path_file, option={'namespace': 'test', 'name': 'aProperty'}) - self.assertEquals(result, 'aValue', 'First property value should be set') + self.assertEqual(result, 'aValue', 'First property value should be set') result = self.client.get_property(remote_path=self.remote_path_file, option={'namespace': 'test', 'name': 'aProperty2'}) - self.assertEquals(result, 'aValue2', 'Second property value should be set') + self.assertEqual(result, 'aValue2', 'Second property value should be set') def _prepare_for_downloading(self): if not self.client.check(remote_path=self.remote_path_dir): diff --git a/tests/test_client_unit.py b/tests/test_client_unit.py index bbdb3c8..7489895 100644 --- a/tests/test_client_unit.py +++ b/tests/test_client_unit.py @@ -18,21 +18,21 @@ class ClientTestCase(TestCase): '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) + result = utils.parse_get_list_response(content.encode('utf-8')) + self.assertEqual(result.__len__(), 2) def test_create_free_space_request_content(self): result = utils.create_free_space_request_content() - self.assertEquals(result, '\n' - '') + self.assertEqual(result, b'\n' + b'') def test_parse_free_space_response(self): content = '/' \ 'HTTP/1.1 200 OK697' \ '10737417543' \ '' - result = utils.parse_free_space_response(content, 'localhost') - self.assertEquals(result, 10737417543) + result = utils.parse_free_space_response(content.encode('utf-8'), 'localhost') + self.assertEqual(result, 10737417543) def test_parse_info_response(self): content = '' \ @@ -42,11 +42,11 @@ class ClientTestCase(TestCase): '41test.txt' \ '2017-10-18T15:16:04Z' \ '' - result = utils.parse_info_response(content, '/test_dir/test.txt', 'localhost') - self.assertEquals(result['created'], '2017-10-18T15:16:04Z') - self.assertEquals(result['name'], 'test.txt') - self.assertEquals(result['modified'], 'Wed, 18 Oct 2017 15:16:04 GMT') - self.assertEquals(result['size'], '41') + result = utils.parse_info_response(content.encode('utf-8'), '/test_dir/test.txt', 'localhost') + self.assertEqual(result['created'], '2017-10-18T15:16:04Z') + self.assertEqual(result['name'], 'test.txt') + self.assertEqual(result['modified'], 'Wed, 18 Oct 2017 15:16:04 GMT') + self.assertEqual(result['size'], '41') def test_create_get_property_request_content(self): option = { @@ -54,24 +54,24 @@ class ClientTestCase(TestCase): 'name': 'aProperty' } result = utils.create_get_property_request_content(option=option, ) - self.assertEquals(result, '\n' - '') + self.assertEqual(result, b'\n' + b'') 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' - '') + self.assertEqual(result, b'\n' + b'') 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') + result = utils.parse_get_property_response(content=content.encode('utf-8'), name='aProperty') + self.assertEqual(result, 'aValue') def test_create_set_one_property_request_content(self): option = { @@ -80,16 +80,16 @@ class ClientTestCase(TestCase): 'value': 'aValue' } result = utils.create_set_property_batch_request_content(options=[option]) - self.assertEquals(result, '\n' - 'aValue') + self.assertEqual(result, b'\n' + b'aValue') def test_create_set_one_property_request_content_name_only(self): option = { 'name': 'aProperty' } result = utils.create_set_property_batch_request_content(options=[option]) - self.assertEquals(result, '\n' - '') + self.assertEqual(result, b'\n' + b'') def test_create_set_property_batch_request_content(self): options = [ @@ -105,9 +105,9 @@ class ClientTestCase(TestCase): } ] result = utils.create_set_property_batch_request_content(options=options) - self.assertEquals(result, '\n' - 'aValueaValue2' - '') + self.assertEqual(result, b'\n' + b'aValueaValue2' + b'') def test_create_set_property_batch_request_content_name_only(self): options = [ @@ -119,14 +119,14 @@ class ClientTestCase(TestCase): } ] result = utils.create_set_property_batch_request_content(options=options) - self.assertEquals(result, '\n' - '' - '') + self.assertEqual(result, b'\n' + b'' + b'') def test_etree_to_string(self): tree = ElementTree(Element('test')) result = utils.etree_to_string(tree) - self.assertEquals(result, '\n') + self.assertEqual(result, b'\n') def test_parse_is_dir_response_directory(self): content = '/' path = '/test_dir' hostname = 'https://webdav.yandex.ru' - result = utils.parse_is_dir_response(content, path, hostname) + result = utils.parse_is_dir_response(content.encode('utf-8'), path, hostname) self.assertTrue(result, 'It should be directory') def test_parse_is_dir_response_directory(self): @@ -190,7 +190,7 @@ class ClientTestCase(TestCase): 'th>' path = '/test_dir/test.txt' hostname = 'https://webdav.yandex.ru' - result = utils.parse_is_dir_response(content, path, hostname) + result = utils.parse_is_dir_response(content.encode('utf-8'), path, hostname) self.assertFalse(result, 'It should be file') From f63a7dc48e55434e0b66ac6d4f65295585e584bb Mon Sep 17 00:00:00 2001 From: "Zimmermann, Stefan" Date: Thu, 20 Dec 2018 11:30:29 +0100 Subject: [PATCH 02/63] Remove the upload filesize limit which is a remnant of using PyCurl --- webdav3/client.py | 9 +-------- webdav3/exceptions.py | 13 ------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/webdav3/client.py b/webdav3/client.py index afc7d57..59526a8 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -83,9 +83,6 @@ class Client(object): # path to root directory of WebDAV root = '/' - # Max size of file for uploading - large_size = 2 * 1024 * 1024 * 1024 - # request timeout in seconds timeout = 30 @@ -475,11 +472,7 @@ class Client(object): raise RemoteParentNotFound(urn.path()) with open(local_path, "rb") as local_file: - file_size = os.path.getsize(local_path) - if file_size > self.large_size: - raise ResourceTooBig(path=local_path, size=file_size, max_size=self.large_size) - - self.execute_request(action='upload', path=urn.quote(), data=local_file) + self.execute_request(action='upload', path=urn.quote(), data=local_file) def upload_sync(self, remote_path, local_path, callback=None): """Uploads resource to remote path on WebDAV server synchronously. diff --git a/webdav3/exceptions.py b/webdav3/exceptions.py index b30abbc..388f8de 100644 --- a/webdav3/exceptions.py +++ b/webdav3/exceptions.py @@ -49,19 +49,6 @@ class RemoteParentNotFound(NotFound): return "Remote parent for: {path} not found".format(path=self.path) -class ResourceTooBig(WebDavException): - def __init__(self, path, size, max_size): - self.path = path - self.size = size - self.max_size = max_size - - def __str__(self): - return "Resource {path} is too big, it should be less then {max_size} but actually: {size}".format( - path=self.path, - max_size=self.max_size, - size=self.size) - - class MethodNotSupported(WebDavException): def __init__(self, name, server): self.name = name From 51458a439b594e9a88ef2f453584211bc8cfc656 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Thu, 31 Jan 2019 15:44:03 +0300 Subject: [PATCH 03/63] Update before release 0.10 --- README.rst | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 9753a2f..3fabdfb 100644 --- a/README.rst +++ b/README.rst @@ -11,6 +11,12 @@ But uses `requests` instead of `PyCURL` Release Notes ============= +**Version 0.10 – 31.01.2018** + * AssertEquals deprecation warnings by https://github.com/StefanZi + * Problems with byte/UTF strings and xml library by https://github.com/StefanZi + * Add some Eclipse specific files to gitignore by https://github.com/StefanZi + * Remove filesize limit by https://github.com/StefanZi + **Version 0.9 – 10.05.2018** * Client.mkdir now accepts 201 HTTP-code by https://github.com/a1ezzz * Tests are updated diff --git a/setup.py b/setup.py index d5c15b9..a01bee8 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import setup, find_packages from setuptools.command.install import install as InstallCommand from setuptools.command.test import test as TestCommand -version = "0.9" +version = "0.10" requirements = "libxml2-dev libxslt-dev python-dev" From 460160ed047b61ebf1aacb20873cf81cd47e8c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20B=C3=B6hmke?= Date: Wed, 27 Mar 2019 13:40:13 +0100 Subject: [PATCH 04/63] fixed download of large files --- webdav3/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webdav3/client.py b/webdav3/client.py index 59526a8..0eb0452 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -158,7 +158,8 @@ class Client(object): auth=(self.webdav.login, self.webdav.password), headers=self.get_headers(action, headers_ext), timeout=self.timeout, - data=data + data=data, + stream=True ) if response.status_code == 507: raise NotEnoughSpace() @@ -306,7 +307,7 @@ class Client(object): raise RemoteResourceNotFound(urn.path()) response = self.execute_request(action='download', path=urn.quote()) - buff.write(response.content) + shutil.copyfileobj(response.raw, buff) def download(self, remote_path, local_path, progress=None): """Downloads remote resource from WebDAV and save it in local path. From d38aded83da077f469dcc4910571cd1e11f7e604 Mon Sep 17 00:00:00 2001 From: Eric Van Horn Date: Thu, 28 Mar 2019 21:24:09 -0500 Subject: [PATCH 05/63] fixed missing requirement; incorrect import --- setup.py | 2 +- wdc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a01bee8..0d08db3 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ setup( version=version, packages=find_packages(), requires=['python (>= 2.7.6)'], - install_requires=['requests', 'lxml'], + install_requires=['requests', 'lxml', 'argcomplete'], scripts=['wdc'], tests_require=['pytest', 'pyhamcrest', 'junit-xml', 'pytest-allure-adaptor'], cmdclass={'install': Install, 'test': Test}, diff --git a/wdc b/wdc index 4f9fc12..3fcbace 100644 --- a/wdc +++ b/wdc @@ -306,7 +306,7 @@ if __name__ == "__main__": parser = argparse.ArgumentParser(prog='wdc', formatter_class=Formatter, epilog=epilog, usage=usage) parser.add_argument("action", help=actions_help, choices=actions) - from webdav.client import __version__ as version + from webdav3.client import __version__ as version version_text = "{name} {version}".format(name="%(prog)s", version=version) parser.add_argument("-v", '--version', action='version', version=version_text) parser.add_argument("-r", "--root", help="example: dir1/dir2") From e576958f4b6ebe63c8d51859df049981f3e17080 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Sat, 30 Mar 2019 09:35:21 +0300 Subject: [PATCH 06/63] Update before release 0.11 --- README.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3fabdfb..a2f149c 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,12 @@ But uses `requests` instead of `PyCURL` Release Notes ============= -**Version 0.10 – 31.01.2018** +**Version 0.11 – 30.03.2019** + * Fixed MemoryError if a large file is downloaded with a 32 bit python by https://github.com/bboehmke + * Fixed argcomplete is required to run wdc but was not included in the requirements by https://github.com/evanhorn + * Fixed wdc tries to import webdav instead of webdav3 by https://github.com/evanhorn + +**Version 0.10 – 31.01.2019** * AssertEquals deprecation warnings by https://github.com/StefanZi * Problems with byte/UTF strings and xml library by https://github.com/StefanZi * Add some Eclipse specific files to gitignore by https://github.com/StefanZi From 1c43adb5bda89fd23e390ae67f57541914b88754 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Sat, 30 Mar 2019 09:35:21 +0300 Subject: [PATCH 07/63] Update before release 0.11 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0d08db3..235e878 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import setup, find_packages from setuptools.command.install import install as InstallCommand from setuptools.command.test import test as TestCommand -version = "0.10" +version = "0.11" requirements = "libxml2-dev libxslt-dev python-dev" From ea139d2ec7dadc59fcdb7ea40d30d6f01f65baa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20H=C3=A5kansson?= Date: Tue, 18 Jun 2019 08:53:41 +0200 Subject: [PATCH 08/63] Added depth_to_copy argument in copy method in client.py Implemented support to control the depth http header for the copy command. --- webdav3/client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webdav3/client.py b/webdav3/client.py index 0eb0452..92bb4ee 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -500,12 +500,13 @@ class Client(object): threading.Thread(target=target).start() @wrap_connection_error - def copy(self, remote_path_from, remote_path_to): + def copy(self, remote_path_from, remote_path_to, depth=1): """Copies resource from one place to another on WebDAV server. More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_COPY :param remote_path_from: the path to resource which will be copied, :param remote_path_to: the path where resource will be copied. + :param depth: folder depth to copy """ urn_from = Urn(remote_path_from) if not self.check(urn_from.path()): @@ -516,7 +517,8 @@ class Client(object): raise RemoteParentNotFound(urn_to.path()) header_destination = "Destination: {path}".format(path=self.get_full_path(urn_to)) - self.execute_request(action='copy', path=urn_from.quote(), headers_ext=[header_destination]) + header_depth = "Depth: {depth}".format(depth=depth) + self.execute_request(action='copy', path=urn_from.quote(), headers_ext=[header_destination, header_depth]) @wrap_connection_error def move(self, remote_path_from, remote_path_to, overwrite=False): From 991e645fdb15082d7556c8b47c755b81b5b5ed62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20H=C3=A5kansson?= Date: Tue, 18 Jun 2019 09:21:31 +0200 Subject: [PATCH 09/63] Added verify attribute to execute_request method To be able to control whether to verify the server's TLS certificate or not, the verify attribute has been added to the execute_request method. --- webdav3/client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/webdav3/client.py b/webdav3/client.py index 92bb4ee..82b26d6 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -86,6 +86,9 @@ class Client(object): # request timeout in seconds timeout = 30 + # controls whether to verify the server's TLS certificate or not + verify = True + # HTTP headers for different actions http_header = { 'list': ["Accept: */*", "Depth: 1"], @@ -159,7 +162,8 @@ class Client(object): headers=self.get_headers(action, headers_ext), timeout=self.timeout, data=data, - stream=True + stream=True, + verify=self.verify ) if response.status_code == 507: raise NotEnoughSpace() From da46592c6f1cc9fb810ca54019763b1e7dce4583 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Fri, 21 Jun 2019 13:39:55 +0300 Subject: [PATCH 10/63] Update before release 0.12 --- README.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a2f149c..e0e2f31 100644 --- a/README.rst +++ b/README.rst @@ -11,6 +11,10 @@ But uses `requests` instead of `PyCURL` Release Notes ============= +**Version 0.12 - 21.06.2019** + * Added depth argument in copy method in client.py by https://github.com/JesperHakansson + * Added verify attribute to execute_request method by https://github.com/JesperHakansson + **Version 0.11 – 30.03.2019** * Fixed MemoryError if a large file is downloaded with a 32 bit python by https://github.com/bboehmke * Fixed argcomplete is required to run wdc but was not included in the requirements by https://github.com/evanhorn diff --git a/setup.py b/setup.py index 235e878..39eefa2 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import setup, find_packages from setuptools.command.install import install as InstallCommand from setuptools.command.test import test as TestCommand -version = "0.11" +version = "0.12" requirements = "libxml2-dev libxslt-dev python-dev" From 2e6284af1b29cb350a49021160c8c3adae0a4c83 Mon Sep 17 00:00:00 2001 From: Jorge Date: Mon, 30 Sep 2019 19:54:06 +0100 Subject: [PATCH 11/63] [Jorge] Updated to add venv/.idea/build --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index bf2fda7..31720d3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ .project .pydevproject /.settings/ +venv +.idea +build From 8491fdec4c34c6ff615a0e54cdac23562c3f182b Mon Sep 17 00:00:00 2001 From: Jorge Date: Mon, 30 Sep 2019 19:54:25 +0100 Subject: [PATCH 12/63] [Jorge] Switch to use python sessions rather than requests --- README.rst | 17 +++++++++++++++++ webdav3/client.py | 19 ++++++++----------- webdav3/connection.py | 28 ---------------------------- 3 files changed, 25 insertions(+), 39 deletions(-) diff --git a/README.rst b/README.rst index e0e2f31..10648c5 100644 --- a/README.rst +++ b/README.rst @@ -8,6 +8,23 @@ webdavclient3 Based on https://github.com/designerror/webdav-client-python But uses `requests` instead of `PyCURL` +Sample Usage +____________ + +>>> from webdav3.client +>>> options = { +... webdav_hostname : +... webdav_login : +... webdav_password : +... } +>>> client = Client(options) +>>> client.verify = False # To not check SSL certificates (Default = True) +>>> client.session.proxies(...) # To set proxy directly into the session (Optional) +>>> client.session.auth(...) # To set proxy auth directly into the session (Optional) +>>> client.execute_request("mkdir", ) + + + Release Notes ============= diff --git a/webdav3/client.py b/webdav3/client.py index 82b26d6..b471dbb 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -89,6 +89,9 @@ class Client(object): # controls whether to verify the server's TLS certificate or not verify = True + # Sets the session for subsequent requests + session = requests.Session() + # HTTP headers for different actions http_header = { 'list': ["Accept: */*", "Depth: 1"], @@ -155,7 +158,7 @@ class Client(object): the specified action. :return: HTTP response of request. """ - response = requests.request( + response = self.session.request( method=Client.requests[action], url=self.get_url(path), auth=(self.webdav.login, self.webdav.password), @@ -197,7 +200,7 @@ class Client(object): def __init__(self, options): """Constructor of WebDAV client - :param options: the dictionary of connection options to WebDAV can include proxy server options. + :param options: the dictionary of connection options to WebDAV. WebDev settings: `webdav_hostname`: url for WebDAV server should contain protocol and ip address or domain name. Example: `https://webdav.server.com`. @@ -212,25 +215,19 @@ class Client(object): `webdav_send_speed`: (optional) rate limit data upload speed in Bytes per second. Defaults to unlimited speed. `webdav_verbose`: (optional) set verbose mode on.off. By default verbose mode is off. - Proxy settings (optional): - `proxy_hostname`: url to proxy server should contain protocol and ip address or domain name and if needed - port. Example: `https://proxy.server.com:8383`. - `proxy_login`: login name for proxy server. - `proxy_password`: password for proxy server. + """ webdav_options = get_options(option_type=WebDAVSettings, from_options=options) - proxy_options = get_options(option_type=ProxySettings, from_options=options) self.webdav = WebDAVSettings(webdav_options) - self.proxy = ProxySettings(proxy_options) self.default_options = {} def valid(self): - """Validates of WebDAV and proxy settings. + """Validates of WebDAV settings. :return: True in case settings are valid and False otherwise. """ - return True if self.webdav.valid() and self.proxy.valid() else False + return True if self.webdav.valid() else False @wrap_connection_error def list(self, remote_path=root): diff --git a/webdav3/connection.py b/webdav3/connection.py index c781797..50becc6 100644 --- a/webdav3/connection.py +++ b/webdav3/connection.py @@ -66,31 +66,3 @@ class WebDAVSettings(ConnectionSettings): if not self.token and not self.login: raise OptionNotValid(name="login", value=self.login, ns=self.ns) - - -class ProxySettings(ConnectionSettings): - ns = "proxy:" - prefix = "proxy_" - keys = {'hostname', 'login', 'password'} - - hostname = None - login = None - password = None - - def __init__(self, options): - - self.options = dict() - - for key in self.keys: - value = options.get(key, '') - self.options[key] = value - self.__dict__[key] = value - - def is_valid(self): - - if self.password and not self.login: - raise OptionNotValid(name="login", value=self.login, ns=self.ns) - - if self.login or self.password: - if not self.hostname: - raise OptionNotValid(name="hostname", value=self.hostname, ns=self.ns) From 3632c2a1e0b40a51839767d533248757ba46bfbf Mon Sep 17 00:00:00 2001 From: Jorge Date: Mon, 30 Sep 2019 20:41:01 +0100 Subject: [PATCH 13/63] [Jorge] Updated to run a GET with proxy auth as overridden in subs request --- webdav3/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webdav3/client.py b/webdav3/client.py index b471dbb..9d5b55d 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -158,6 +158,8 @@ class Client(object): the specified action. :return: HTTP response of request. """ + if self.session.auth: + self.session.request(method="GET", url=self.webdav.hostname) # (Re)Authenticates against the proxy response = self.session.request( method=Client.requests[action], url=self.get_url(path), From af85e056bdf0fc73ee8608fd14e418fa8e79922e Mon Sep 17 00:00:00 2001 From: Jorge Date: Mon, 30 Sep 2019 20:50:14 +0100 Subject: [PATCH 14/63] [Jorge] Updated to add verify to workaround request --- webdav3/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webdav3/client.py b/webdav3/client.py index 9d5b55d..b62e860 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -159,7 +159,7 @@ class Client(object): :return: HTTP response of request. """ if self.session.auth: - self.session.request(method="GET", url=self.webdav.hostname) # (Re)Authenticates against the proxy + self.session.request(method="GET", url=self.webdav.hostname, verify=self.verify) # (Re)Authenticates against the proxy response = self.session.request( method=Client.requests[action], url=self.get_url(path), From d4edab9044eaf3c28f509de2b19463499472c891 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Sun, 6 Oct 2019 18:27:45 +0300 Subject: [PATCH 15/63] Main version of Python is updated up to 3.7 And small style fixes --- .travis.yml | 2 +- README.rst | 2 ++ setup.py | 6 +++++- tests/test_client_unit.py | 22 +++++++++++----------- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 207751e..b8661df 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python python: - - "2.7" + - "3.7" install: - python setup.py develop script: diff --git a/README.rst b/README.rst index e0e2f31..959f34b 100644 --- a/README.rst +++ b/README.rst @@ -10,6 +10,8 @@ But uses `requests` instead of `PyCURL` Release Notes ============= +**Version 0.13 – TBD** + * Main version of Python is updated up to 3.7 **Version 0.12 - 21.06.2019** * Added depth argument in copy method in client.py by https://github.com/JesperHakansson diff --git a/setup.py b/setup.py index 39eefa2..a716a14 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,8 @@ setup( requires=['python (>= 2.7.6)'], install_requires=['requests', 'lxml', 'argcomplete'], scripts=['wdc'], - tests_require=['pytest', 'pyhamcrest', 'junit-xml', 'pytest-allure-adaptor'], + test_suite='tests', + tests_require=['pytest'], cmdclass={'install': Install, 'test': Test}, description='WebDAV client, based on original package https://github.com/designerror/webdav-client-python but ' 'uses requests instead of PyCURL', @@ -73,6 +74,9 @@ setup( 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Internet', 'Topic :: Software Development :: Libraries :: Python Modules', ], diff --git a/tests/test_client_unit.py b/tests/test_client_unit.py index 7489895..2959b48 100644 --- a/tests/test_client_unit.py +++ b/tests/test_client_unit.py @@ -24,7 +24,7 @@ class ClientTestCase(TestCase): def test_create_free_space_request_content(self): result = utils.create_free_space_request_content() self.assertEqual(result, b'\n' - b'') + b'') def test_parse_free_space_response(self): content = '/' \ @@ -55,7 +55,7 @@ class ClientTestCase(TestCase): } result = utils.create_get_property_request_content(option=option, ) self.assertEqual(result, b'\n' - b'') + b'') def test_create_get_property_request_content_name_only(self): option = { @@ -63,7 +63,7 @@ class ClientTestCase(TestCase): } result = utils.create_get_property_request_content(option=option) self.assertEqual(result, b'\n' - b'') + b'') def test_parse_get_property_response(self): content = '' \ @@ -81,7 +81,7 @@ class ClientTestCase(TestCase): } result = utils.create_set_property_batch_request_content(options=[option]) self.assertEqual(result, b'\n' - b'aValue') + b'aValue') def test_create_set_one_property_request_content_name_only(self): option = { @@ -89,7 +89,7 @@ class ClientTestCase(TestCase): } result = utils.create_set_property_batch_request_content(options=[option]) self.assertEqual(result, b'\n' - b'') + b'') def test_create_set_property_batch_request_content(self): options = [ @@ -106,8 +106,8 @@ class ClientTestCase(TestCase): ] result = utils.create_set_property_batch_request_content(options=options) self.assertEqual(result, b'\n' - b'aValueaValue2' - b'') + b'aValueaValue2' + b'') def test_create_set_property_batch_request_content_name_only(self): options = [ @@ -120,8 +120,8 @@ class ClientTestCase(TestCase): ] result = utils.create_set_property_batch_request_content(options=options) self.assertEqual(result, b'\n' - b'' - b'') + b'' + b'') def test_etree_to_string(self): tree = ElementTree(Element('test')) @@ -175,10 +175,10 @@ class ClientTestCase(TestCase): '' path = '/test_dir' hostname = 'https://webdav.yandex.ru' - result = utils.parse_is_dir_response(content.encode('utf-8'), path, hostname) + result = utils.parse_is_dir_response(content, path, hostname) self.assertTrue(result, 'It should be directory') - def test_parse_is_dir_response_directory(self): + def test_parse_is_dir_response_file(self): content = '/test_' \ 'dir/HTTP/1.1 200 OK2018-05-10T07' \ ':40:11Ztest_dirThu, 10 May 2018' \ From b663345e9dc8997b0c63c3fc0677d95e57c3cf96 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Sun, 13 Oct 2019 17:17:01 +0300 Subject: [PATCH 16/63] Tests are fixed --- tests/conftest.py | 32 ------- tests/response_dir.xml | 136 ++++++++++++++++++++++++++++++ tests/test_client_unit.py | 46 +--------- tests/webdav/test_authenticate.py | 1 - tests/webdav/test_connection.py | 11 --- tests/webdav/test_methods.py | 1 - webdav3/client.py | 2 + 7 files changed, 140 insertions(+), 89 deletions(-) delete mode 100644 tests/conftest.py create mode 100644 tests/response_dir.xml delete mode 100644 tests/webdav/test_authenticate.py delete mode 100644 tests/webdav/test_connection.py delete mode 100644 tests/webdav/test_methods.py diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 723db66..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,32 +0,0 @@ -__author__ = 'designerror' - -from hamcrest.core.base_matcher import BaseMatcher -from hamcrest.core.helpers.hasmethod import hasmethod - -class Valid(BaseMatcher): - def _matches(self, item): - return False if not hasmethod(item, 'valid') else item.valid() - -class NotValid(BaseMatcher): - def _matches(self, item): - return False if not hasmethod(item, 'valid') else not item.valid() - -class Success(BaseMatcher): - def _matches(self, item): - return item - -class NotSuccess(BaseMatcher): - def _matches(self, item): - return not item - -def valid(): - return Valid() - -def not_valid(): - return NotValid() - -def success(): - return Success() - -def not_success(): - return NotSuccess() \ No newline at end of file diff --git a/tests/response_dir.xml b/tests/response_dir.xml new file mode 100644 index 0000000..c0e3a7b --- /dev/null +++ b/tests/response_dir.xml @@ -0,0 +1,136 @@ + + + + / + + HTTP/1.1 200 OK + + 2012-04-04T20:00:00Z + disk + Wed, 04 Apr 2012 20:00:00 GMT + + + + + + + + /test_dir/ + + HTTP/1.1 200 OK + + 2018-05-10T07:31:13Z + test_dir + Thu, 10 May 2018 07:31:13 GMT + + + + + + + + /%D0%93%D0%BE%D1%80%D1%8B.jpg + + HTTP/1.1 200 OK + + 1392851f0668017168ee4b5a59d66e7b + 2018-05-09T14:44:28Z + Горы.jpg + Wed, 09 May 2018 14:44:28 GMT + image/jpeg + 1762478 + + + + + + /%D0%97%D0%B8%D0%BC%D0%B0.jpg + + HTTP/1.1 200 OK + + a64146fee5e15b3b94c204e544426d43 + 2018-05-09T14:44:28Z + Зима.jpg + Wed, 09 May 2018 14:44:28 GMT + image/jpeg + 1394575 + + + + + + /%D0%9C%D0%B8%D1%88%D0%BA%D0%B8.jpg + + HTTP/1.1 200 OK + + 569a1c98696050439b5b2a1ecfa52d19 + 2018-05-09T14:44:27Z + Мишки.jpg + Wed, 09 May 2018 14:44:27 GMT + image/jpeg + 1555830 + + + + + + /%D0%9C%D0%BE%D1%80%D0%B5.jpg + + HTTP/1.1 200 OK + + ab903d9cab031eca2a8f12f37bbc9d37 + 2018-05-09T14:44:27Z + Море.jpg + Wed, 09 May 2018 14:44:27 GMT + image/jpeg + 1080301 + + + + + + /%D0%9C%D0%BE%D1%81%D0%BA%D0%B2%D0%B0.jpg + + HTTP/1.1 200 OK + + d27d72a3059ad5ebed7a5470459d2670 + 2018-05-09T14:44:27Z + Москва.jpg + Wed, 09 May 2018 14:44:27 GMT + image/jpeg + 1454228 + + + + + + /%D0%A1%D0%B0%D0%BD%D0%BA%D1%82-%D0%9F%D0%B5%D1%82%D0%B5%D1%80%D0%B1%D1%83%D1%80%D0%B3.jpg + + HTTP/1.1 200 OK + + f1abe3b27b410128623fd1ca00a45c29 + 2018-05-09T14:44:27Z + Санкт-Петербург.jpg + Wed, 09 May 2018 14:44:27 GMT + image/jpeg + 2573704 + + + + + + /%D0%A5%D0%BB%D0%B5%D0%B1%D0%BD%D1%8B%D0%B5%20%D0%BA%D1%80%D0%BE%D1%88%D0%BA%D0%B8.mp4 + + HTTP/1.1 200 OK + + ea977f513074d5524bee3638798183b9 + 2018-05-09T14:44:28Z + Хлебные крошки.mp4 + Wed, 09 May 2018 14:44:28 GMT + video/mp4 + 31000079 + + + + + \ No newline at end of file diff --git a/tests/test_client_unit.py b/tests/test_client_unit.py index 2959b48..fe38c79 100644 --- a/tests/test_client_unit.py +++ b/tests/test_client_unit.py @@ -129,50 +129,8 @@ class ClientTestCase(TestCase): self.assertEqual(result, b'\n') def test_parse_is_dir_response_directory(self): - content = '/HTTP/1.1 200 OK2012-04-04T20:00:00ZdiskWed, 04 Apr 2012 20:00:00 GMT' \ - '/test_dir/HTTP/1.1 200 OK2018-05-10T07:31:13Ztest_dirThu, 10 May 2018 07:31:13 GMT/%D0%93%D0%BE%D1%80%D1%8B.jpg' \ - 'HTTP/1.1 200 OK1392851f0668017168ee4b' \ - '5a59d66e7b2018-05-09T14:44:28ZГоры.jpg' \ - 'Wed, 09 May 2018 14:44:28 GMTimage/jpeg1762478' \ - '/%D0%97%D0%B8%D0%BC%D0%B0.jpgHTTP/1.1 200 OKa64146fee5e15b3b94c204e544426d432018-05-09T14:44:28ZЗима.jpgWed, 09 May 2018 14:44:28 GMTimage/jpeg' \ - '1394575/%D0%9C%D0%B8%D1%88%D0%BA%D0%B8.jpg<' \ - 'd:status>HTTP/1.1 200 OK569a1c98696050439b5b2a1ecfa52d19' \ - '2018-05-09T14:44:27ZМишки.jpgWed, 09 May 2018 14:44:27 GMTimage/jpeg1555830/%D0%9C%D0%BE%D1%80%D0%B5.jpgHTTP' \ - '/1.1 200 OKab903d9cab031eca2a8f12f37bbc9d372018-05-09T14:44:27ZМоре.jpg' \ - 'Wed, 09 May 2018 14:44:27 GMTimage/jpeg1080301' \ - '/%D0%9C%D0%BE%D1%81%D0%BA%D0%B2%D0%B0.jpgHTTP/1' \ - '.1 200 OKd27d72a3059ad5ebed7a5470459d26702018-05-09T14:44:27ZМосква.jpg' \ - 'Wed, 09 May 2018 14:44:27 GMTimage/jpeg1454228' \ - '/%D0%A1%D0%B0%D0%BD%D0%BA%D1%82-%D0%9F%D0%B5%D1%82%D0%B5%D1%80%D0%B1%D1%83%D1%' \ - '80%D0%B3.jpgHTTP/1.1 200 OKf1abe3b27b' \ - '410128623fd1ca00a45c292018-05-09T14:44:27ZСанкт-Петербург.jpgWed, 09 May 2018 14:44:27 GMTimage/jpeg2573704/%D0%A5%D0%BB%D0%B5%' \ - 'D0%B1%D0%BD%D1%8B%D0%B5%20%D0%BA%D1%80%D0%BE%D1%88%D0%BA%D0%B8.mp4H' \ - 'TTP/1.1 200 OKea977f513074d5524bee3638798183b92018-05-09T14:44:28ZХлебные крошки.mp4Wed, 09 May 2018 14:44:28 GMTvideo/mp431000079' \ - '' + f = open('./tests/response_dir.xml') + content = f.read() path = '/test_dir' hostname = 'https://webdav.yandex.ru' result = utils.parse_is_dir_response(content, path, hostname) diff --git a/tests/webdav/test_authenticate.py b/tests/webdav/test_authenticate.py deleted file mode 100644 index c03e2b9..0000000 --- a/tests/webdav/test_authenticate.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'designerror' diff --git a/tests/webdav/test_connection.py b/tests/webdav/test_connection.py deleted file mode 100644 index 445483f..0000000 --- a/tests/webdav/test_connection.py +++ /dev/null @@ -1,11 +0,0 @@ -__author__ = 'designerror' - -import allure -#from hamcrest import * - -class TestRequiredOptions: - - def test_without_webdav_hostname(self): - options = { 'webdav_server': "https://webdav.yandex.ru"} - allure.attach('options', options.__str__()) - assert 1 diff --git a/tests/webdav/test_methods.py b/tests/webdav/test_methods.py deleted file mode 100644 index c03e2b9..0000000 --- a/tests/webdav/test_methods.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'designerror' diff --git a/webdav3/client.py b/webdav3/client.py index 82b26d6..5a8324a 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -960,6 +960,8 @@ class WebDavXmlUtils: :return: XML object of response for the remote resource defined by path. """ try: + if isinstance(content, str): + content = content.encode('utf-8') tree = etree.fromstring(content) responses = tree.findall("{DAV:}response") From 035c7d315e493f5c225ca3395aca9f8555f77160 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Sun, 13 Oct 2019 17:43:30 +0300 Subject: [PATCH 17/63] Update git ignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index bf2fda7..2af286f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ .project .pydevproject /.settings/ +.idea/ +venv/ From dc617d43aa0b08e98c8aaab7e546b22b23e9d78a Mon Sep 17 00:00:00 2001 From: Jorge Date: Mon, 30 Sep 2019 19:54:06 +0100 Subject: [PATCH 18/63] [Jorge] Updated to add venv/.idea/build --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2af286f..c22e3c6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ .project .pydevproject /.settings/ -.idea/ venv/ +.idea/ +build From 5a01cbf823c0270f4fd7f3f3c5e8f711a100a81a Mon Sep 17 00:00:00 2001 From: Jorge Date: Mon, 30 Sep 2019 19:54:25 +0100 Subject: [PATCH 19/63] [Jorge] Switch to use python sessions rather than requests --- README.rst | 17 +++++++++++++++++ webdav3/client.py | 19 ++++++++----------- webdav3/connection.py | 28 ---------------------------- 3 files changed, 25 insertions(+), 39 deletions(-) diff --git a/README.rst b/README.rst index 959f34b..2bb59e8 100644 --- a/README.rst +++ b/README.rst @@ -8,6 +8,23 @@ webdavclient3 Based on https://github.com/designerror/webdav-client-python But uses `requests` instead of `PyCURL` +Sample Usage +____________ + +>>> from webdav3.client +>>> options = { +... webdav_hostname : +... webdav_login : +... webdav_password : +... } +>>> client = Client(options) +>>> client.verify = False # To not check SSL certificates (Default = True) +>>> client.session.proxies(...) # To set proxy directly into the session (Optional) +>>> client.session.auth(...) # To set proxy auth directly into the session (Optional) +>>> client.execute_request("mkdir", ) + + + Release Notes ============= **Version 0.13 – TBD** diff --git a/webdav3/client.py b/webdav3/client.py index 5a8324a..ef90a75 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -89,6 +89,9 @@ class Client(object): # controls whether to verify the server's TLS certificate or not verify = True + # Sets the session for subsequent requests + session = requests.Session() + # HTTP headers for different actions http_header = { 'list': ["Accept: */*", "Depth: 1"], @@ -155,7 +158,7 @@ class Client(object): the specified action. :return: HTTP response of request. """ - response = requests.request( + response = self.session.request( method=Client.requests[action], url=self.get_url(path), auth=(self.webdav.login, self.webdav.password), @@ -197,7 +200,7 @@ class Client(object): def __init__(self, options): """Constructor of WebDAV client - :param options: the dictionary of connection options to WebDAV can include proxy server options. + :param options: the dictionary of connection options to WebDAV. WebDev settings: `webdav_hostname`: url for WebDAV server should contain protocol and ip address or domain name. Example: `https://webdav.server.com`. @@ -212,25 +215,19 @@ class Client(object): `webdav_send_speed`: (optional) rate limit data upload speed in Bytes per second. Defaults to unlimited speed. `webdav_verbose`: (optional) set verbose mode on.off. By default verbose mode is off. - Proxy settings (optional): - `proxy_hostname`: url to proxy server should contain protocol and ip address or domain name and if needed - port. Example: `https://proxy.server.com:8383`. - `proxy_login`: login name for proxy server. - `proxy_password`: password for proxy server. + """ webdav_options = get_options(option_type=WebDAVSettings, from_options=options) - proxy_options = get_options(option_type=ProxySettings, from_options=options) self.webdav = WebDAVSettings(webdav_options) - self.proxy = ProxySettings(proxy_options) self.default_options = {} def valid(self): - """Validates of WebDAV and proxy settings. + """Validates of WebDAV settings. :return: True in case settings are valid and False otherwise. """ - return True if self.webdav.valid() and self.proxy.valid() else False + return True if self.webdav.valid() else False @wrap_connection_error def list(self, remote_path=root): diff --git a/webdav3/connection.py b/webdav3/connection.py index c781797..50becc6 100644 --- a/webdav3/connection.py +++ b/webdav3/connection.py @@ -66,31 +66,3 @@ class WebDAVSettings(ConnectionSettings): if not self.token and not self.login: raise OptionNotValid(name="login", value=self.login, ns=self.ns) - - -class ProxySettings(ConnectionSettings): - ns = "proxy:" - prefix = "proxy_" - keys = {'hostname', 'login', 'password'} - - hostname = None - login = None - password = None - - def __init__(self, options): - - self.options = dict() - - for key in self.keys: - value = options.get(key, '') - self.options[key] = value - self.__dict__[key] = value - - def is_valid(self): - - if self.password and not self.login: - raise OptionNotValid(name="login", value=self.login, ns=self.ns) - - if self.login or self.password: - if not self.hostname: - raise OptionNotValid(name="hostname", value=self.hostname, ns=self.ns) From 0f0721554b7eb3c3ecb524249da8110aab0e302b Mon Sep 17 00:00:00 2001 From: Jorge Date: Mon, 30 Sep 2019 20:41:01 +0100 Subject: [PATCH 20/63] [Jorge] Updated to run a GET with proxy auth as overridden in subs request --- webdav3/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webdav3/client.py b/webdav3/client.py index ef90a75..45372fc 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -158,6 +158,8 @@ class Client(object): the specified action. :return: HTTP response of request. """ + if self.session.auth: + self.session.request(method="GET", url=self.webdav.hostname) # (Re)Authenticates against the proxy response = self.session.request( method=Client.requests[action], url=self.get_url(path), From 75a4241201ba4394bd6e19da1882e55702d6670a Mon Sep 17 00:00:00 2001 From: Jorge Date: Mon, 30 Sep 2019 20:50:14 +0100 Subject: [PATCH 21/63] [Jorge] Updated to add verify to workaround request --- webdav3/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webdav3/client.py b/webdav3/client.py index 45372fc..d923bd1 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -159,7 +159,7 @@ class Client(object): :return: HTTP response of request. """ if self.session.auth: - self.session.request(method="GET", url=self.webdav.hostname) # (Re)Authenticates against the proxy + self.session.request(method="GET", url=self.webdav.hostname, verify=self.verify) # (Re)Authenticates against the proxy response = self.session.request( method=Client.requests[action], url=self.get_url(path), From 7e409f1f1c02b212c71383d4fc02630ef7baf1d6 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Sun, 13 Oct 2019 17:54:45 +0300 Subject: [PATCH 22/63] Update git ignore --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 2bb59e8..6e68a1e 100644 --- a/README.rst +++ b/README.rst @@ -29,6 +29,7 @@ Release Notes ============= **Version 0.13 – TBD** * Main version of Python is updated up to 3.7 + * Switch to use python sessions rather than requests by https://github.com/delrey1 **Version 0.12 - 21.06.2019** * Added depth argument in copy method in client.py by https://github.com/JesperHakansson From 1fb79c36be572f2b8240b32d4571982b1791948a Mon Sep 17 00:00:00 2001 From: Emil 'Skeen' Madsen Date: Tue, 26 Nov 2019 12:05:33 +0100 Subject: [PATCH 23/63] Stripping suburl from paths in extract_response_for_path (#15) When accessing a server on a suburl, such as an Alfresco WebDav: http://172.17.0.8:8080/alfresco/webdav The file paths processed by extract_response_for_path, specifically href contains the suburl as a prefix, for instance: /alfresco/webdav/Sites/ rather than just Sites/. I am not sure if this is a specific problem with Alfresco WebDav, or a common issue. Either way, the provided code fixes this issue by removing the suburl from paths, if it exists. --- webdav3/client.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/webdav3/client.py b/webdav3/client.py index 82b26d6..0a4b308 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -16,10 +16,10 @@ from webdav3.exceptions import * from webdav3.urn import Urn try: - from urllib.parse import unquote, urlsplit + from urllib.parse import unquote, urlsplit, urlparse except ImportError: from urllib import unquote - from urlparse import urlsplit + from urlparse import urlsplit, urlparse __version__ = "0.2" log = logging.getLogger(__name__) @@ -959,10 +959,10 @@ class WebDavXmlUtils: :param hostname: the server hostname. :return: XML object of response for the remote resource defined by path. """ + prefix = urlparse(hostname).path try: tree = etree.fromstring(content) responses = tree.findall("{DAV:}response") - n_path = Urn.normalize_path(path) for resp in responses: @@ -970,6 +970,9 @@ class WebDavXmlUtils: if Urn.compare_path(n_path, href) is True: return resp + href_without_prefix = href[len(prefix):] if href.startswith(prefix) else href + if Urn.compare_path(n_path, href_without_prefix) is True: + return resp raise RemoteResourceNotFound(path) except etree.XMLSyntaxError: raise MethodNotSupported(name="is_dir", server=hostname) From 64a3b66efb77c563a81932e568cb206543b4dd4d Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Fri, 21 Jun 2019 15:20:25 +0300 Subject: [PATCH 24/63] Add webdav container --- .travis.yml | 10 ++++++++++ tests/test_client_it.py | 6 +++--- tests/webdav/test_connection.py | 0 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 tests/webdav/test_connection.py diff --git a/.travis.yml b/.travis.yml index b8661df..48814cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,16 @@ language: python + +services: + - docker + python: - "3.7" + +before_install: + - docker pull bytemark/webdav + - docker run -d --name webdav -e AUTH_TYPE=Basic -e USERNAME=alice -e PASSWORD=secret1234 -p 8585:80 bytemark/webdav + - docker ps -a + install: - python setup.py develop script: diff --git a/tests/test_client_it.py b/tests/test_client_it.py index cc4838b..4c10c46 100644 --- a/tests/test_client_it.py +++ b/tests/test_client_it.py @@ -20,9 +20,9 @@ class ClientTestCase(TestCase): def setUp(self): options = { - 'webdav_hostname': 'https://webdav.yandex.ru', - 'webdav_login': 'webdavclient.test2', - 'webdav_password': 'Qwerty123!' + 'webdav_hostname': 'http://localhost:8585', + 'webdav_login': 'alice', + 'webdav_password': 'secret1234' } self.client = Client(options) if path.exists(path=self.local_path_dir): diff --git a/tests/webdav/test_connection.py b/tests/webdav/test_connection.py new file mode 100644 index 0000000..e69de29 From ba2453800dd3c19de299ccb66c746e0dfcb1c030 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Wed, 27 Nov 2019 13:29:18 +0300 Subject: [PATCH 25/63] Readme update --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 6e68a1e..ac4bf1e 100644 --- a/README.rst +++ b/README.rst @@ -30,6 +30,8 @@ Release Notes **Version 0.13 – TBD** * Main version of Python is updated up to 3.7 * Switch to use python sessions rather than requests by https://github.com/delrey1 + * Stripping suburl from paths in extract_response_for_path by https://github.com/Skeen + * Added Docker Web DAV for CI **Version 0.12 - 21.06.2019** * Added depth argument in copy method in client.py by https://github.com/JesperHakansson From 78a979715d463b726dac3abb91e3620247bcee88 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Wed, 27 Nov 2019 14:57:22 +0300 Subject: [PATCH 26/63] Adapt to use Apache WebDAV Container for CI --- tests/test_client_it.py | 19 +++++-------------- tests/test_client_unit.py | 2 +- webdav3/client.py | 19 +++++++++++-------- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/tests/test_client_it.py b/tests/test_client_it.py index 4c10c46..abcc1c3 100644 --- a/tests/test_client_it.py +++ b/tests/test_client_it.py @@ -6,6 +6,7 @@ from os import path from unittest import TestCase from webdav3.client import Client +from webdav3.exceptions import MethodNotSupported class ClientTestCase(TestCase): @@ -43,7 +44,8 @@ class ClientTestCase(TestCase): self.assertGreater(file_list.__len__(), 0, 'Expected that amount of files more then 0') def test_free(self): - self.assertGreater(self.client.free(), 0, 'Expected that free space on WebDAV server is more then 0 bytes') + with self.assertRaises(MethodNotSupported): + self.assertGreater(self.client.free(), 0, 'Expected that free space on WebDAV server is more then 0 bytes') def test_check(self): self.assertTrue(self.client.check(), 'Expected that root directory is exist') @@ -54,16 +56,12 @@ class ClientTestCase(TestCase): self.client.mkdir(remote_path=self.remote_path_dir) self.assertTrue(self.client.check(remote_path=self.remote_path_dir), 'Expected the directory is created.') - @unittest.skip("Yandex brakes response for file it contains property resourcetype as collection but it should " - "be empty for file") def test_download_to(self): self._prepare_for_downloading() buff = BytesIO() self.client.download_from(buff=buff, remote_path=self.remote_path_file) - self.assertEqual(buff.getvalue(), 'test content for testing of webdav client') + self.assertEqual(buff.getvalue(), b'test content for testing of webdav client') - @unittest.skip("Yandex brakes response for file it contains property resourcetype as collection but it should " - "be empty for file") def test_download(self): self._prepare_for_downloading() self.client.download(local_path=self.local_path_dir, remote_path=self.remote_path_dir) @@ -71,14 +69,11 @@ class ClientTestCase(TestCase): self.assertTrue(path.isdir(self.local_path_dir), 'Expected this is a directory.') self.assertTrue(path.exists(self.local_path_dir + os.path.sep + self.local_file), 'Expected the file is downloaded') - self.assertTrue(path.isfile(self.local_path_dir + os.path.sep + self.local_path_file), + self.assertTrue(path.isfile(self.local_path_dir + os.path.sep + self.local_file), 'Expected this is a file') - @unittest.skip("Yandex brakes response for file it contains property resourcetype as collection but it should " - "be empty for file") def test_download_sync(self): self._prepare_for_downloading() - os.mkdir(self.local_path_dir) def callback(): self.assertTrue(path.exists(self.local_path_dir + os.path.sep + self.local_file), @@ -91,11 +86,8 @@ class ClientTestCase(TestCase): self.assertTrue(path.exists(self.local_path_dir + os.path.sep + self.local_file), 'Expected the file has already been downloaded') - @unittest.skip("Yandex brakes response for file it contains property resourcetype as collection but it should " - "be empty for file") def test_download_async(self): self._prepare_for_downloading() - os.mkdir(self.local_path_dir) def callback(): self.assertTrue(path.exists(self.local_path_dir + os.path.sep + self.local_file), @@ -166,7 +158,6 @@ class ClientTestCase(TestCase): def test_info(self): self._prepare_for_downloading() result = self.client.info(remote_path=self.remote_path_file) - self.assertEqual(result['name'], 'test.txt') self.assertEqual(result['size'], '41') self.assertTrue('created' in result) self.assertTrue('modified' in result) diff --git a/tests/test_client_unit.py b/tests/test_client_unit.py index fe38c79..4e4378b 100644 --- a/tests/test_client_unit.py +++ b/tests/test_client_unit.py @@ -129,7 +129,7 @@ class ClientTestCase(TestCase): self.assertEqual(result, b'\n') def test_parse_is_dir_response_directory(self): - f = open('./tests/response_dir.xml') + f = open('./tests/response_dir.xml', encoding='utf-8') content = f.read() path = '/test_dir' hostname = 'https://webdav.yandex.ru' diff --git a/webdav3/client.py b/webdav3/client.py index 7640ebc..b832baf 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -21,7 +21,6 @@ except ImportError: from urllib import unquote from urlparse import urlsplit, urlparse -__version__ = "0.2" log = logging.getLogger(__name__) @@ -74,6 +73,7 @@ def wrap_connection_error(fn): raise ConnectionException(re) else: return res + return _wrapper @@ -128,7 +128,7 @@ class Client(object): if self.webdav.token: webdav_token = "Authorization: OAuth {token}".format(token=self.webdav.token) headers.append(webdav_token) - return dict([map(lambda s: s.strip(), i.split(':')) for i in headers]) + return dict([map(lambda s: s.strip(), i.split(':', 1)) for i in headers]) def get_url(self, path): """Generates url by uri path. @@ -159,7 +159,7 @@ class Client(object): :return: HTTP response of request. """ if self.session.auth: - self.session.request(method="GET", url=self.webdav.hostname, verify=self.verify) # (Re)Authenticates against the proxy + self.session.request(method="GET", url=self.webdav.hostname, verify=self.verify) # (Re)Authenticates against the proxy response = self.session.request( method=Client.requests[action], url=self.get_url(path), @@ -476,7 +476,7 @@ class Client(object): raise RemoteParentNotFound(urn.path()) with open(local_path, "rb") as local_file: - self.execute_request(action='upload', path=urn.quote(), data=local_file) + self.execute_request(action='upload', path=urn.quote(), data=local_file) def upload_sync(self, remote_path, local_path, callback=None): """Uploads resource to remote path on WebDAV server synchronously. @@ -519,9 +519,12 @@ class Client(object): if not self.check(urn_to.parent()): raise RemoteParentNotFound(urn_to.path()) - header_destination = "Destination: {path}".format(path=self.get_full_path(urn_to)) - header_depth = "Depth: {depth}".format(depth=depth) - self.execute_request(action='copy', path=urn_from.quote(), headers_ext=[header_destination, header_depth]) + headers = [ + "Destination: {url}".format(url=self.get_url(urn_to)) + ] + if self.is_dir(urn_from.path()): + headers.append("Depth: {depth}".format(depth=depth)) + self.execute_request(action='copy', path=urn_from.quote(), headers_ext=headers) @wrap_connection_error def move(self, remote_path_from, remote_path_to, overwrite=False): @@ -540,7 +543,7 @@ class Client(object): if not self.check(urn_to.parent()): raise RemoteParentNotFound(urn_to.path()) - header_destination = "Destination: {path}".format(path=self.get_full_path(urn_to)) + header_destination = "Destination: {path}".format(path=self.get_url(urn_to)) header_overwrite = "Overwrite: {flag}".format(flag="T" if overwrite else "F") self.execute_request(action='move', path=urn_from.quote(), headers_ext=[header_destination, header_overwrite]) From 5b1285392c3ee7e6f48bcc808e08b409d619b166 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Wed, 27 Nov 2019 15:18:02 +0300 Subject: [PATCH 27/63] Changed HEAD to GET method for 'check' request due of not all servers support HEAD --- README.rst | 1 + webdav3/client.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ac4bf1e..7efe3d4 100644 --- a/README.rst +++ b/README.rst @@ -32,6 +32,7 @@ Release Notes * Switch to use python sessions rather than requests by https://github.com/delrey1 * Stripping suburl from paths in extract_response_for_path by https://github.com/Skeen * Added Docker Web DAV for CI + * Changed HEAD to GET method for 'check' request due of not all servers support HEAD by request of https://github.com/danieleTrimarchi **Version 0.12 - 21.06.2019** * Added depth argument in copy method in client.py by https://github.com/JesperHakansson diff --git a/webdav3/client.py b/webdav3/client.py index b832baf..42a510c 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -184,7 +184,7 @@ class Client(object): 'move': "MOVE", 'mkdir': "MKCOL", 'clean': "DELETE", - 'check': "HEAD", + 'check': "GET", 'list': "PROPFIND", 'free': "PROPFIND", 'info': "PROPFIND", From 6415617852f495aa98efc9e0887d348632def781 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Wed, 27 Nov 2019 15:45:53 +0300 Subject: [PATCH 28/63] Tests for push and pull methods --- tests/test_client_it.py | 28 +++++++++++++++++++++++++++- tests/webdav/test_connection.py | 0 2 files changed, 27 insertions(+), 1 deletion(-) delete mode 100644 tests/webdav/test_connection.py diff --git a/tests/test_client_it.py b/tests/test_client_it.py index abcc1c3..f8c5856 100644 --- a/tests/test_client_it.py +++ b/tests/test_client_it.py @@ -12,8 +12,10 @@ from webdav3.exceptions import MethodNotSupported class ClientTestCase(TestCase): remote_path_file = 'test_dir/test.txt' remote_path_file2 = 'test_dir2/test.txt' + remote_inner_path_file = 'test_dir/inner/test.txt' remote_path_dir = 'test_dir' remote_path_dir2 = 'test_dir2' + remote_inner_path_dir = 'test_dir/inner' local_base_dir = 'tests/' local_file = 'test.txt' local_file_path = local_base_dir + 'test.txt' @@ -207,13 +209,37 @@ class ClientTestCase(TestCase): option={'namespace': 'test', 'name': 'aProperty2'}) self.assertEqual(result, 'aValue2', 'Second property value should be set') - def _prepare_for_downloading(self): + def test_pull(self): + self._prepare_for_downloading(True) + self.client.pull(self.remote_path_dir, self.local_path_dir) + self.assertTrue(path.exists(self.local_path_dir), 'Expected the directory is downloaded.') + self.assertTrue(path.exists(self.local_path_dir + os.path.sep + 'inner'), 'Expected the directory is downloaded.') + self.assertTrue(path.exists(self.local_path_dir + os.path.sep + 'inner'), 'Expected the directory is downloaded.') + self.assertTrue(path.isdir(self.local_path_dir), 'Expected this is a directory.') + self.assertTrue(path.isdir(self.local_path_dir + os.path.sep + 'inner'), 'Expected this is a directory.') + self.assertTrue(path.exists(self.local_path_dir + os.path.sep + self.local_file), + 'Expected the file is downloaded') + self.assertTrue(path.isfile(self.local_path_dir + os.path.sep + self.local_file), + 'Expected this is a file') + + def test_push(self): + self._prepare_for_uploading() + self.client.push(self.remote_path_dir, self.local_path_dir) + self.assertTrue(self.client.check(self.remote_path_dir), 'Expected the directory is created.') + self.assertTrue(self.client.check(self.remote_path_file), 'Expected the file is uploaded.') + + def _prepare_for_downloading(self, inner_dir=False): if not self.client.check(remote_path=self.remote_path_dir): self.client.mkdir(remote_path=self.remote_path_dir) if not self.client.check(remote_path=self.remote_path_file): self.client.upload_file(remote_path=self.remote_path_file, local_path=self.local_file_path) if not path.exists(self.local_path_dir): os.makedirs(self.local_path_dir) + if inner_dir: + if not self.client.check(remote_path=self.remote_inner_path_dir): + self.client.mkdir(remote_path=self.remote_inner_path_dir) + if not self.client.check(remote_path=self.remote_inner_path_file): + self.client.upload_file(remote_path=self.remote_inner_path_file, local_path=self.local_file_path) def _prepare_for_uploading(self): if not self.client.check(remote_path=self.remote_path_dir): diff --git a/tests/webdav/test_connection.py b/tests/webdav/test_connection.py deleted file mode 100644 index e69de29..0000000 From 6b90e1d16cdcd5436237d41d46c955ff10cb56e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20K=C3=BCchel?= Date: Tue, 26 Nov 2019 00:43:00 +0100 Subject: [PATCH 29/63] remove a costy is_dir-check on obvious directories to speed up a pull --- webdav3/client.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/webdav3/client.py b/webdav3/client.py index 42a510c..3d29207 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -691,6 +691,7 @@ class Client(object): urn = Urn(remote_directory, directory=True) + ## here we check for the current directory if not self.is_dir(urn.path()): raise OptionNotValid(name="remote_path", value=remote_directory) @@ -699,6 +700,7 @@ class Client(object): local_resource_names = listdir(local_directory) + ## here we get all files and directories in the current directory paths = self.list(urn.path()) expression = "{begin}{end}".format(begin="^", end=remote_directory) remote_resource_names = prune(paths, expression) @@ -711,9 +713,12 @@ class Client(object): remote_urn = Urn(remote_path) - if self.is_dir(remote_urn.path()): + ## if the string ends with an "/" it *must* be a directory + ## no need to check it here + if remote_urn.path().endswith("/"): if not os.path.exists(local_path): os.mkdir(local_path) + ## nevertheless, since we pull the directory, it will be checked up there self.pull(remote_directory=remote_path, local_directory=local_path) else: if remote_resource_name in local_resource_names: From 8a919ac86688ae972911670077f83391904c6d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20K=C3=BCchel?= Date: Tue, 26 Nov 2019 07:25:24 +0100 Subject: [PATCH 30/63] on pull() return a boolean, if something was changed on disk or not --- webdav3/client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/webdav3/client.py b/webdav3/client.py index 3d29207..65a6b1b 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -689,6 +689,8 @@ class Client(object): def prune(src, exp): return [sub(exp, "", item) for item in src] + updated=False + urn = Urn(remote_directory, directory=True) ## here we check for the current directory @@ -717,6 +719,7 @@ class Client(object): ## no need to check it here if remote_urn.path().endswith("/"): if not os.path.exists(local_path): + updated=True os.mkdir(local_path) ## nevertheless, since we pull the directory, it will be checked up there self.pull(remote_directory=remote_path, local_directory=local_path) @@ -724,6 +727,8 @@ class Client(object): if remote_resource_name in local_resource_names: continue self.download_file(remote_path=remote_path, local_path=local_path) + updated=True + return updated def sync(self, remote_directory, local_directory): From 13ba29604ac07213618319cae78529e678b7231a Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Wed, 27 Nov 2019 15:48:03 +0300 Subject: [PATCH 31/63] Style fixes --- webdav3/client.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/webdav3/client.py b/webdav3/client.py index 65a6b1b..f7d9593 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -689,11 +689,10 @@ class Client(object): def prune(src, exp): return [sub(exp, "", item) for item in src] - updated=False + updated = False urn = Urn(remote_directory, directory=True) - ## here we check for the current directory if not self.is_dir(urn.path()): raise OptionNotValid(name="remote_path", value=remote_directory) @@ -702,7 +701,6 @@ class Client(object): local_resource_names = listdir(local_directory) - ## here we get all files and directories in the current directory paths = self.list(urn.path()) expression = "{begin}{end}".format(begin="^", end=remote_directory) remote_resource_names = prune(paths, expression) @@ -715,19 +713,16 @@ class Client(object): remote_urn = Urn(remote_path) - ## if the string ends with an "/" it *must* be a directory - ## no need to check it here if remote_urn.path().endswith("/"): if not os.path.exists(local_path): - updated=True + updated = True os.mkdir(local_path) - ## nevertheless, since we pull the directory, it will be checked up there self.pull(remote_directory=remote_path, local_directory=local_path) else: if remote_resource_name in local_resource_names: continue self.download_file(remote_path=remote_path, local_path=local_path) - updated=True + updated = True return updated def sync(self, remote_directory, local_directory): From c90ee856fd0856199750e5a0ff1bd2000ad7b3cb Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Wed, 27 Nov 2019 15:53:24 +0300 Subject: [PATCH 32/63] Update Readme --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7efe3d4..e2d0d21 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ webdavclient3 ============= -.. image:: https://travis-ci.com/ezhov-evgeny/webdav-client-python-3.svg?branch=master +.. image:: https://travis-ci.com/ezhov-evgeny/webdav-client-python-3.svg?branch=develop :target: https://travis-ci.com/ezhov-evgeny/webdav-client-python-3 @@ -33,6 +33,7 @@ Release Notes * Stripping suburl from paths in extract_response_for_path by https://github.com/Skeen * Added Docker Web DAV for CI * Changed HEAD to GET method for 'check' request due of not all servers support HEAD by request of https://github.com/danieleTrimarchi + * Removed a costy is_dir-check on obvious directories to speed up a pull by https://github.com/jolly-jump **Version 0.12 - 21.06.2019** * Added depth argument in copy method in client.py by https://github.com/JesperHakansson From 64bbd967bc869a6896d583b866fb4af401e7adee Mon Sep 17 00:00:00 2001 From: dzhuang Date: Wed, 2 Oct 2019 23:25:33 +0800 Subject: [PATCH 33/63] Disable client.check by default. --- setup.py | 7 ++++++- webdav3/client.py | 7 +++++++ webdav3/connection.py | 3 ++- webdav3/exceptions.py | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index a716a14..3ca4460 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,11 @@ class Test(TestCommand): errno = pytest.main(self.pytest_args) sys.exit(errno) +try: + long_description = open('README.rst', encoding="utf-8").read() +except TypeError: + long_description = open('README.rst').read() + setup( name='webdavclient3', version=version, @@ -54,7 +59,7 @@ setup( cmdclass={'install': Install, 'test': Test}, description='WebDAV client, based on original package https://github.com/designerror/webdav-client-python but ' 'uses requests instead of PyCURL', - long_description=open('README.rst').read(), + long_description=long_description, author='Evgeny Ezhov', author_email='ezhov.evgeny@gmail.com', url='https://github.com/ezhov-evgeny/webdav-client-python-3', diff --git a/webdav3/client.py b/webdav3/client.py index f7d9593..3b7a34a 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -172,6 +172,10 @@ class Client(object): ) if response.status_code == 507: raise NotEnoughSpace() + if response.status_code == 404: + raise RemoteResourceNotFound(path=path) + if response.status_code == 405: + raise MethodNotSupported(name=action, server=hostname) if response.status_code >= 400: raise ResponseErrorCode(url=self.get_url(path), code=response.status_code, message=response.content) return response @@ -269,6 +273,9 @@ class Client(object): :param remote_path: (optional) path to resource on WebDAV server. Defaults is root directory of WebDAV. :return: True if resource is exist or False otherwise """ + + if not self.webdav.do_check: + return True urn = Urn(remote_path) try: response = self.execute_request(action='check', path=urn.quote()) diff --git a/webdav3/connection.py b/webdav3/connection.py index 50becc6..a90854a 100644 --- a/webdav3/connection.py +++ b/webdav3/connection.py @@ -22,7 +22,7 @@ class WebDAVSettings(ConnectionSettings): ns = "webdav:" prefix = "webdav_" keys = {'hostname', 'login', 'password', 'token', 'root', 'cert_path', 'key_path', 'recv_speed', 'send_speed', - 'verbose'} + 'verbose', 'do_check'} hostname = None login = None @@ -34,6 +34,7 @@ class WebDAVSettings(ConnectionSettings): recv_speed = None send_speed = None verbose = None + do_check = False def __init__(self, options): diff --git a/webdav3/exceptions.py b/webdav3/exceptions.py index 388f8de..096c1f3 100644 --- a/webdav3/exceptions.py +++ b/webdav3/exceptions.py @@ -55,7 +55,7 @@ class MethodNotSupported(WebDavException): self.server = server def __str__(self): - return "Method {name} not supported for {server}".format(name=self.name, server=self.server) + return "Method '{name}' not supported for {server}".format(name=self.name, server=self.server) class ConnectionException(WebDavException): From dc7d90846266895fd298937aed962685e49d62d8 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Wed, 27 Nov 2019 16:15:21 +0300 Subject: [PATCH 34/63] Added an option to disable check in case WebDAV server is not support it --- README.rst | 1 + webdav3/client.py | 4 +++- webdav3/connection.py | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index e2d0d21..f23e81c 100644 --- a/README.rst +++ b/README.rst @@ -34,6 +34,7 @@ Release Notes * Added Docker Web DAV for CI * Changed HEAD to GET method for 'check' request due of not all servers support HEAD by request of https://github.com/danieleTrimarchi * Removed a costy is_dir-check on obvious directories to speed up a pull by https://github.com/jolly-jump + * Added an option to disable check in case WebDAV server is not support it by request of https://github.com/dzhuang **Version 0.12 - 21.06.2019** * Added depth argument in copy method in client.py by https://github.com/JesperHakansson diff --git a/webdav3/client.py b/webdav3/client.py index 3b7a34a..fecc5d4 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -274,11 +274,13 @@ class Client(object): :return: True if resource is exist or False otherwise """ - if not self.webdav.do_check: + if self.webdav.disable_check: return True urn = Urn(remote_path) try: response = self.execute_request(action='check', path=urn.quote()) + except RemoteResourceNotFound: + return False except ResponseErrorCode: return False diff --git a/webdav3/connection.py b/webdav3/connection.py index a90854a..f24593c 100644 --- a/webdav3/connection.py +++ b/webdav3/connection.py @@ -22,7 +22,7 @@ class WebDAVSettings(ConnectionSettings): ns = "webdav:" prefix = "webdav_" keys = {'hostname', 'login', 'password', 'token', 'root', 'cert_path', 'key_path', 'recv_speed', 'send_speed', - 'verbose', 'do_check'} + 'verbose', 'disable_check'} hostname = None login = None @@ -34,7 +34,7 @@ class WebDAVSettings(ConnectionSettings): recv_speed = None send_speed = None verbose = None - do_check = False + disable_check = False def __init__(self, options): From fe3296f20328ae48711bc3f4f6e5bfb203602a85 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Wed, 27 Nov 2019 21:30:57 +0300 Subject: [PATCH 35/63] Support Python 2.7 in tests --- .travis.yml | 1 + tests/test_client_unit.py | 8 ++++++-- webdav3/client.py | 7 ++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 48814cb..9309718 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ services: - docker python: + - "2.7" - "3.7" before_install: diff --git a/tests/test_client_unit.py b/tests/test_client_unit.py index 4e4378b..be544d1 100644 --- a/tests/test_client_unit.py +++ b/tests/test_client_unit.py @@ -129,8 +129,12 @@ class ClientTestCase(TestCase): self.assertEqual(result, b'\n') def test_parse_is_dir_response_directory(self): - f = open('./tests/response_dir.xml', encoding='utf-8') - content = f.read() + try: + f = open('./tests/response_dir.xml', encoding='utf-8') + content = f.read().encode('utf-8') + except: + f = open('./tests/response_dir.xml') + content = f.read().decode('utf-8').encode('utf-8') path = '/test_dir' hostname = 'https://webdav.yandex.ru' result = utils.parse_is_dir_response(content, path, hostname) diff --git a/webdav3/client.py b/webdav3/client.py index fecc5d4..8d138b8 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -2,15 +2,14 @@ import functools import logging +import lxml.etree as etree import os +import requests import shutil import threading from io import BytesIO from re import sub -import lxml.etree as etree -import requests - from webdav3.connection import * from webdav3.exceptions import * from webdav3.urn import Urn @@ -977,8 +976,6 @@ class WebDavXmlUtils: """ prefix = urlparse(hostname).path try: - if isinstance(content, str): - content = content.encode('utf-8') tree = etree.fromstring(content) responses = tree.findall("{DAV:}response") n_path = Urn.normalize_path(path) From c3a15dd14539f115d55cf29c627b79a9c74a329e Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Wed, 27 Nov 2019 21:51:23 +0300 Subject: [PATCH 36/63] Added SonarQube analysis --- .travis.yml | 10 +++++++++- sonar-project.properties | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 sonar-project.properties diff --git a/.travis.yml b/.travis.yml index 9309718..9ccd034 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,11 @@ language: python +dist: xenial + +addons: + sonarcloud: + organization: "ezhov-evgeny" + token: + secure: f0f714f3bea6bd103e3eb82724ef3bb0d3b54d1d services: - docker @@ -15,4 +22,5 @@ before_install: install: - python setup.py develop script: - - python setup.py test \ No newline at end of file + - python setup.py test + - sonar-scanner -X diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..9b05135 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,9 @@ +sonar.projectKey=ezhov-evgeny_webdav-client-python-3 +sonar.organization=ezhov-evgeny +# This is the name and version displayed in the SonarCloud UI. +sonar.projectName=webdav-client-python-3 +sonar.projectVersion=0.13 +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +sonar.sources=. +# Encoding of the source code. Default is default system encoding +sonar.sourceEncoding=UTF-8 \ No newline at end of file From a3a8e31059b36acd752e8ebb975e22114bf1ec95 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Wed, 27 Nov 2019 22:31:12 +0300 Subject: [PATCH 37/63] Update readme and setup --- README.rst => README.md | 20 ++++++++------------ setup.py | 15 ++++----------- 2 files changed, 12 insertions(+), 23 deletions(-) rename README.rst => README.md (89%) diff --git a/README.rst b/README.md similarity index 89% rename from README.rst rename to README.md index f23e81c..03a51a9 100644 --- a/README.rst +++ b/README.md @@ -1,16 +1,13 @@ -webdavclient3 -============= - -.. image:: https://travis-ci.com/ezhov-evgeny/webdav-client-python-3.svg?branch=develop - :target: https://travis-ci.com/ezhov-evgeny/webdav-client-python-3 - +webdavclient3 [![Build Status](https://travis-ci.com/ezhov-evgeny/webdav-client-python-3.svg?branch=develop)](https://travis-ci.com/ezhov-evgeny/webdav-client-python-3) [![PyPI](https://img.shields.io/pypi/v/webdavclient3)](https://pypi.org/project/webdavclient3/) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/webdavclient3) +========= Based on https://github.com/designerror/webdav-client-python But uses `requests` instead of `PyCURL` Sample Usage -____________ - +------------ +`pip install webdavclient3` +``` >>> from webdav3.client >>> options = { ... webdav_hostname : @@ -22,12 +19,11 @@ ____________ >>> client.session.proxies(...) # To set proxy directly into the session (Optional) >>> client.session.auth(...) # To set proxy auth directly into the session (Optional) >>> client.execute_request("mkdir", ) - - +``` Release Notes -============= -**Version 0.13 – TBD** +------------- +**Version 0.13 – 27.11.2019** * Main version of Python is updated up to 3.7 * Switch to use python sessions rather than requests by https://github.com/delrey1 * Stripping suburl from paths in extract_response_for_path by https://github.com/Skeen diff --git a/setup.py b/setup.py index 3ca4460..d278cc7 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import setup, find_packages from setuptools.command.install import install as InstallCommand from setuptools.command.test import test as TestCommand -version = "0.12" +version = "0.13" requirements = "libxml2-dev libxslt-dev python-dev" @@ -43,9 +43,9 @@ class Test(TestCommand): sys.exit(errno) try: - long_description = open('README.rst', encoding="utf-8").read() + long_description = open('README.md', encoding="utf-8").read() except TypeError: - long_description = open('README.rst').read() + long_description = open('README.md').read() setup( name='webdavclient3', @@ -60,6 +60,7 @@ setup( description='WebDAV client, based on original package https://github.com/designerror/webdav-client-python but ' 'uses requests instead of PyCURL', long_description=long_description, + long_description_content_type='text/markdown', author='Evgeny Ezhov', author_email='ezhov.evgeny@gmail.com', url='https://github.com/ezhov-evgeny/webdav-client-python-3', @@ -72,15 +73,7 @@ setup( 'Operating System :: MacOS', 'Operating System :: Microsoft', 'Operating System :: Unix', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.0', - 'Programming Language :: Python :: 3.1', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Topic :: Internet', 'Topic :: Software Development :: Libraries :: Python Modules', From 120a6adfab9e8c233f5c929071ddf39f2e9e1883 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Wed, 27 Nov 2019 23:03:38 +0300 Subject: [PATCH 38/63] Added Coverage in SonarQube --- .travis.yml | 6 ++++-- README.md | 2 +- sonar-project.properties | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9ccd034..0277e80 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,8 @@ before_install: install: - python setup.py develop + - pip install coverage script: - - python setup.py test - - sonar-scanner -X + - coverage run setup.py test + - coverage xml + - sonar-scanner diff --git a/README.md b/README.md index 03a51a9..c80cf7d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -webdavclient3 [![Build Status](https://travis-ci.com/ezhov-evgeny/webdav-client-python-3.svg?branch=develop)](https://travis-ci.com/ezhov-evgeny/webdav-client-python-3) [![PyPI](https://img.shields.io/pypi/v/webdavclient3)](https://pypi.org/project/webdavclient3/) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/webdavclient3) +webdavclient3 [![Build Status](https://travis-ci.com/ezhov-evgeny/webdav-client-python-3.svg?branch=develop)](https://travis-ci.com/ezhov-evgeny/webdav-client-python-3) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ezhov-evgeny_webdav-client-python-3&metric=alert_status)](https://sonarcloud.io/dashboard?id=ezhov-evgeny_webdav-client-python-3) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ezhov-evgeny_webdav-client-python-3&metric=coverage)](https://sonarcloud.io/dashboard?id=ezhov-evgeny_webdav-client-python-3) [![PyPI](https://img.shields.io/pypi/v/webdavclient3)](https://pypi.org/project/webdavclient3/) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/webdavclient3) ========= Based on https://github.com/designerror/webdav-client-python diff --git a/sonar-project.properties b/sonar-project.properties index 9b05135..935a751 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -4,6 +4,8 @@ sonar.organization=ezhov-evgeny sonar.projectName=webdav-client-python-3 sonar.projectVersion=0.13 # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. -sonar.sources=. +sonar.sources=webdav3 +sonar.tests=tests +sonar.python.coverage.reportPaths=coverage.xml # Encoding of the source code. Default is default system encoding sonar.sourceEncoding=UTF-8 \ No newline at end of file From 1859e43daf6b039b4f7a03a6b6f5d4a3f05e4d3a Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Thu, 28 Nov 2019 14:20:29 +0300 Subject: [PATCH 39/63] Add manual for using API --- README.md | 286 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 271 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c80cf7d..08df687 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,280 @@ -webdavclient3 [![Build Status](https://travis-ci.com/ezhov-evgeny/webdav-client-python-3.svg?branch=develop)](https://travis-ci.com/ezhov-evgeny/webdav-client-python-3) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ezhov-evgeny_webdav-client-python-3&metric=alert_status)](https://sonarcloud.io/dashboard?id=ezhov-evgeny_webdav-client-python-3) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ezhov-evgeny_webdav-client-python-3&metric=coverage)](https://sonarcloud.io/dashboard?id=ezhov-evgeny_webdav-client-python-3) [![PyPI](https://img.shields.io/pypi/v/webdavclient3)](https://pypi.org/project/webdavclient3/) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/webdavclient3) +webdavclient3 ========= +[![Build Status](https://travis-ci.com/ezhov-evgeny/webdav-client-python-3.svg?branch=develop)](https://travis-ci.com/ezhov-evgeny/webdav-client-python-3) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ezhov-evgeny_webdav-client-python-3&metric=alert_status)](https://sonarcloud.io/dashboard?id=ezhov-evgeny_webdav-client-python-3) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ezhov-evgeny_webdav-client-python-3&metric=coverage)](https://sonarcloud.io/dashboard?id=ezhov-evgeny_webdav-client-python-3) +[![PyPI](https://img.shields.io/pypi/v/webdavclient3)](https://pypi.org/project/webdavclient3/) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/webdavclient3) -Based on https://github.com/designerror/webdav-client-python -But uses `requests` instead of `PyCURL` +Package webdavclient3 based on https://github.com/designerror/webdav-client-python but uses `requests` instead of `PyCURL`. +It provides easy way to work with WebDAV-servers. + +Installation +------------ +```bash +$ pip install webdavclient3 +``` Sample Usage ------------ -`pip install webdavclient3` +```python +from webdav3.client import Client +options = { + 'webdav_hostname': "https://webdav.server.ru", + 'webdav_login': "login", + 'webdav_password': "password" +} +client = Client(options) +client.verify = False # To not check SSL certificates (Default = True) +client.session.proxies(...) # To set proxy directly into the session (Optional) +client.session.auth(...) # To set proxy auth directly into the session (Optional) +client.execute_request("mkdir", 'directory_name') ``` ->>> from webdav3.client ->>> options = { -... webdav_hostname : -... webdav_login : -... webdav_password : -... } ->>> client = Client(options) ->>> client.verify = False # To not check SSL certificates (Default = True) ->>> client.session.proxies(...) # To set proxy directly into the session (Optional) ->>> client.session.auth(...) # To set proxy auth directly into the session (Optional) ->>> client.execute_request("mkdir", ) + +Webdav API +========== + +Webdav API is a set of webdav methods of work with cloud storage. This set includes the following methods: +`check`, `free`, `info`, `list`, `mkdir`, `clean`, `copy`, `move`, `download`, `upload`, `publish` and `unpublish`. + +**Configuring the client** + +Required keys for configuring client connection with WevDAV-server are webdav\_hostname and webdav\_login, webdav\_password. + +```python +from webdav3.client import Client + +options = { + 'webdav_hostname': "https://webdav.server.ru", + 'webdav_login': "login", + 'webdav_password': "password" +} +client = Client(options) +``` + +When a proxy server you need to specify settings to connect through it. + +```python +from webdav3.client import Client +options = { + 'webdav_hostname': "https://webdav.server.ru", + 'webdav_login': "w_login", + 'webdav_password': "w_password", + 'proxy_hostname': "http://127.0.0.1:8080", + 'proxy_login': "p_login", + 'proxy_password': "p_password" +} +client = Client(options) +``` + +If you want to use the certificate path to certificate and private key is defined as follows: + +```python +from webdav3.client import Client +options = { + 'webdav_hostname': "https://webdav.server.ru", + 'webdav_login': "w_login", + 'webdav_password': "w_password", + 'cert_path': "/etc/ssl/certs/certificate.crt", + 'key_path': "/etc/ssl/private/certificate.key" +} +client = Client(options) +``` + +Or you want to limit the speed or turn on verbose mode: + +```python +options = { + ... + 'recv_speed' : 3000000, + 'send_speed' : 3000000, + 'verbose' : True +} +client = Client(options) +``` + +recv_speed: rate limit data download speed in Bytes per second. Defaults to unlimited speed. +send_speed: rate limit data upload speed in Bytes per second. Defaults to unlimited speed. +verbose: set verbose mode on/off. By default verbose mode is off. + +Also if your server does not support `check` it is possible to disable it: + +```python +options = { + ... + 'disable_check': True +} +client = Client(options) +``` + +By default checking of remote resources is enabled. + +**Synchronous methods** + +```python +# Checking existence of the resource + +client.check("dir1/file1") +client.check("dir1") +``` + +```python +# Get information about the resource + +client.info("dir1/file1") +client.info("dir1/") +``` + +```python +# Check free space + +free_size = client.free() +``` + +```python +# Get a list of resources + +files1 = client.list() +files2 = client.list("dir1") +``` + +```python +# Create directory + +client.mkdir("dir1/dir2") +``` + +```python +# Delete resource + +client.clean("dir1/dir2") +``` + +```python +# Copy resource + +client.copy(remote_path_from="dir1/file1", remote_path_to="dir2/file1") +client.copy(remote_path_from="dir2", remote_path_to="dir3") +``` + +```python +# Move resource + +client.move(remote_path_from="dir1/file1", remote_path_to="dir2/file1") +client.move(remote_path_from="dir2", remote_path_to="dir3") +``` + +```python +# Move resource + +client.download_sync(remote_path="dir1/file1", local_path="~/Downloads/file1") +client.download_sync(remote_path="dir1/dir2/", local_path="~/Downloads/dir2/") +``` + +```python +# Unload resource + +client.upload_sync(remote_path="dir1/file1", local_path="~/Documents/file1") +client.upload_sync(remote_path="dir1/dir2/", local_path="~/Documents/dir2/") +``` + +```python +# Publish the resource + +link = client.publish("dir1/file1") +link = client.publish("dir2") +``` + +```python +# Unpublish resource + +client.unpublish("dir1/file1") +client.unpublish("dir2") +``` + +```python +# Exception handling + +from webdav3.client import WebDavException +try: +... +except WebDavException as exception: +... +``` + +```python +# Get the missing files + +client.pull(remote_directory='dir1', local_directory='~/Documents/dir1') +``` + +```python +# Send missing files + +client.push(remote_directory='dir1', local_directory='~/Documents/dir1') +``` + +**Asynchronous methods** + +```python +# Load resource + +kwargs = { + 'remote_path': "dir1/file1", + 'local_path': "~/Downloads/file1", + 'callback': callback +} +client.download_async(**kwargs) + +kwargs = { + 'remote_path': "dir1/dir2/", + 'local_path': "~/Downloads/dir2/", + 'callback': callback +} +client.download_async(**kwargs) +``` + +```python +# Unload resource + +kwargs = { + 'remote_path': "dir1/file1", + 'local_path': "~/Downloads/file1", + 'callback': callback +} +client.upload_async(**kwargs) + +kwargs = { + 'remote_path': "dir1/dir2/", + 'local_path': "~/Downloads/dir2/", + 'callback': callback +} +client.upload_async(**kwargs) +``` + +Resource API +============ + +Resource API using the concept of OOP that enables cloud-level resources. + +```python +# Get a resource + +res1 = client.resource("dir1/file1") +``` + +```python +# Work with the resource + +res1.rename("file2") +res1.move("dir1/file2") +res1.copy("dir2/file1") +info = res1.info() +res1.read_from(buffer) +res1.read(local_path="~/Documents/file1") +res1.read_async(local_path="~/Documents/file1", callback) +res1.write_to(buffer) +res1.write(local_path="~/Downloads/file1") +res1.write_async(local_path="~/Downloads/file1", callback) ``` Release Notes From 0b9c61e7e7f79c3b6c8cb1c53bad165f58f673f1 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Thu, 28 Nov 2019 14:50:39 +0300 Subject: [PATCH 40/63] Fixed SonarQube analysis issues --- webdav3/client.py | 15 ++++++--------- webdav3/connection.py | 7 ++++--- webdav3/exceptions.py | 4 ++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/webdav3/client.py b/webdav3/client.py index 8d138b8..0127f24 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -2,14 +2,15 @@ import functools import logging -import lxml.etree as etree import os -import requests import shutil import threading from io import BytesIO from re import sub +import lxml.etree as etree +import requests + from webdav3.connection import * from webdav3.exceptions import * from webdav3.urn import Urn @@ -174,7 +175,7 @@ class Client(object): if response.status_code == 404: raise RemoteResourceNotFound(path=path) if response.status_code == 405: - raise MethodNotSupported(name=action, server=hostname) + raise MethodNotSupported(name=action, server=self.webdav.hostname) if response.status_code >= 400: raise ResponseErrorCode(url=self.get_url(path), code=response.status_code, message=response.content) return response @@ -243,9 +244,8 @@ class Client(object): :return: list of nested file or directory names. """ 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()) + if directory_urn.path() != Client.root and not self.check(directory_urn.path()): + raise RemoteResourceNotFound(directory_urn.path()) response = self.execute_request(action='list', path=directory_urn.quote()) urns = WebDavXmlUtils.parse_get_list_response(response.content) @@ -817,9 +817,6 @@ class Resource(object): 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. diff --git a/webdav3/connection.py b/webdav3/connection.py index f24593c..dee4812 100644 --- a/webdav3/connection.py +++ b/webdav3/connection.py @@ -6,10 +6,13 @@ from webdav3.urn import Urn class ConnectionSettings: def is_valid(self): + """ + Method checks is settings are valid + :return: True if settings are valid otherwise False + """ pass def valid(self): - try: self.is_valid() except OptionNotValid: @@ -37,7 +40,6 @@ class WebDAVSettings(ConnectionSettings): disable_check = False def __init__(self, options): - self.options = dict() for key in self.keys: @@ -49,7 +51,6 @@ class WebDAVSettings(ConnectionSettings): self.root = self.root.rstrip(Urn.separate) def is_valid(self): - if not self.hostname: raise OptionNotValid(name="hostname", value=self.hostname, ns=self.ns) diff --git a/webdav3/exceptions.py b/webdav3/exceptions.py index 096c1f3..718cc7f 100644 --- a/webdav3/exceptions.py +++ b/webdav3/exceptions.py @@ -96,7 +96,7 @@ class ResponseErrorCode(WebDavException): class NotEnoughSpace(WebDavException): def __init__(self): - pass + self.message = "Not enough space on the server" def __str__(self): - return "Not enough space on the server" + return self.message From 5f84bb1343a64316a443164ecde6830ef3c22e3b Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Thu, 28 Nov 2019 23:16:35 +0300 Subject: [PATCH 41/63] Fixed #24 an issue with checking resources on Yandex WebDAV --- README.md | 3 ++ webdav3/client.py | 107 +++++++++++++++++++++++++--------------------- 2 files changed, 61 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 08df687..7bbc55a 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,9 @@ res1.write_async(local_path="~/Downloads/file1", callback) Release Notes ------------- +**Version 0.14 – TBD** + * Fixed an issue with checking resources on Yandex WebDAV server + **Version 0.13 – 27.11.2019** * Main version of Python is updated up to 3.7 * Switch to use python sessions rather than requests by https://github.com/delrey1 diff --git a/webdav3/client.py b/webdav3/client.py index 0127f24..eab1371 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -106,6 +106,59 @@ class Client(object): 'set_property': ["Accept: */*", "Depth: 1", "Content-Type: application/x-www-form-urlencoded"] } + # mapping of actions to WebDAV methods + requests = { + 'options': 'OPTIONS', + 'download': "GET", + 'upload': "PUT", + 'copy': "COPY", + 'move': "MOVE", + 'mkdir': "MKCOL", + 'clean': "DELETE", + 'check': "HEAD", + 'list': "PROPFIND", + 'free': "PROPFIND", + 'info': "PROPFIND", + 'publish': "PROPPATCH", + 'unpublish': "PROPPATCH", + 'published': "PROPPATCH", + 'get_property': "PROPFIND", + 'set_property': "PROPPATCH" + } + + meta_xmlns = { + 'https://webdav.yandex.ru': "urn:yandex:disk:meta", + } + + def __init__(self, options): + """Constructor of WebDAV client + + :param options: the dictionary of connection options to WebDAV. + WebDev settings: + `webdav_hostname`: url for WebDAV server should contain protocol and ip address or domain name. + Example: `https://webdav.server.com`. + `webdav_login`: (optional) login name for WebDAV server can be empty in case using of token auth. + `webdav_password`: (optional) password for WebDAV server can be empty in case using of token auth. + `webdav_token': (optional) token for WebDAV server can be empty in case using of login/password auth. + `webdav_root`: (optional) root directory of WebDAV server. Defaults is `/`. + `webdav_cert_path`: (optional) path to certificate. + `webdav_key_path`: (optional) path to private key. + `webdav_recv_speed`: (optional) rate limit data download speed in Bytes per second. + Defaults to unlimited speed. + `webdav_send_speed`: (optional) rate limit data upload speed in Bytes per second. + Defaults to unlimited speed. + `webdav_verbose`: (optional) set verbose mode on.off. By default verbose mode is off. + + """ + webdav_options = get_options(option_type=WebDAVSettings, from_options=options) + + self.webdav = WebDAVSettings(webdav_options) + response = self.execute_request('options', '') + self.supported_methods = response.headers.get('Allow') + if 'HEAD' not in self.supported_methods: + self.requests['check'] = 'GET' + self.default_options = {} + def get_headers(self, action, headers_ext=None): """Returns HTTP headers of specified WebDAV actions. @@ -180,54 +233,6 @@ class Client(object): raise ResponseErrorCode(url=self.get_url(path), code=response.status_code, message=response.content) return response - # mapping of actions to WebDAV methods - requests = { - 'download': "GET", - 'upload': "PUT", - 'copy': "COPY", - 'move': "MOVE", - 'mkdir': "MKCOL", - 'clean': "DELETE", - 'check': "GET", - 'list': "PROPFIND", - 'free': "PROPFIND", - 'info': "PROPFIND", - 'publish': "PROPPATCH", - 'unpublish': "PROPPATCH", - 'published': "PROPPATCH", - 'get_property': "PROPFIND", - 'set_property': "PROPPATCH" - } - - meta_xmlns = { - 'https://webdav.yandex.ru': "urn:yandex:disk:meta", - } - - def __init__(self, options): - """Constructor of WebDAV client - - :param options: the dictionary of connection options to WebDAV. - WebDev settings: - `webdav_hostname`: url for WebDAV server should contain protocol and ip address or domain name. - Example: `https://webdav.server.com`. - `webdav_login`: (optional) login name for WebDAV server can be empty in case using of token auth. - `webdav_password`: (optional) password for WebDAV server can be empty in case using of token auth. - `webdav_token': (optional) token for WebDAV server can be empty in case using of login/password auth. - `webdav_root`: (optional) root directory of WebDAV server. Defaults is `/`. - `webdav_cert_path`: (optional) path to certificate. - `webdav_key_path`: (optional) path to private key. - `webdav_recv_speed`: (optional) rate limit data download speed in Bytes per second. - Defaults to unlimited speed. - `webdav_send_speed`: (optional) rate limit data upload speed in Bytes per second. - Defaults to unlimited speed. - `webdav_verbose`: (optional) set verbose mode on.off. By default verbose mode is off. - - """ - webdav_options = get_options(option_type=WebDAVSettings, from_options=options) - - self.webdav = WebDAVSettings(webdav_options) - self.default_options = {} - def valid(self): """Validates of WebDAV settings. @@ -300,7 +305,11 @@ class Client(object): if not self.check(directory_urn.parent()): raise RemoteParentNotFound(directory_urn.path()) - response = self.execute_request(action='mkdir', path=directory_urn.quote()) + try: + response = self.execute_request(action='mkdir', path=directory_urn.quote()) + except MethodNotSupported: + # Yandex WebDAV returns 405 status code when directory already exists + return True return response.status_code in (200, 201) @wrap_connection_error From 5aad617fbf206d7e6a8107ddd4caef071c6c2058 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Fri, 29 Nov 2019 13:24:30 +0300 Subject: [PATCH 42/63] Update tests and fix issues --- tests/test_client_it.py | 27 ++++++++++++++++++++------- tests/test_client_unit.py | 1 + 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/test_client_it.py b/tests/test_client_it.py index f8c5856..2d45316 100644 --- a/tests/test_client_it.py +++ b/tests/test_client_it.py @@ -3,6 +3,7 @@ import shutil import unittest from io import BytesIO, StringIO from os import path +from time import sleep from unittest import TestCase from webdav3.client import Client @@ -21,13 +22,20 @@ class ClientTestCase(TestCase): local_file_path = local_base_dir + 'test.txt' local_path_dir = local_base_dir + 'res/test_dir' + options = { + 'webdav_hostname': 'http://localhost:8585', + 'webdav_login': 'alice', + 'webdav_password': 'secret1234' + } + + # options = { + # 'webdav_hostname': 'https://webdav.yandex.ru', + # 'webdav_login': 'webdavclient.test2', + # 'webdav_password': 'Qwerty123!' + # } + def setUp(self): - options = { - 'webdav_hostname': 'http://localhost:8585', - 'webdav_login': 'alice', - 'webdav_password': 'secret1234' - } - self.client = Client(options) + self.client = Client(self.options) if path.exists(path=self.local_path_dir): shutil.rmtree(path=self.local_path_dir) @@ -46,7 +54,10 @@ class ClientTestCase(TestCase): self.assertGreater(file_list.__len__(), 0, 'Expected that amount of files more then 0') def test_free(self): - with self.assertRaises(MethodNotSupported): + if 'localhost' in self.options['webdav_hostname']: + with self.assertRaises(MethodNotSupported): + self.client.free() + else: self.assertGreater(self.client.free(), 0, 'Expected that free space on WebDAV server is more then 0 bytes') def test_check(self): @@ -101,6 +112,8 @@ class ClientTestCase(TestCase): remote_path=self.remote_path_file, callback=callback) self.assertFalse(path.exists(self.local_path_dir + os.path.sep + self.local_file), 'Expected the file has not been downloaded yet') + # It needs for ending download before environment will be cleaned in tearDown + sleep(0.4) def test_upload_from(self): self._prepare_for_uploading() diff --git a/tests/test_client_unit.py b/tests/test_client_unit.py index be544d1..bfa66b9 100644 --- a/tests/test_client_unit.py +++ b/tests/test_client_unit.py @@ -135,6 +135,7 @@ class ClientTestCase(TestCase): except: f = open('./tests/response_dir.xml') content = f.read().decode('utf-8').encode('utf-8') + f.close() path = '/test_dir' hostname = 'https://webdav.yandex.ru' result = utils.parse_is_dir_response(content, path, hostname) From 5e2a9468c15a7a13ec2fe084fdd3c892f807d649 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Tue, 17 Dec 2019 12:20:29 +0300 Subject: [PATCH 43/63] Increase test coverage --- tests/__init__.py | 0 tests/base_client_it.py | 69 ++++++++++++++++++++++++ tests/test_client_it.py | 84 ++++++++--------------------- tests/test_client_resource_it.py | 75 ++++++++++++++++++++++++++ tests/test_client_unit.py | 7 ++- tests/test_connection.py | 91 ++++++++++++++++++++++++++++++++ tests/test_exceptions.py | 50 ++++++++++++++++++ webdav3/client.py | 17 +++--- webdav3/connection.py | 1 + webdav3/exceptions.py | 2 +- 10 files changed, 327 insertions(+), 69 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/base_client_it.py create mode 100644 tests/test_client_resource_it.py create mode 100644 tests/test_connection.py create mode 100644 tests/test_exceptions.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/base_client_it.py b/tests/base_client_it.py new file mode 100644 index 0000000..b8bce2f --- /dev/null +++ b/tests/base_client_it.py @@ -0,0 +1,69 @@ +import os +import shutil +import unittest +from os import path + +from webdav3.client import Client + + +class BaseClientTestCase(unittest.TestCase): + remote_path_file = 'test_dir/test.txt' + remote_path_file2 = 'test_dir2/test.txt' + remote_inner_path_file = 'test_dir/inner/test.txt' + remote_path_dir = 'test_dir' + remote_path_dir2 = 'test_dir2' + remote_inner_path_dir = 'test_dir/inner' + local_base_dir = 'tests/' + local_file = 'test.txt' + local_file_path = local_base_dir + 'test.txt' + local_path_dir = local_base_dir + 'res/test_dir' + + options = { + 'webdav_hostname': 'http://localhost:8585', + 'webdav_login': 'alice', + 'webdav_password': 'secret1234' + } + + # options = { + # 'webdav_hostname': 'https://webdav.yandex.ru', + # 'webdav_login': 'webdavclient.test2', + # 'webdav_password': 'Qwerty123!' + # } + + def setUp(self): + self.client = Client(self.options) + if path.exists(path=self.local_path_dir): + shutil.rmtree(path=self.local_path_dir) + + def tearDown(self): + if path.exists(path=self.local_path_dir): + shutil.rmtree(path=self.local_path_dir) + if self.client.check(remote_path=self.remote_path_dir): + self.client.clean(remote_path=self.remote_path_dir) + if self.client.check(remote_path=self.remote_path_dir2): + self.client.clean(remote_path=self.remote_path_dir2) + + def _prepare_for_downloading(self, inner_dir=False): + if not self.client.check(remote_path=self.remote_path_dir): + self.client.mkdir(remote_path=self.remote_path_dir) + if not self.client.check(remote_path=self.remote_path_file): + self.client.upload_file(remote_path=self.remote_path_file, local_path=self.local_file_path) + if not path.exists(self.local_path_dir): + os.makedirs(self.local_path_dir) + if inner_dir: + if not self.client.check(remote_path=self.remote_inner_path_dir): + self.client.mkdir(remote_path=self.remote_inner_path_dir) + if not self.client.check(remote_path=self.remote_inner_path_file): + self.client.upload_file(remote_path=self.remote_inner_path_file, local_path=self.local_file_path) + + def _prepare_for_uploading(self): + if not self.client.check(remote_path=self.remote_path_dir): + self.client.mkdir(remote_path=self.remote_path_dir) + if not path.exists(path=self.local_path_dir): + os.makedirs(self.local_path_dir) + if not path.exists(path=self.local_path_dir + os.sep + self.local_file): + shutil.copy(src=self.local_file_path, dst=self.local_path_dir + os.sep + self.local_file) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_client_it.py b/tests/test_client_it.py index 2d45316..900df58 100644 --- a/tests/test_client_it.py +++ b/tests/test_client_it.py @@ -1,51 +1,14 @@ import os.path -import shutil import unittest from io import BytesIO, StringIO from os import path from time import sleep -from unittest import TestCase -from webdav3.client import Client -from webdav3.exceptions import MethodNotSupported +from tests.base_client_it import BaseClientTestCase +from webdav3.exceptions import MethodNotSupported, OptionNotValid, RemoteResourceNotFound -class ClientTestCase(TestCase): - remote_path_file = 'test_dir/test.txt' - remote_path_file2 = 'test_dir2/test.txt' - remote_inner_path_file = 'test_dir/inner/test.txt' - remote_path_dir = 'test_dir' - remote_path_dir2 = 'test_dir2' - remote_inner_path_dir = 'test_dir/inner' - local_base_dir = 'tests/' - local_file = 'test.txt' - local_file_path = local_base_dir + 'test.txt' - local_path_dir = local_base_dir + 'res/test_dir' - - options = { - 'webdav_hostname': 'http://localhost:8585', - 'webdav_login': 'alice', - 'webdav_password': 'secret1234' - } - - # options = { - # 'webdav_hostname': 'https://webdav.yandex.ru', - # 'webdav_login': 'webdavclient.test2', - # 'webdav_password': 'Qwerty123!' - # } - - def setUp(self): - self.client = Client(self.options) - if path.exists(path=self.local_path_dir): - shutil.rmtree(path=self.local_path_dir) - - def tearDown(self): - if path.exists(path=self.local_path_dir): - shutil.rmtree(path=self.local_path_dir) - if self.client.check(remote_path=self.remote_path_dir): - self.client.clean(remote_path=self.remote_path_dir) - if self.client.check(remote_path=self.remote_path_dir2): - self.client.clean(remote_path=self.remote_path_dir2) +class ClientTestCase(BaseClientTestCase): def test_list(self): self._prepare_for_downloading() @@ -69,12 +32,29 @@ class ClientTestCase(TestCase): self.client.mkdir(remote_path=self.remote_path_dir) self.assertTrue(self.client.check(remote_path=self.remote_path_dir), 'Expected the directory is created.') - def test_download_to(self): + def test_download_from(self): self._prepare_for_downloading() buff = BytesIO() self.client.download_from(buff=buff, remote_path=self.remote_path_file) self.assertEqual(buff.getvalue(), b'test content for testing of webdav client') + def test_download_from_dir(self): + self._prepare_for_downloading() + buff = BytesIO() + with self.assertRaises(OptionNotValid): + self.client.download_from(buff=buff, remote_path=self.remote_path_dir) + + def test_download_from_wrong_file(self): + self._prepare_for_downloading() + buff = BytesIO() + with self.assertRaises(RemoteResourceNotFound): + self.client.download_from(buff=buff, remote_path='wrong') + + def test_download_directory_wrong(self): + self._prepare_for_downloading() + with self.assertRaises(RemoteResourceNotFound): + self.client.download_directory(remote_path=self.remote_path_file, local_path=self.local_path_dir) + def test_download(self): self._prepare_for_downloading() self.client.download(local_path=self.local_path_dir, remote_path=self.remote_path_dir) @@ -241,26 +221,8 @@ class ClientTestCase(TestCase): self.assertTrue(self.client.check(self.remote_path_dir), 'Expected the directory is created.') self.assertTrue(self.client.check(self.remote_path_file), 'Expected the file is uploaded.') - def _prepare_for_downloading(self, inner_dir=False): - if not self.client.check(remote_path=self.remote_path_dir): - self.client.mkdir(remote_path=self.remote_path_dir) - if not self.client.check(remote_path=self.remote_path_file): - self.client.upload_file(remote_path=self.remote_path_file, local_path=self.local_file_path) - if not path.exists(self.local_path_dir): - os.makedirs(self.local_path_dir) - if inner_dir: - if not self.client.check(remote_path=self.remote_inner_path_dir): - self.client.mkdir(remote_path=self.remote_inner_path_dir) - if not self.client.check(remote_path=self.remote_inner_path_file): - self.client.upload_file(remote_path=self.remote_inner_path_file, local_path=self.local_file_path) - - def _prepare_for_uploading(self): - if not self.client.check(remote_path=self.remote_path_dir): - self.client.mkdir(remote_path=self.remote_path_dir) - if not path.exists(path=self.local_path_dir): - os.makedirs(self.local_path_dir) - if not path.exists(path=self.local_path_dir + os.sep + self.local_file): - shutil.copy(src=self.local_file_path, dst=self.local_path_dir + os.sep + self.local_file) + def test_valid(self): + self.assertTrue(self.client.valid()) if __name__ == '__main__': diff --git a/tests/test_client_resource_it.py b/tests/test_client_resource_it.py new file mode 100644 index 0000000..4f69022 --- /dev/null +++ b/tests/test_client_resource_it.py @@ -0,0 +1,75 @@ +import unittest + +from tests.base_client_it import BaseClientTestCase +from webdav3.client import Resource +from webdav3.urn import Urn + + +class ResourceTestCase(BaseClientTestCase): + + def test_str(self): + self._prepare_for_downloading() + resource = Resource(self.client, Urn(self.remote_path_file)) + self.assertEqual('resource /test_dir/test.txt', resource.__str__()) + + def test_is_not_dir(self): + self._prepare_for_downloading() + resource = Resource(self.client, Urn(self.remote_path_file)) + self.assertFalse(resource.is_dir()) + + def test_is_dir(self): + self._prepare_for_downloading() + resource = Resource(self.client, Urn(self.remote_path_dir)) + self.assertTrue(resource.is_dir()) + + def test_rename(self): + self._prepare_for_downloading() + resource = Resource(self.client, Urn(self.remote_path_file)) + resource.rename('new_name.text') + self.assertTrue(self.client.check(self.remote_path_dir + '/new_name.text')) + + def test_move(self): + self._prepare_for_downloading() + resource = Resource(self.client, Urn(self.remote_path_file)) + self.client.mkdir(self.remote_path_dir2) + resource.move(self.remote_path_file2) + self.assertFalse(self.client.check(self.remote_path_file)) + self.assertTrue(self.client.check(self.remote_path_file2)) + + def test_copy(self): + self._prepare_for_downloading() + resource = Resource(self.client, Urn(self.remote_path_file)) + self.client.mkdir(self.remote_path_dir2) + resource.copy(self.remote_path_file2) + self.assertTrue(self.client.check(self.remote_path_file)) + self.assertTrue(self.client.check(self.remote_path_file2)) + + def test_info(self): + self._prepare_for_downloading() + resource = Resource(self.client, Urn(self.remote_path_file)) + info = resource.info() + self.assertIsNotNone(info) + self.assertGreater(len(info), 0) + + def test_info_params(self): + self._prepare_for_downloading() + resource = Resource(self.client, Urn(self.remote_path_file)) + info = resource.info(['size']) + self.assertIsNotNone(info) + self.assertEqual(1, len(info)) + self.assertTrue('size' in info) + + def test_clean(self): + self._prepare_for_downloading() + resource = Resource(self.client, Urn(self.remote_path_file)) + resource.clean() + self.assertFalse(self.client.check(self.remote_path_file)) + + def test_check(self): + self._prepare_for_downloading() + resource = Resource(self.client, Urn(self.remote_path_file)) + self.assertTrue(resource.check()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_client_unit.py b/tests/test_client_unit.py index bfa66b9..538c415 100644 --- a/tests/test_client_unit.py +++ b/tests/test_client_unit.py @@ -4,7 +4,7 @@ from unittest import TestCase from lxml.etree import ElementTree, Element -from webdav3.client import WebDavXmlUtils as utils +from webdav3.client import WebDavXmlUtils as utils, listdir class ClientTestCase(TestCase): @@ -156,6 +156,11 @@ class ClientTestCase(TestCase): result = utils.parse_is_dir_response(content.encode('utf-8'), path, hostname) self.assertFalse(result, 'It should be file') + def test_listdir_inner_dir(self): + file_names = listdir('.') + self.assertGreater(len(file_names), 0) + self.assertTrue('webdav3/' in file_names) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_connection.py b/tests/test_connection.py new file mode 100644 index 0000000..dff3772 --- /dev/null +++ b/tests/test_connection.py @@ -0,0 +1,91 @@ +import unittest + +from webdav3.client import get_options +from webdav3.connection import WebDAVSettings, OptionNotValid, ConnectionSettings + + +class ConnectionTestCase(unittest.TestCase): + def test_connection_settings_valid(self): + options = { + 'webdav_hostname': 'http://localhost:8585', + 'webdav_login': 'alice', + 'webdav_password': 'secret1234' + } + webdav_options = get_options(option_type=WebDAVSettings, from_options=options) + settings = WebDAVSettings(webdav_options) + self.assertTrue(settings.is_valid()) + self.assertTrue(settings.valid()) + + def test_connection_settings_no_hostname(self): + options = { + 'webdav_login': 'alice', + 'webdav_password': 'secret1234' + } + webdav_options = get_options(option_type=WebDAVSettings, from_options=options) + settings = WebDAVSettings(webdav_options) + self.assertRaises(OptionNotValid, settings.is_valid) + self.assertFalse(settings.valid()) + + def test_connection_settings_no_login(self): + options = { + 'webdav_hostname': 'http://localhost:8585', + 'webdav_password': 'secret1234' + } + webdav_options = get_options(option_type=WebDAVSettings, from_options=options) + settings = WebDAVSettings(webdav_options) + self.assertRaises(OptionNotValid, settings.is_valid) + self.assertFalse(settings.valid()) + + def test_connection_settings_no_token_and_no_login(self): + options = { + 'webdav_hostname': 'http://localhost:8585' + } + webdav_options = get_options(option_type=WebDAVSettings, from_options=options) + settings = WebDAVSettings(webdav_options) + self.assertRaises(OptionNotValid, settings.is_valid) + self.assertFalse(settings.valid()) + + def test_connection_settings_wrong_cert_path(self): + options = { + 'webdav_hostname': 'http://localhost:8585', + 'webdav_login': 'alice', + 'cert_path': './wrong.file', + 'webdav_password': 'secret1234' + } + webdav_options = get_options(option_type=WebDAVSettings, from_options=options) + settings = WebDAVSettings(webdav_options) + self.assertRaises(OptionNotValid, settings.is_valid) + self.assertFalse(settings.valid()) + + def test_connection_settings_wrong_key_path(self): + options = { + 'webdav_hostname': 'http://localhost:8585', + 'webdav_login': 'alice', + 'key_path': './wrong.file', + 'webdav_password': 'secret1234' + } + webdav_options = get_options(option_type=WebDAVSettings, from_options=options) + settings = WebDAVSettings(webdav_options) + self.assertRaises(OptionNotValid, settings.is_valid) + self.assertFalse(settings.valid()) + + def test_connection_settings_with_key_path_an_no_cert_path(self): + options = { + 'webdav_hostname': 'http://localhost:8585', + 'webdav_login': 'alice', + 'key_path': './publish.sh', + 'webdav_password': 'secret1234' + } + webdav_options = get_options(option_type=WebDAVSettings, from_options=options) + settings = WebDAVSettings(webdav_options) + self.assertRaises(OptionNotValid, settings.is_valid) + self.assertFalse(settings.valid()) + + def test_connection_settings_does_nothing(self): + settings = ConnectionSettings() + settings.is_valid() + self.assertTrue(settings.valid()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 0000000..d25d313 --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,50 @@ +import unittest + +from webdav3.exceptions import OptionNotValid, LocalResourceNotFound, RemoteResourceNotFound, MethodNotSupported, ConnectionException, NoConnection, \ + RemoteParentNotFound, NotConnection, ResponseErrorCode, NotEnoughSpace + + +class ExceptionsTestCase(unittest.TestCase): + def test_option_not_valid(self): + exception = OptionNotValid('Name', 'Value', 'Namespace/') + self.assertEqual("Option (Namespace/Name=Value) have invalid name or value", exception.__str__()) + + def test_local_resource_not_found(self): + exception = LocalResourceNotFound('Path') + self.assertEqual("Local file: Path not found", exception.__str__()) + + def test_remote_resource_not_found(self): + exception = RemoteResourceNotFound('Path') + self.assertEqual("Remote resource: Path not found", exception.__str__()) + + def test_remote_parent_not_found(self): + exception = RemoteParentNotFound('Path') + self.assertEqual("Remote parent for: Path not found", exception.__str__()) + + def test_method_not_supported(self): + exception = MethodNotSupported('HEAD', 'Server') + self.assertEqual("Method 'HEAD' not supported for Server", exception.__str__()) + + def test_connection_exception(self): + exception = ConnectionException(MethodNotSupported('HEAD', 'Server')) + self.assertEqual("Method 'HEAD' not supported for Server", exception.__str__()) + + def test_no_connection(self): + exception = NoConnection('Server') + self.assertEqual("No connection with Server", exception.__str__()) + + def test_not_connection_legacy(self): + exception = NotConnection('Server') + self.assertEqual("No connection with Server", exception.__str__()) + + def test_response_error_code(self): + exception = ResponseErrorCode('http://text/', 502, 'Service Unavailable') + self.assertEqual("Request to http://text/ failed with code 502 and message: Service Unavailable", exception.__str__()) + + def test_not_enough_space(self): + exception = NotEnoughSpace() + self.assertEqual("Not enough space on the server", exception.__str__()) + + +if __name__ == '__main__': + unittest.main() diff --git a/webdav3/client.py b/webdav3/client.py index eab1371..766d0b8 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -277,9 +277,9 @@ class Client(object): :param remote_path: (optional) path to resource on WebDAV server. Defaults is root directory of WebDAV. :return: True if resource is exist or False otherwise """ - if self.webdav.disable_check: return True + urn = Urn(remote_path) try: response = self.execute_request(action='check', path=urn.quote()) @@ -466,7 +466,7 @@ class Client(object): self.mkdir(remote_path) for resource_name in listdir(local_path): - _remote_path = "{parent}{name}".format(parent=urn.path(), name=resource_name) + _remote_path = "{parent}{name}".format(parent=urn.path(), name=resource_name).replace('\\', '') _local_path = os.path.join(local_path, resource_name) self.upload(local_path=_local_path, remote_path=_remote_path, progress=progress) @@ -588,13 +588,16 @@ class Client(object): `modified`: date of resource modification. """ urn = Urn(remote_path) - if not self.check(urn.path()) and not self.check(Urn(remote_path, directory=True).path()): - raise RemoteResourceNotFound(remote_path) + self._check_remote_resource(remote_path, urn) response = self.execute_request(action='info', path=urn.quote()) path = self.get_full_path(urn) return WebDavXmlUtils.parse_info_response(content=response.content, path=path, hostname=self.webdav.hostname) + def _check_remote_resource(self, remote_path, urn): + if not self.check(urn.path()) and not self.check(Urn(remote_path, directory=True).path()): + raise RemoteResourceNotFound(remote_path) + @wrap_connection_error def is_dir(self, remote_path): """Checks is the remote resource directory. @@ -605,8 +608,7 @@ class Client(object): """ urn = Urn(remote_path) parent_urn = Urn(urn.parent()) - if not self.check(urn.path()) and not self.check(Urn(remote_path, directory=True).path()): - raise RemoteResourceNotFound(remote_path) + self._check_remote_resource(remote_path, urn) response = self.execute_request(action='info', path=parent_urn.quote()) path = self.get_full_path(urn) @@ -826,6 +828,9 @@ class Resource(object): 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. diff --git a/webdav3/connection.py b/webdav3/connection.py index dee4812..ce99211 100644 --- a/webdav3/connection.py +++ b/webdav3/connection.py @@ -68,3 +68,4 @@ class WebDAVSettings(ConnectionSettings): if not self.token and not self.login: raise OptionNotValid(name="login", value=self.login, ns=self.ns) + return True diff --git a/webdav3/exceptions.py b/webdav3/exceptions.py index 718cc7f..7ccf020 100644 --- a/webdav3/exceptions.py +++ b/webdav3/exceptions.py @@ -71,7 +71,7 @@ class NoConnection(WebDavException): self.hostname = hostname def __str__(self): - return "Not connection with {hostname}".format(hostname=self.hostname) + return "No connection with {hostname}".format(hostname=self.hostname) # This exception left only for supporting original library interface. From 7db1cfd27415383507f937c8936cf02ce2ee1100 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Tue, 17 Dec 2019 13:52:58 +0300 Subject: [PATCH 44/63] Bump version to 0.14 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d278cc7..c7dde81 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import setup, find_packages from setuptools.command.install import install as InstallCommand from setuptools.command.test import test as TestCommand -version = "0.13" +version = "0.14" requirements = "libxml2-dev libxslt-dev python-dev" From 95bea2182c5f29ecc75437b0f0b742cb816b47df Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Tue, 17 Dec 2019 13:57:29 +0300 Subject: [PATCH 45/63] Remove release date --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7bbc55a..3027daa 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ res1.write_async(local_path="~/Downloads/file1", callback) Release Notes ------------- -**Version 0.14 – TBD** +**Version 0.14** * Fixed an issue with checking resources on Yandex WebDAV server **Version 0.13 – 27.11.2019** From 336db8ae19182992e3ad62c4a5f2a70e3be0d57a Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Mon, 20 Jan 2020 17:31:25 -0800 Subject: [PATCH 46/63] Override methods for customizing communication with WebDAV servers. Fix #31 and #30 --- README.md | 23 +++++++++++++++++++++-- tests/base_client_it.py | 5 ++++- tests/test_client_it.py | 3 +++ webdav3/client.py | 5 +---- webdav3/connection.py | 3 ++- 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3027daa..53c148d 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,12 @@ client.execute_request("mkdir", 'directory_name') Webdav API ========== -Webdav API is a set of webdav methods of work with cloud storage. This set includes the following methods: +Webdav API is a set of webdav actions of work with cloud storage. This set includes the following actions: `check`, `free`, `info`, `list`, `mkdir`, `clean`, `copy`, `move`, `download`, `upload`, `publish` and `unpublish`. **Configuring the client** -Required keys for configuring client connection with WevDAV-server are webdav\_hostname and webdav\_login, webdav\_password. +Required keys for configuring client connection with WevDAV-server are `webdav_hostname` and `webdav_login`, `webdav_password`. ```python from webdav3.client import Client @@ -51,6 +51,25 @@ options = { client = Client(options) ``` +If your server does not support `HEAD` method or there are other reasons to override default WebDAV methods for actions use a dictionary option `webdav_override_methods`. +The key should be in the following list: `check`, `free`, `info`, `list`, `mkdir`, `clean`, `copy`, `move`, `download`, `upload`, + `publish` and `unpublish`. The value should a string name of WebDAV method, for example `GET`. + +```python +from webdav3.client import Client + +options = { + 'webdav_hostname': "https://webdav.server.ru", + 'webdav_login': "login", + 'webdav_password': "password", + 'webdav_override_methods': { + 'check': 'GET' + } + +} +client = Client(options) +``` + When a proxy server you need to specify settings to connect through it. ```python diff --git a/tests/base_client_it.py b/tests/base_client_it.py index b8bce2f..3b04cd4 100644 --- a/tests/base_client_it.py +++ b/tests/base_client_it.py @@ -21,7 +21,10 @@ class BaseClientTestCase(unittest.TestCase): options = { 'webdav_hostname': 'http://localhost:8585', 'webdav_login': 'alice', - 'webdav_password': 'secret1234' + 'webdav_password': 'secret1234', + 'webdav_override_methods': { + 'check': 'GET' + } } # options = { diff --git a/tests/test_client_it.py b/tests/test_client_it.py index 900df58..0998fe6 100644 --- a/tests/test_client_it.py +++ b/tests/test_client_it.py @@ -224,6 +224,9 @@ class ClientTestCase(BaseClientTestCase): def test_valid(self): self.assertTrue(self.client.valid()) + def test_check_is_overridden(self): + self.assertEqual('GET', self.client.requests['check']) + if __name__ == '__main__': unittest.main() diff --git a/webdav3/client.py b/webdav3/client.py index 766d0b8..0e85b08 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -153,10 +153,7 @@ class Client(object): webdav_options = get_options(option_type=WebDAVSettings, from_options=options) self.webdav = WebDAVSettings(webdav_options) - response = self.execute_request('options', '') - self.supported_methods = response.headers.get('Allow') - if 'HEAD' not in self.supported_methods: - self.requests['check'] = 'GET' + self.requests.update(self.webdav.override_methods) self.default_options = {} def get_headers(self, action, headers_ext=None): diff --git a/webdav3/connection.py b/webdav3/connection.py index ce99211..a122f86 100644 --- a/webdav3/connection.py +++ b/webdav3/connection.py @@ -25,7 +25,7 @@ class WebDAVSettings(ConnectionSettings): ns = "webdav:" prefix = "webdav_" keys = {'hostname', 'login', 'password', 'token', 'root', 'cert_path', 'key_path', 'recv_speed', 'send_speed', - 'verbose', 'disable_check'} + 'verbose', 'disable_check', 'override_methods'} hostname = None login = None @@ -38,6 +38,7 @@ class WebDAVSettings(ConnectionSettings): send_speed = None verbose = None disable_check = False + override_methods = {} def __init__(self, options): self.options = dict() From fc14ed2be1252e5c278d14e065552d85d25aec3a Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Mon, 20 Jan 2020 17:58:31 -0800 Subject: [PATCH 47/63] Update dependencies --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c7dde81..5d14233 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ setup( version=version, packages=find_packages(), requires=['python (>= 2.7.6)'], - install_requires=['requests', 'lxml', 'argcomplete'], + install_requires=['requests', 'lxml'], scripts=['wdc'], test_suite='tests', tests_require=['pytest'], From f3e7d4427669ed8dd8f940d95e8f83a5638406f1 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Sun, 26 Jan 2020 15:34:05 -0800 Subject: [PATCH 48/63] Support multiple clients simultaneously Fixed #34 --- tests/test_multi_client_it.py | 26 ++++++++++++++++++++++++++ webdav3/client.py | 18 +++++++++--------- webdav3/connection.py | 26 +++++++++++++------------- 3 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 tests/test_multi_client_it.py diff --git a/tests/test_multi_client_it.py b/tests/test_multi_client_it.py new file mode 100644 index 0000000..d8a1d15 --- /dev/null +++ b/tests/test_multi_client_it.py @@ -0,0 +1,26 @@ +import unittest + +from tests.test_client_it import ClientTestCase +from webdav3.client import Client + + +class MultiClientTestCase(ClientTestCase): + options2 = { + 'webdav_hostname': 'https://wrong.url.ru', + 'webdav_login': 'webdavclient.test2', + 'webdav_password': 'Qwerty123!', + 'webdav_override_methods': { + 'check': 'FAKE', + 'download': 'FAKE', + 'upload': 'FAKE', + 'clean': 'FAKE', + } + } + + def setUp(self): + super(ClientTestCase, self).setUp() + self.second_client = Client(self.options2) + + +if __name__ == '__main__': + unittest.main() diff --git a/webdav3/client.py b/webdav3/client.py index 0e85b08..8bfe688 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -89,11 +89,8 @@ class Client(object): # controls whether to verify the server's TLS certificate or not verify = True - # Sets the session for subsequent requests - session = requests.Session() - # HTTP headers for different actions - http_header = { + default_http_header = { 'list': ["Accept: */*", "Depth: 1"], 'free': ["Accept: */*", "Depth: 0", "Content-Type: text/xml"], 'copy': ["Accept: */*"], @@ -107,7 +104,7 @@ class Client(object): } # mapping of actions to WebDAV methods - requests = { + default_requests = { 'options': 'OPTIONS', 'download': "GET", 'upload': "PUT", @@ -150,6 +147,9 @@ class Client(object): `webdav_verbose`: (optional) set verbose mode on.off. By default verbose mode is off. """ + self.session = requests.Session() + self.http_header = Client.default_http_header.copy() + self.requests = Client.default_requests.copy() webdav_options = get_options(option_type=WebDAVSettings, from_options=options) self.webdav = WebDAVSettings(webdav_options) @@ -164,11 +164,11 @@ class Client(object): the specified action. :return: the dictionary of headers for specified action. """ - if action in Client.http_header: + if action in self.http_header: try: - headers = Client.http_header[action].copy() + headers = self.http_header[action].copy() except AttributeError: - headers = Client.http_header[action][:] + headers = self.http_header[action][:] else: headers = list() @@ -211,7 +211,7 @@ class Client(object): if self.session.auth: self.session.request(method="GET", url=self.webdav.hostname, verify=self.verify) # (Re)Authenticates against the proxy response = self.session.request( - method=Client.requests[action], + method=self.requests[action], url=self.get_url(path), auth=(self.webdav.login, self.webdav.password), headers=self.get_headers(action, headers_ext), diff --git a/webdav3/connection.py b/webdav3/connection.py index a122f86..302212b 100644 --- a/webdav3/connection.py +++ b/webdav3/connection.py @@ -27,20 +27,20 @@ class WebDAVSettings(ConnectionSettings): keys = {'hostname', 'login', 'password', 'token', 'root', 'cert_path', 'key_path', 'recv_speed', 'send_speed', 'verbose', 'disable_check', 'override_methods'} - hostname = None - login = None - password = None - token = None - root = None - cert_path = None - key_path = None - recv_speed = None - send_speed = None - verbose = None - disable_check = False - override_methods = {} - def __init__(self, options): + self.hostname = None + self.login = None + self.password = None + self.token = None + self.root = None + self.cert_path = None + self.key_path = None + self.recv_speed = None + self.send_speed = None + self.verbose = None + self.disable_check = False + self.override_methods = {} + self.options = dict() for key in self.keys: From c380dc56c71a14c94132357b0d9702bcb23b5ac0 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Sun, 26 Jan 2020 19:05:33 -0800 Subject: [PATCH 49/63] Update readme release notes --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 53c148d..71a963f 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,10 @@ res1.write_async(local_path="~/Downloads/file1", callback) Release Notes ------------- +**Version 3.14** + * Override methods for customizing communication with WebDAV servers + * Support multiple clients simultaneously + **Version 0.14** * Fixed an issue with checking resources on Yandex WebDAV server From 3d4751cdd03f682aa539b764f45beabaf5ffde33 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Sun, 26 Jan 2020 19:17:03 -0800 Subject: [PATCH 50/63] Support Python 3.8 --- .travis.yml | 6 +++++- setup.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0277e80..3e5e1e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ services: python: - "2.7" - "3.7" + - "3.8" before_install: - docker pull bytemark/webdav @@ -25,4 +26,7 @@ install: script: - coverage run setup.py test - coverage xml - - sonar-scanner + - | + if [[ $TRAVIS_PYTHON_VERSION == "3.8" ]]; then + sonar-scanner + fi diff --git a/setup.py b/setup.py index 5d14233..dc99d68 100644 --- a/setup.py +++ b/setup.py @@ -75,6 +75,7 @@ setup( 'Operating System :: Unix', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Topic :: Internet', 'Topic :: Software Development :: Libraries :: Python Modules', ], From b3c753c1269b7f1261e5dfa95815550a0ada5223 Mon Sep 17 00:00:00 2001 From: Ishak BELAHMAR Date: Sat, 11 Jan 2020 08:08:38 -0800 Subject: [PATCH 51/63] Handle 2-way sync when files are modified --- setup.py | 2 +- webdav3/client.py | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index dc99d68..8f7e190 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ setup( version=version, packages=find_packages(), requires=['python (>= 2.7.6)'], - install_requires=['requests', 'lxml'], + install_requires=['requests', 'lxml', 'python-dateutil'], scripts=['wdc'], test_suite='tests', tests_require=['pytest'], diff --git a/webdav3/client.py b/webdav3/client.py index 8bfe688..420f1af 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -10,6 +10,7 @@ from re import sub import lxml.etree as etree import requests +from dateutil import parser as dateutil_parser from webdav3.connection import * from webdav3.exceptions import * @@ -697,11 +698,12 @@ class Client(object): self.push(remote_directory=remote_path, local_directory=local_path) else: if local_resource_name in remote_resource_names: - continue + if self.is_local_more_recent(local_path, remote_path): + continue self.upload_file(remote_path=remote_path, local_path=local_path) - def pull(self, remote_directory, local_directory): + def pull(self, remote_directory, local_directory): def prune(src, exp): return [sub(exp, "", item) for item in src] @@ -736,13 +738,39 @@ class Client(object): self.pull(remote_directory=remote_path, local_directory=local_path) else: if remote_resource_name in local_resource_names: - continue + # Skip pull if local resource is more recent of we can't tell + if self.is_local_more_recent(local_path, remote_path) in (True, None): + continue + self.download_file(remote_path=remote_path, local_path=local_path) updated = True return updated - def sync(self, remote_directory, local_directory): + def is_local_more_recent(self, local_path, remote_path): + """Tells if local resource is more recent that the remote on if possible + :param local_path: the path to local resource. + :param remote_path: the path to remote resource. + + :return: True if local resource is more recent, False if the remote one is + None if comparison is not possible + """ + try: + remote_info = self.info(remote_path) + # Try to compare modification dates if our server + # offers that information + remote_last_mod_date = remote_info['modified'] + remote_last_mod_date = dateutil_parser.parse(remote_last_mod_date) + remote_last_mod_date_unix_ts = int(remote_last_mod_date.strftime("%s")) + local_last_mod_date_unix_ts = os.stat(local_path).st_mtime + + return (local_last_mod_date_unix_ts > remote_last_mod_date_unix_ts) + except: + # If there is problem when parsing dates, or cannot get + # last modified information, return None + return None + + def sync(self, remote_directory, local_directory): self.pull(remote_directory=remote_directory, local_directory=local_directory) self.push(remote_directory=remote_directory, local_directory=local_directory) From 85b10d53c926c7e4a0ae053b821bf9345643515a Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Sun, 26 Jan 2020 20:03:58 -0800 Subject: [PATCH 52/63] Update release notes --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 71a963f..8db0bda 100644 --- a/README.md +++ b/README.md @@ -301,6 +301,7 @@ Release Notes **Version 3.14** * Override methods for customizing communication with WebDAV servers * Support multiple clients simultaneously + * Sync modified files during pull and push by https://github.com/mont5piques **Version 0.14** * Fixed an issue with checking resources on Yandex WebDAV server From 8c7ee2e372a6a4be787f4278d8561722e89d9e2c Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Sun, 26 Jan 2020 21:24:26 -0800 Subject: [PATCH 53/63] Fix issues and tests --- tests/base_client_it.py | 44 +++++++++++++++---------- tests/test_client_it.py | 51 +++++++++++++++++++++++++++++ webdav3/client.py | 72 +++++++++++++++++++---------------------- 3 files changed, 111 insertions(+), 56 deletions(-) diff --git a/tests/base_client_it.py b/tests/base_client_it.py index 3b04cd4..9fa988c 100644 --- a/tests/base_client_it.py +++ b/tests/base_client_it.py @@ -35,29 +35,39 @@ class BaseClientTestCase(unittest.TestCase): def setUp(self): self.client = Client(self.options) - if path.exists(path=self.local_path_dir): - shutil.rmtree(path=self.local_path_dir) + self.clean_local_dir(self.local_path_dir) def tearDown(self): - if path.exists(path=self.local_path_dir): - shutil.rmtree(path=self.local_path_dir) - if self.client.check(remote_path=self.remote_path_dir): - self.client.clean(remote_path=self.remote_path_dir) - if self.client.check(remote_path=self.remote_path_dir2): - self.client.clean(remote_path=self.remote_path_dir2) + self.clean_local_dir(self.local_path_dir) + self.clean_remote_dir(self.remote_path_dir) + self.clean_remote_dir(self.remote_path_dir2) - def _prepare_for_downloading(self, inner_dir=False): - if not self.client.check(remote_path=self.remote_path_dir): - self.client.mkdir(remote_path=self.remote_path_dir) - if not self.client.check(remote_path=self.remote_path_file): - self.client.upload_file(remote_path=self.remote_path_file, local_path=self.local_file_path) + def clean_remote_dir(self, remote_path_dir): + if self.client.check(remote_path=remote_path_dir): + self.client.clean(remote_path=remote_path_dir) + + @staticmethod + def clean_local_dir(local_path_dir): + if path.exists(path=local_path_dir): + shutil.rmtree(path=local_path_dir) + + def _prepare_for_downloading(self, inner_dir=False, base_path=''): + if base_path: + self._create_remote_dir_if_needed(base_path) + self._prepare_dir_for_downloading(base_path + self.remote_path_dir, base_path + self.remote_path_file, self.local_file_path) if not path.exists(self.local_path_dir): os.makedirs(self.local_path_dir) if inner_dir: - if not self.client.check(remote_path=self.remote_inner_path_dir): - self.client.mkdir(remote_path=self.remote_inner_path_dir) - if not self.client.check(remote_path=self.remote_inner_path_file): - self.client.upload_file(remote_path=self.remote_inner_path_file, local_path=self.local_file_path) + self._prepare_dir_for_downloading(base_path + self.remote_inner_path_dir, base_path + self.remote_inner_path_file, self.local_file_path) + + def _prepare_dir_for_downloading(self, remote_path_dir, remote_path_file, local_file_path): + self._create_remote_dir_if_needed(remote_path_dir) + if not self.client.check(remote_path=remote_path_file): + self.client.upload_file(remote_path=remote_path_file, local_path=local_file_path) + + def _create_remote_dir_if_needed(self, remote_dir): + if not self.client.check(remote_path=remote_dir): + self.client.mkdir(remote_path=remote_dir) def _prepare_for_uploading(self): if not self.client.check(remote_path=self.remote_path_dir): diff --git a/tests/test_client_it.py b/tests/test_client_it.py index 0998fe6..2d6c14f 100644 --- a/tests/test_client_it.py +++ b/tests/test_client_it.py @@ -1,4 +1,5 @@ import os.path +import shutil import unittest from io import BytesIO, StringIO from os import path @@ -9,6 +10,7 @@ from webdav3.exceptions import MethodNotSupported, OptionNotValid, RemoteResourc class ClientTestCase(BaseClientTestCase): + pulled_file = BaseClientTestCase.local_path_dir + os.sep + BaseClientTestCase.local_file def test_list(self): self._prepare_for_downloading() @@ -227,6 +229,55 @@ class ClientTestCase(BaseClientTestCase): def test_check_is_overridden(self): self.assertEqual('GET', self.client.requests['check']) + def test_pull_newer(self): + init_modification_time = int(self._prepare_local_test_file_and_get_modification_time()) + sleep(1) + self._prepare_for_downloading(base_path='time/') + result = self.client.pull('time/' + self.remote_path_dir, self.local_path_dir) + update_modification_time = int(os.path.getmtime(self.pulled_file)) + self.assertTrue(result) + self.assertGreater(update_modification_time, init_modification_time) + self.client.clean(remote_path='time/' + self.remote_path_dir) + + def test_pull_older(self): + self._prepare_for_downloading(base_path='time/') + sleep(1) + init_modification_time = int(self._prepare_local_test_file_and_get_modification_time()) + result = self.client.pull('time/' + self.remote_path_dir, self.local_path_dir) + update_modification_time = int(os.path.getmtime(self.pulled_file)) + self.assertFalse(result) + self.assertEqual(update_modification_time, init_modification_time) + self.client.clean(remote_path='time/' + self.remote_path_dir) + + def test_push_newer(self): + self._prepare_for_downloading(base_path='time/') + sleep(1) + self._prepare_for_uploading() + init_modification_time = self.client.info('time/' + self.remote_path_file)['modified'] + result = self.client.push('time/' + self.remote_path_dir, self.local_path_dir) + update_modification_time = self.client.info('time/' + self.remote_path_file)['modified'] + self.assertTrue(result) + self.assertNotEqual(init_modification_time, update_modification_time) + self.client.clean(remote_path='time/' + self.remote_path_dir) + + def test_push_older(self): + self._prepare_for_uploading() + sleep(1) + self._prepare_for_downloading(base_path='time/') + init_modification_time = self.client.info('time/' + self.remote_path_file)['modified'] + result = self.client.push('time/' + self.remote_path_dir, self.local_path_dir) + update_modification_time = self.client.info('time/' + self.remote_path_file)['modified'] + self.assertFalse(result) + self.assertEqual(init_modification_time, update_modification_time) + self.client.clean(remote_path='time/' + self.remote_path_dir) + + def _prepare_local_test_file_and_get_modification_time(self): + if not path.exists(path=self.local_path_dir): + os.mkdir(self.local_path_dir) + if not path.exists(path=self.local_path_dir + os.sep + self.local_file): + shutil.copy(src=self.local_file_path, dst=self.pulled_file) + return os.path.getmtime(self.pulled_file) + if __name__ == '__main__': unittest.main() diff --git a/webdav3/client.py b/webdav3/client.py index 420f1af..a5da364 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -671,51 +671,39 @@ class Client(object): def prune(src, exp): return [sub(exp, "", item) for item in src] + updated = False urn = Urn(remote_directory, directory=True) - - if not self.is_dir(urn.path()): - raise OptionNotValid(name="remote_path", value=remote_directory) - - if not os.path.isdir(local_directory): - raise OptionNotValid(name="local_path", value=local_directory) - - if not os.path.exists(local_directory): - raise LocalResourceNotFound(local_directory) + self._validate_remote_directory(urn) + self._validate_local_directory(local_directory) paths = self.list(urn.path()) expression = "{begin}{end}".format(begin="^", end=urn.path()) remote_resource_names = prune(paths, expression) for local_resource_name in listdir(local_directory): - local_path = os.path.join(local_directory, local_resource_name) - remote_path = "{remote_directory}{resource_name}".format(remote_directory=urn.path(), - resource_name=local_resource_name) + remote_path = "{remote_directory}{resource_name}".format(remote_directory=urn.path(), resource_name=local_resource_name) if os.path.isdir(local_path): if not self.check(remote_path=remote_path): self.mkdir(remote_path=remote_path) - self.push(remote_directory=remote_path, local_directory=local_path) + result = self.push(remote_directory=remote_path, local_directory=local_path) + updated = updated or result else: - if local_resource_name in remote_resource_names: - if self.is_local_more_recent(local_path, remote_path): - continue + if local_resource_name in remote_resource_names and not self.is_local_more_recent(local_path, remote_path): + continue self.upload_file(remote_path=remote_path, local_path=local_path) - + updated = True + return updated def pull(self, remote_directory, local_directory): def prune(src, exp): return [sub(exp, "", item) for item in src] updated = False - urn = Urn(remote_directory, directory=True) - - if not self.is_dir(urn.path()): - raise OptionNotValid(name="remote_path", value=remote_directory) - - if not os.path.exists(local_directory): - raise LocalResourceNotFound(local_directory) + self._validate_remote_directory(urn) + self._validate_local_directory(local_directory) local_resource_names = listdir(local_directory) @@ -724,23 +712,19 @@ class Client(object): remote_resource_names = prune(paths, expression) for remote_resource_name in remote_resource_names: - local_path = os.path.join(local_directory, remote_resource_name) - remote_path = "{remote_directory}{resource_name}".format(remote_directory=urn.path(), - resource_name=remote_resource_name) - + remote_path = "{remote_directory}{resource_name}".format(remote_directory=urn.path(), resource_name=remote_resource_name) remote_urn = Urn(remote_path) if remote_urn.path().endswith("/"): if not os.path.exists(local_path): updated = True os.mkdir(local_path) - self.pull(remote_directory=remote_path, local_directory=local_path) + result = self.pull(remote_directory=remote_path, local_directory=local_path) + updated = updated or result else: - if remote_resource_name in local_resource_names: - # Skip pull if local resource is more recent of we can't tell - if self.is_local_more_recent(local_path, remote_path) in (True, None): - continue + if remote_resource_name in local_resource_names and self.is_local_more_recent(local_path, remote_path): + continue self.download_file(remote_path=remote_path, local_path=local_path) updated = True @@ -757,15 +741,13 @@ class Client(object): """ try: remote_info = self.info(remote_path) - # Try to compare modification dates if our server - # offers that information remote_last_mod_date = remote_info['modified'] remote_last_mod_date = dateutil_parser.parse(remote_last_mod_date) - remote_last_mod_date_unix_ts = int(remote_last_mod_date.strftime("%s")) - local_last_mod_date_unix_ts = os.stat(local_path).st_mtime + remote_last_mod_date_unix_ts = int(remote_last_mod_date.timestamp()) + local_last_mod_date_unix_ts = int(os.stat(local_path).st_mtime) - return (local_last_mod_date_unix_ts > remote_last_mod_date_unix_ts) - except: + return remote_last_mod_date_unix_ts < local_last_mod_date_unix_ts + except ValueError or RuntimeWarning or KeyError: # If there is problem when parsing dates, or cannot get # last modified information, return None return None @@ -774,6 +756,18 @@ class Client(object): self.pull(remote_directory=remote_directory, local_directory=local_directory) self.push(remote_directory=remote_directory, local_directory=local_directory) + def _validate_remote_directory(self, urn): + if not self.is_dir(urn.path()): + raise OptionNotValid(name="remote_path", value=urn.path()) + + @staticmethod + def _validate_local_directory(local_directory): + if not os.path.isdir(local_directory): + raise OptionNotValid(name="local_path", value=local_directory) + + if not os.path.exists(local_directory): + raise LocalResourceNotFound(local_directory) + class Resource(object): def __init__(self, client, urn): From fe4e56caad796b2fe3f20fb9017194d4ca1c79e2 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Wed, 29 Jan 2020 22:34:16 -0800 Subject: [PATCH 54/63] Cancel support Python earlie 3.5 --- .travis.yml | 3 ++- setup.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3e5e1e4..1bbd775 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,8 @@ services: - docker python: - - "2.7" + - "3.5" + - "3.6" - "3.7" - "3.8" diff --git a/setup.py b/setup.py index 8f7e190..e4c41e7 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ setup( name='webdavclient3', version=version, packages=find_packages(), - requires=['python (>= 2.7.6)'], + requires=['python (>= 3.3.0)'], install_requires=['requests', 'lxml', 'python-dateutil'], scripts=['wdc'], test_suite='tests', @@ -73,7 +73,8 @@ setup( 'Operating System :: MacOS', 'Operating System :: Microsoft', 'Operating System :: Unix', - 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Topic :: Internet', From eced3b5fe5d9777b9f7899d6732c643008caab5e Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Fri, 31 Jan 2020 21:02:43 -0800 Subject: [PATCH 55/63] Bump version to 3.14 and prepare to release --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e4c41e7..fcbf7b2 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import setup, find_packages from setuptools.command.install import install as InstallCommand from setuptools.command.test import test as TestCommand -version = "0.14" +version = "3.14" requirements = "libxml2-dev libxslt-dev python-dev" From f64efedd96ffe3c844f1fa3b3093b32c52bfe932 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Tue, 18 Feb 2020 16:47:02 -0800 Subject: [PATCH 56/63] Fixed issue #40 - error during coping and moving files with cyrillic names --- README.md | 3 +++ tests/base_client_it.py | 1 + tests/test_client_it.py | 6 +++--- tests/test_cyrilic_client_it.py | 23 +++++++++++++++++++++++ tests/тестовый.txt | 1 + webdav3/client.py | 4 ++-- 6 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 tests/test_cyrilic_client_it.py create mode 100644 tests/тестовый.txt diff --git a/README.md b/README.md index 8db0bda..0445a5a 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,9 @@ res1.write_async(local_path="~/Downloads/file1", callback) Release Notes ------------- +**Version 3.14.1** + * Fixed issue during coping and moving files with cyrillic names + **Version 3.14** * Override methods for customizing communication with WebDAV servers * Support multiple clients simultaneously diff --git a/tests/base_client_it.py b/tests/base_client_it.py index 9fa988c..5b4290f 100644 --- a/tests/base_client_it.py +++ b/tests/base_client_it.py @@ -13,6 +13,7 @@ class BaseClientTestCase(unittest.TestCase): remote_path_dir = 'test_dir' remote_path_dir2 = 'test_dir2' remote_inner_path_dir = 'test_dir/inner' + inner_dir_name = 'inner' local_base_dir = 'tests/' local_file = 'test.txt' local_file_path = local_base_dir + 'test.txt' diff --git a/tests/test_client_it.py b/tests/test_client_it.py index 2d6c14f..515cca6 100644 --- a/tests/test_client_it.py +++ b/tests/test_client_it.py @@ -208,10 +208,10 @@ class ClientTestCase(BaseClientTestCase): self._prepare_for_downloading(True) self.client.pull(self.remote_path_dir, self.local_path_dir) self.assertTrue(path.exists(self.local_path_dir), 'Expected the directory is downloaded.') - self.assertTrue(path.exists(self.local_path_dir + os.path.sep + 'inner'), 'Expected the directory is downloaded.') - self.assertTrue(path.exists(self.local_path_dir + os.path.sep + 'inner'), 'Expected the directory is downloaded.') + self.assertTrue(path.exists(self.local_path_dir + os.path.sep + self.inner_dir_name), 'Expected the directory is downloaded.') + self.assertTrue(path.exists(self.local_path_dir + os.path.sep + self.inner_dir_name), 'Expected the directory is downloaded.') self.assertTrue(path.isdir(self.local_path_dir), 'Expected this is a directory.') - self.assertTrue(path.isdir(self.local_path_dir + os.path.sep + 'inner'), 'Expected this is a directory.') + self.assertTrue(path.isdir(self.local_path_dir + os.path.sep + self.inner_dir_name), 'Expected this is a directory.') self.assertTrue(path.exists(self.local_path_dir + os.path.sep + self.local_file), 'Expected the file is downloaded') self.assertTrue(path.isfile(self.local_path_dir + os.path.sep + self.local_file), diff --git a/tests/test_cyrilic_client_it.py b/tests/test_cyrilic_client_it.py new file mode 100644 index 0000000..87add07 --- /dev/null +++ b/tests/test_cyrilic_client_it.py @@ -0,0 +1,23 @@ +import os +import unittest + +from tests.test_client_it import ClientTestCase + + +class MultiClientTestCase(ClientTestCase): + remote_path_file = 'директория/тестовый.txt' + remote_path_file2 = 'директория/тестовый2.txt' + remote_inner_path_file = 'директория/вложенная/тестовый.txt' + remote_path_dir = 'директория' + remote_path_dir2 = 'директория2' + remote_inner_path_dir = 'директория/вложенная' + inner_dir_name = 'вложенная' + local_base_dir = 'tests/' + local_file = 'тестовый.txt' + local_file_path = local_base_dir + 'тестовый.txt' + local_path_dir = local_base_dir + 'res/директория' + pulled_file = local_path_dir + os.sep + local_file + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/тестовый.txt b/tests/тестовый.txt new file mode 100644 index 0000000..1cc4810 --- /dev/null +++ b/tests/тестовый.txt @@ -0,0 +1 @@ +test content for testing of webdav client \ No newline at end of file diff --git a/webdav3/client.py b/webdav3/client.py index a5da364..0768b41 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -535,7 +535,7 @@ class Client(object): raise RemoteParentNotFound(urn_to.path()) headers = [ - "Destination: {url}".format(url=self.get_url(urn_to)) + "Destination: {url}".format(url=self.get_url(urn_to.quote())) ] if self.is_dir(urn_from.path()): headers.append("Depth: {depth}".format(depth=depth)) @@ -558,7 +558,7 @@ class Client(object): if not self.check(urn_to.parent()): raise RemoteParentNotFound(urn_to.path()) - header_destination = "Destination: {path}".format(path=self.get_url(urn_to)) + header_destination = "Destination: {path}".format(path=self.get_url(urn_to.quote())) header_overwrite = "Overwrite: {flag}".format(flag="T" if overwrite else "F") self.execute_request(action='move', path=urn_from.quote(), headers_ext=[header_destination, header_overwrite]) From 3c5ba516af8ae198f00632a72dbd28f8b0343656 Mon Sep 17 00:00:00 2001 From: Daniel Loader Date: Fri, 14 Feb 2020 09:34:38 +0000 Subject: [PATCH 57/63] Add Oauth2 Bearer Token support Basic implementation of authenticating with token instead of basic auth, as the auth=(login,password) flag on requests sends junk authentication as it overrides the bearer authentication header with a base64 encoded blank string. --- webdav3/client.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/webdav3/client.py b/webdav3/client.py index 0768b41..94e212c 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -177,7 +177,7 @@ class Client(object): headers.extend(headers_ext) if self.webdav.token: - webdav_token = "Authorization: OAuth {token}".format(token=self.webdav.token) + webdav_token = "Authorization: Bearer {token}".format(token=self.webdav.token) headers.append(webdav_token) return dict([map(lambda s: s.strip(), i.split(':', 1)) for i in headers]) @@ -211,16 +211,27 @@ class Client(object): """ if self.session.auth: self.session.request(method="GET", url=self.webdav.hostname, verify=self.verify) # (Re)Authenticates against the proxy - response = self.session.request( - method=self.requests[action], - url=self.get_url(path), - auth=(self.webdav.login, self.webdav.password), - headers=self.get_headers(action, headers_ext), - timeout=self.timeout, - data=data, - stream=True, - verify=self.verify - ) + if all([self.webdav.login, self.webdav.password]) is False: + response = self.session.request( + method=self.requests[action], + url=self.get_url(path), + headers=self.get_headers(action, headers_ext), + timeout=self.timeout, + data=data, + stream=True, + verify=self.verify + ) + else: + response = self.session.request( + method=self.requests[action], + url=self.get_url(path), + auth=(self.webdav.login, self.webdav.password), + headers=self.get_headers(action, headers_ext), + timeout=self.timeout, + data=data, + stream=True, + verify=self.verify + ) if response.status_code == 507: raise NotEnoughSpace() if response.status_code == 404: From 0ac3c5e55faafb986c4f0c7ec4c1da39ba545780 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Tue, 18 Feb 2020 17:19:57 -0800 Subject: [PATCH 58/63] Small refactoring --- README.md | 1 + webdav3/client.py | 31 ++++++++++--------------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 0445a5a..a6eef1a 100644 --- a/README.md +++ b/README.md @@ -300,6 +300,7 @@ Release Notes ------------- **Version 3.14.1** * Fixed issue during coping and moving files with cyrillic names + * Support OAuth2 bearer tokens by https://github.com/danielloader **Version 3.14** * Override methods for customizing communication with WebDAV servers diff --git a/webdav3/client.py b/webdav3/client.py index 94e212c..ca29f80 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -211,27 +211,16 @@ class Client(object): """ if self.session.auth: self.session.request(method="GET", url=self.webdav.hostname, verify=self.verify) # (Re)Authenticates against the proxy - if all([self.webdav.login, self.webdav.password]) is False: - response = self.session.request( - method=self.requests[action], - url=self.get_url(path), - headers=self.get_headers(action, headers_ext), - timeout=self.timeout, - data=data, - stream=True, - verify=self.verify - ) - else: - response = self.session.request( - method=self.requests[action], - url=self.get_url(path), - auth=(self.webdav.login, self.webdav.password), - headers=self.get_headers(action, headers_ext), - timeout=self.timeout, - data=data, - stream=True, - verify=self.verify - ) + response = self.session.request( + method=self.requests[action], + url=self.get_url(path), + auth=(self.webdav.login, self.webdav.password) if not self.webdav.token else None, + headers=self.get_headers(action, headers_ext), + timeout=self.timeout, + data=data, + stream=True, + verify=self.verify + ) if response.status_code == 507: raise NotEnoughSpace() if response.status_code == 404: From 3aab20b02a00213bb6265f467a7e0527d9979f68 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Sun, 23 Feb 2020 17:43:12 -0800 Subject: [PATCH 59/63] Bump version to 3.14.1 before publish --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fcbf7b2..abd81c1 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import setup, find_packages from setuptools.command.install import install as InstallCommand from setuptools.command.test import test as TestCommand -version = "3.14" +version = "3.14.1" requirements = "libxml2-dev libxslt-dev python-dev" From af64110364af2253e3afd5e7f9f62982fa5a8be1 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 23 Mar 2020 10:34:34 +0100 Subject: [PATCH 60/63] Fix request calling to use session's auth. --- webdav3/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webdav3/client.py b/webdav3/client.py index ca29f80..c88a172 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -214,7 +214,7 @@ class Client(object): response = self.session.request( method=self.requests[action], url=self.get_url(path), - auth=(self.webdav.login, self.webdav.password) if not self.webdav.token else None, + auth=(self.webdav.login, self.webdav.password) if (not self.webdav.token and not self.session.auth) else None, headers=self.get_headers(action, headers_ext), timeout=self.timeout, data=data, From eeaf66a2788ca42ec93ed5b0410c391e28badfde Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Sun, 5 Apr 2020 17:10:49 -0700 Subject: [PATCH 61/63] Update Travis CI config for SonarQube --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1bbd775..0873913 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,7 @@ dist: xenial addons: sonarcloud: organization: "ezhov-evgeny" - token: - secure: f0f714f3bea6bd103e3eb82724ef3bb0d3b54d1d + token: f0f714f3bea6bd103e3eb82724ef3bb0d3b54d1d services: - docker From 39afefffc5e7b5613786dcaa2d719c50d847e703 Mon Sep 17 00:00:00 2001 From: Evgeny Ezhov Date: Sun, 5 Apr 2020 17:43:10 -0700 Subject: [PATCH 62/63] Fixed #49: Remove tailing slashes in web_hostname --- tests/test_tailing_slash_client_it.py | 26 ++++++++++++++++++++++++++ webdav3/connection.py | 1 + webdav3/urn.py | 2 -- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 tests/test_tailing_slash_client_it.py diff --git a/tests/test_tailing_slash_client_it.py b/tests/test_tailing_slash_client_it.py new file mode 100644 index 0000000..6a8744c --- /dev/null +++ b/tests/test_tailing_slash_client_it.py @@ -0,0 +1,26 @@ +import unittest + +from tests.test_client_it import ClientTestCase + + +class TailingSlashClientTestCase(ClientTestCase): + options = { + 'webdav_hostname': 'http://localhost:8585/', + 'webdav_login': 'alice', + 'webdav_password': 'secret1234', + 'webdav_override_methods': { + 'check': 'GET' + } + } + + def test_list_inner(self): + self._prepare_for_downloading(True) + file_list = self.client.list(self.remote_inner_path_dir) + self.assertIsNotNone(file_list, 'List of files should not be None') + + def test_hostname_no_tailing_slash(self): + self.assertEqual('5', self.client.webdav.hostname[-1]) + + +if __name__ == '__main__': + unittest.main() diff --git a/webdav3/connection.py b/webdav3/connection.py index 302212b..499ba23 100644 --- a/webdav3/connection.py +++ b/webdav3/connection.py @@ -50,6 +50,7 @@ class WebDAVSettings(ConnectionSettings): self.root = Urn(self.root).quote() if self.root else '' self.root = self.root.rstrip(Urn.separate) + self.hostname = self.hostname.rstrip(Urn.separate) def is_valid(self): if not self.hostname: diff --git a/webdav3/urn.py b/webdav3/urn.py index 6279de2..e78fa24 100644 --- a/webdav3/urn.py +++ b/webdav3/urn.py @@ -34,13 +34,11 @@ class Urn(object): return self._path def filename(self): - path_split = self._path.split(Urn.separate) name = path_split[-2] + Urn.separate if path_split[-1] == '' else path_split[-1] return unquote(name) def parent(self): - path_split = self._path.split(Urn.separate) nesting_level = self.nesting_level() parent_path_split = path_split[:nesting_level] From a3c75b7155662cbc0c63e202e20dfea2e8d76480 Mon Sep 17 00:00:00 2001 From: Alex Jordan Date: Mon, 6 Apr 2020 08:39:51 -0400 Subject: [PATCH 63/63] Fixes ezhov-evgeny#43 - local certificate options ignored If the webdav_cert_path and webdav_key_path options are set, they should be passed to the call to requests in Client.execute_request as the cert parameter. --- webdav3/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/webdav3/client.py b/webdav3/client.py index c88a172..9e495d1 100644 --- a/webdav3/client.py +++ b/webdav3/client.py @@ -217,6 +217,7 @@ class Client(object): auth=(self.webdav.login, self.webdav.password) if (not self.webdav.token and not self.session.auth) else None, headers=self.get_headers(action, headers_ext), timeout=self.timeout, + cert=(self.webdav.cert_path, self.webdav.key_path) if (self.webdav.cert_path and self.webdav.key_path) else None, data=data, stream=True, verify=self.verify