Compare commits

..

26 commits

Author SHA1 Message Date
Alex Jordan
a3c75b7155 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.
2020-04-06 15:40:35 -07:00
Evgeny Ezhov
39afefffc5 Fixed #49: Remove tailing slashes in web_hostname 2020-04-05 17:56:56 -07:00
Evgeny Ezhov
eeaf66a278 Update Travis CI config for SonarQube 2020-04-05 17:15:34 -07:00
Martin
af64110364 Fix request calling to use session's auth. 2020-04-05 11:43:51 -07:00
Evgeny Ezhov
3aab20b02a Bump version to 3.14.1 before publish 2020-02-23 17:43:12 -08:00
Evgeny Ezhov
0ac3c5e55f Small refactoring 2020-02-19 16:49:52 -08:00
Daniel Loader
3c5ba516af 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.
2020-02-19 16:49:52 -08:00
Evgeny Ezhov
f64efedd96 Fixed issue #40 - error during coping and moving files with cyrillic names 2020-02-18 16:56:36 -08:00
Evgeny Ezhov
eced3b5fe5 Bump version to 3.14 and prepare to release 2020-01-31 21:02:43 -08:00
Evgeny Ezhov
fe4e56caad Cancel support Python earlie 3.5 2020-01-31 21:00:10 -08:00
Evgeny Ezhov
8c7ee2e372 Fix issues and tests 2020-01-31 21:00:10 -08:00
Evgeny Ezhov
85b10d53c9 Update release notes 2020-01-31 21:00:10 -08:00
Ishak BELAHMAR
b3c753c126 Handle 2-way sync when files are modified 2020-01-31 21:00:10 -08:00
Evgeny Ezhov
3d4751cdd0 Support Python 3.8 2020-01-26 19:50:01 -08:00
Evgeny Ezhov
c380dc56c7 Update readme release notes 2020-01-26 19:05:33 -08:00
Evgeny Ezhov
f3e7d44276 Support multiple clients simultaneously
Fixed #34
2020-01-26 19:00:25 -08:00
Evgeny Ezhov
fc14ed2be1 Update dependencies 2020-01-20 18:12:24 -08:00
Evgeny Ezhov
336db8ae19 Override methods for customizing communication with WebDAV servers.
Fix #31 and #30
2020-01-20 18:12:24 -08:00
Evgeny Ezhov
95bea2182c Remove release date 2019-12-17 13:57:29 +03:00
Evgeny Ezhov
7db1cfd274 Bump version to 0.14 2019-12-17 13:52:58 +03:00
Evgeny Ezhov
5e2a9468c1 Increase test coverage 2019-12-17 13:49:10 +03:00
Evgeny Ezhov
5aad617fbf Update tests and fix issues 2019-12-02 11:52:04 +03:00
Evgeny Ezhov
5f84bb1343 Fixed #24 an issue with checking resources on Yandex WebDAV 2019-12-02 11:52:04 +03:00
Evgeny Ezhov
0b9c61e7e7 Fixed SonarQube analysis issues 2019-11-28 14:50:39 +03:00
Evgeny Ezhov
1859e43daf Add manual for using API 2019-11-28 14:20:29 +03:00
Evgeny Ezhov
120a6adfab Added Coverage in SonarQube 2019-11-28 00:43:38 +03:00
19 changed files with 963 additions and 223 deletions

View file

@ -4,15 +4,16 @@ dist: xenial
addons: addons:
sonarcloud: sonarcloud:
organization: "ezhov-evgeny" organization: "ezhov-evgeny"
token: token: f0f714f3bea6bd103e3eb82724ef3bb0d3b54d1d
secure: f0f714f3bea6bd103e3eb82724ef3bb0d3b54d1d
services: services:
- docker - docker
python: python:
- "2.7" - "3.5"
- "3.6"
- "3.7" - "3.7"
- "3.8"
before_install: before_install:
- docker pull bytemark/webdav - docker pull bytemark/webdav
@ -21,6 +22,11 @@ before_install:
install: install:
- python setup.py develop - python setup.py develop
- pip install coverage
script: script:
- python setup.py test - coverage run setup.py test
- sonar-scanner -X - coverage xml
- |
if [[ $TRAVIS_PYTHON_VERSION == "3.8" ]]; then
sonar-scanner
fi

317
README.md
View file

@ -1,28 +1,315 @@
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 Package webdavclient3 based on https://github.com/designerror/webdav-client-python but uses `requests` instead of `PyCURL`.
But uses `requests` instead of `PyCURL` It provides easy way to work with WebDAV-servers.
Installation
------------
```bash
$ pip install webdavclient3
```
Sample Usage 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 API
... webdav_hostname : <hostname> ==========
... webdav_login : <login>
... webdav_password : <password> 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`.
>>> client = Client(options)
>>> client.verify = False # To not check SSL certificates (Default = True) **Configuring the client**
>>> client.session.proxies(...) # To set proxy directly into the session (Optional)
>>> client.session.auth(...) # To set proxy auth directly into the session (Optional) Required keys for configuring client connection with WevDAV-server are `webdav_hostname` and `webdav_login`, `webdav_password`.
>>> client.execute_request("mkdir", <directory_name>)
```python
from webdav3.client import Client
options = {
'webdav_hostname': "https://webdav.server.ru",
'webdav_login': "login",
'webdav_password': "password"
}
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
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 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
* 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
**Version 0.13 27.11.2019** **Version 0.13 27.11.2019**
* Main version of Python is updated up to 3.7 * Main version of Python is updated up to 3.7
* Switch to use python sessions rather than requests by https://github.com/delrey1 * Switch to use python sessions rather than requests by https://github.com/delrey1

View file

@ -6,7 +6,7 @@ from setuptools import setup, find_packages
from setuptools.command.install import install as InstallCommand from setuptools.command.install import install as InstallCommand
from setuptools.command.test import test as TestCommand from setuptools.command.test import test as TestCommand
version = "0.13" version = "3.14.1"
requirements = "libxml2-dev libxslt-dev python-dev" requirements = "libxml2-dev libxslt-dev python-dev"
@ -51,8 +51,8 @@ setup(
name='webdavclient3', name='webdavclient3',
version=version, version=version,
packages=find_packages(), packages=find_packages(),
requires=['python (>= 2.7.6)'], requires=['python (>= 3.3.0)'],
install_requires=['requests', 'lxml', 'argcomplete'], install_requires=['requests', 'lxml', 'python-dateutil'],
scripts=['wdc'], scripts=['wdc'],
test_suite='tests', test_suite='tests',
tests_require=['pytest'], tests_require=['pytest'],
@ -73,8 +73,10 @@ setup(
'Operating System :: MacOS', 'Operating System :: MacOS',
'Operating System :: Microsoft', 'Operating System :: Microsoft',
'Operating System :: Unix', '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.7',
'Programming Language :: Python :: 3.8',
'Topic :: Internet', 'Topic :: Internet',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',
], ],

View file

@ -4,6 +4,8 @@ sonar.organization=ezhov-evgeny
sonar.projectName=webdav-client-python-3 sonar.projectName=webdav-client-python-3
sonar.projectVersion=0.13 sonar.projectVersion=0.13
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. # 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 # Encoding of the source code. Default is default system encoding
sonar.sourceEncoding=UTF-8 sonar.sourceEncoding=UTF-8

0
tests/__init__.py Normal file
View file

83
tests/base_client_it.py Normal file
View file

@ -0,0 +1,83 @@
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'
inner_dir_name = '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',
'webdav_override_methods': {
'check': 'GET'
}
}
# options = {
# 'webdav_hostname': 'https://webdav.yandex.ru',
# 'webdav_login': 'webdavclient.test2',
# 'webdav_password': 'Qwerty123!'
# }
def setUp(self):
self.client = Client(self.options)
self.clean_local_dir(self.local_path_dir)
def tearDown(self):
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 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:
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):
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()

View file

@ -3,41 +3,14 @@ import shutil
import unittest import unittest
from io import BytesIO, StringIO from io import BytesIO, StringIO
from os import path from os import path
from unittest import TestCase from time import sleep
from webdav3.client import Client from tests.base_client_it import BaseClientTestCase
from webdav3.exceptions import MethodNotSupported from webdav3.exceptions import MethodNotSupported, OptionNotValid, RemoteResourceNotFound
class ClientTestCase(TestCase): class ClientTestCase(BaseClientTestCase):
remote_path_file = 'test_dir/test.txt' pulled_file = BaseClientTestCase.local_path_dir + os.sep + BaseClientTestCase.local_file
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'
def setUp(self):
options = {
'webdav_hostname': 'http://localhost:8585',
'webdav_login': 'alice',
'webdav_password': 'secret1234'
}
self.client = Client(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 test_list(self): def test_list(self):
self._prepare_for_downloading() self._prepare_for_downloading()
@ -46,7 +19,10 @@ class ClientTestCase(TestCase):
self.assertGreater(file_list.__len__(), 0, 'Expected that amount of files more then 0') self.assertGreater(file_list.__len__(), 0, 'Expected that amount of files more then 0')
def test_free(self): 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') self.assertGreater(self.client.free(), 0, 'Expected that free space on WebDAV server is more then 0 bytes')
def test_check(self): def test_check(self):
@ -58,12 +34,29 @@ class ClientTestCase(TestCase):
self.client.mkdir(remote_path=self.remote_path_dir) 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.') 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() self._prepare_for_downloading()
buff = BytesIO() buff = BytesIO()
self.client.download_from(buff=buff, remote_path=self.remote_path_file) self.client.download_from(buff=buff, remote_path=self.remote_path_file)
self.assertEqual(buff.getvalue(), b'test content for testing of webdav client') 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): def test_download(self):
self._prepare_for_downloading() self._prepare_for_downloading()
self.client.download(local_path=self.local_path_dir, remote_path=self.remote_path_dir) self.client.download(local_path=self.local_path_dir, remote_path=self.remote_path_dir)
@ -101,6 +94,8 @@ class ClientTestCase(TestCase):
remote_path=self.remote_path_file, callback=callback) remote_path=self.remote_path_file, callback=callback)
self.assertFalse(path.exists(self.local_path_dir + os.path.sep + self.local_file), self.assertFalse(path.exists(self.local_path_dir + os.path.sep + self.local_file),
'Expected the file has not been downloaded yet') '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): def test_upload_from(self):
self._prepare_for_uploading() self._prepare_for_uploading()
@ -213,10 +208,10 @@ class ClientTestCase(TestCase):
self._prepare_for_downloading(True) self._prepare_for_downloading(True)
self.client.pull(self.remote_path_dir, self.local_path_dir) 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), '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 + '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.isdir(self.local_path_dir), 'Expected this is a directory.') 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), self.assertTrue(path.exists(self.local_path_dir + os.path.sep + self.local_file),
'Expected the file is downloaded') 'Expected the file is downloaded')
self.assertTrue(path.isfile(self.local_path_dir + os.path.sep + self.local_file), self.assertTrue(path.isfile(self.local_path_dir + os.path.sep + self.local_file),
@ -228,26 +223,60 @@ 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_dir), 'Expected the directory is created.')
self.assertTrue(self.client.check(self.remote_path_file), 'Expected the file is uploaded.') self.assertTrue(self.client.check(self.remote_path_file), 'Expected the file is uploaded.')
def _prepare_for_downloading(self, inner_dir=False): def test_valid(self):
if not self.client.check(remote_path=self.remote_path_dir): self.assertTrue(self.client.valid())
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): def test_check_is_overridden(self):
if not self.client.check(remote_path=self.remote_path_dir): self.assertEqual('GET', self.client.requests['check'])
self.client.mkdir(remote_path=self.remote_path_dir)
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): if not path.exists(path=self.local_path_dir):
os.makedirs(self.local_path_dir) os.mkdir(self.local_path_dir)
if not path.exists(path=self.local_path_dir + os.sep + self.local_file): 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) shutil.copy(src=self.local_file_path, dst=self.pulled_file)
return os.path.getmtime(self.pulled_file)
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -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()

View file

@ -4,7 +4,7 @@ from unittest import TestCase
from lxml.etree import ElementTree, Element from lxml.etree import ElementTree, Element
from webdav3.client import WebDavXmlUtils as utils from webdav3.client import WebDavXmlUtils as utils, listdir
class ClientTestCase(TestCase): class ClientTestCase(TestCase):
@ -135,6 +135,7 @@ class ClientTestCase(TestCase):
except: except:
f = open('./tests/response_dir.xml') f = open('./tests/response_dir.xml')
content = f.read().decode('utf-8').encode('utf-8') content = f.read().decode('utf-8').encode('utf-8')
f.close()
path = '/test_dir' path = '/test_dir'
hostname = 'https://webdav.yandex.ru' hostname = 'https://webdav.yandex.ru'
result = utils.parse_is_dir_response(content, path, hostname) result = utils.parse_is_dir_response(content, path, hostname)
@ -155,6 +156,11 @@ class ClientTestCase(TestCase):
result = utils.parse_is_dir_response(content.encode('utf-8'), path, hostname) result = utils.parse_is_dir_response(content.encode('utf-8'), path, hostname)
self.assertFalse(result, 'It should be file') 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__': if __name__ == '__main__':
unittest.main() unittest.main()

91
tests/test_connection.py Normal file
View file

@ -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()

View file

@ -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()

50
tests/test_exceptions.py Normal file
View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -0,0 +1 @@
test content for testing of webdav client

View file

@ -2,14 +2,16 @@
import functools import functools
import logging import logging
import lxml.etree as etree
import os import os
import requests
import shutil import shutil
import threading import threading
from io import BytesIO from io import BytesIO
from re import sub from re import sub
import lxml.etree as etree
import requests
from dateutil import parser as dateutil_parser
from webdav3.connection import * from webdav3.connection import *
from webdav3.exceptions import * from webdav3.exceptions import *
from webdav3.urn import Urn from webdav3.urn import Urn
@ -88,11 +90,8 @@ class Client(object):
# controls whether to verify the server's TLS certificate or not # controls whether to verify the server's TLS certificate or not
verify = True verify = True
# Sets the session for subsequent requests
session = requests.Session()
# HTTP headers for different actions # HTTP headers for different actions
http_header = { default_http_header = {
'list': ["Accept: */*", "Depth: 1"], 'list': ["Accept: */*", "Depth: 1"],
'free': ["Accept: */*", "Depth: 0", "Content-Type: text/xml"], 'free': ["Accept: */*", "Depth: 0", "Content-Type: text/xml"],
'copy': ["Accept: */*"], 'copy': ["Accept: */*"],
@ -105,89 +104,16 @@ class Client(object):
'set_property': ["Accept: */*", "Depth: 1", "Content-Type: application/x-www-form-urlencoded"] 'set_property': ["Accept: */*", "Depth: 1", "Content-Type: application/x-www-form-urlencoded"]
} }
def get_headers(self, action, headers_ext=None):
"""Returns HTTP headers of specified WebDAV actions.
:param action: the identifier of action.
:param headers_ext: (optional) the addition headers list witch sgould be added to basic HTTP headers for
the specified action.
:return: the dictionary of headers for specified action.
"""
if action in Client.http_header:
try:
headers = Client.http_header[action].copy()
except AttributeError:
headers = Client.http_header[action][:]
else:
headers = list()
if headers_ext:
headers.extend(headers_ext)
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(':', 1)) for i in headers])
def get_url(self, path):
"""Generates url by uri path.
:param path: uri path.
:return: the url string.
"""
url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': path}
return "{hostname}{root}{path}".format(**url)
def get_full_path(self, urn):
"""Generates full path to remote resource exclude hostname.
:param urn: the URN to resource.
:return: full path to resource with root path.
"""
return "{root}{path}".format(root=self.webdav.root, path=urn.path())
def execute_request(self, action, path, data=None, headers_ext=None):
"""Generate request to WebDAV server for specified action and path and execute it.
:param action: the action for WebDAV server which should be executed.
:param path: the path to resource for action
:param data: (optional) Dictionary or list of tuples ``[(key, value)]`` (will be form-encoded), bytes,
or file-like object to send in the body of the :class:`Request`.
:param headers_ext: (optional) the addition headers list witch should be added to basic HTTP headers for
the specified action.
: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
response = self.session.request(
method=Client.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:
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
# mapping of actions to WebDAV methods # mapping of actions to WebDAV methods
requests = { default_requests = {
'options': 'OPTIONS',
'download': "GET", 'download': "GET",
'upload': "PUT", 'upload': "PUT",
'copy': "COPY", 'copy': "COPY",
'move': "MOVE", 'move': "MOVE",
'mkdir': "MKCOL", 'mkdir': "MKCOL",
'clean': "DELETE", 'clean': "DELETE",
'check': "GET", 'check': "HEAD",
'list': "PROPFIND", 'list': "PROPFIND",
'free': "PROPFIND", 'free': "PROPFIND",
'info': "PROPFIND", 'info': "PROPFIND",
@ -222,11 +148,90 @@ class Client(object):
`webdav_verbose`: (optional) set verbose mode on.off. By default verbose mode is off. `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) webdav_options = get_options(option_type=WebDAVSettings, from_options=options)
self.webdav = WebDAVSettings(webdav_options) self.webdav = WebDAVSettings(webdav_options)
self.requests.update(self.webdav.override_methods)
self.default_options = {} self.default_options = {}
def get_headers(self, action, headers_ext=None):
"""Returns HTTP headers of specified WebDAV actions.
:param action: the identifier of action.
:param headers_ext: (optional) the addition headers list witch sgould be added to basic HTTP headers for
the specified action.
:return: the dictionary of headers for specified action.
"""
if action in self.http_header:
try:
headers = self.http_header[action].copy()
except AttributeError:
headers = self.http_header[action][:]
else:
headers = list()
if headers_ext:
headers.extend(headers_ext)
if 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])
def get_url(self, path):
"""Generates url by uri path.
:param path: uri path.
:return: the url string.
"""
url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': path}
return "{hostname}{root}{path}".format(**url)
def get_full_path(self, urn):
"""Generates full path to remote resource exclude hostname.
:param urn: the URN to resource.
:return: full path to resource with root path.
"""
return "{root}{path}".format(root=self.webdav.root, path=urn.path())
def execute_request(self, action, path, data=None, headers_ext=None):
"""Generate request to WebDAV server for specified action and path and execute it.
:param action: the action for WebDAV server which should be executed.
:param path: the path to resource for action
:param data: (optional) Dictionary or list of tuples ``[(key, value)]`` (will be form-encoded), bytes,
or file-like object to send in the body of the :class:`Request`.
:param headers_ext: (optional) the addition headers list witch should be added to basic HTTP headers for
the specified action.
: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
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 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
)
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=self.webdav.hostname)
if response.status_code >= 400:
raise ResponseErrorCode(url=self.get_url(path), code=response.status_code, message=response.content)
return response
def valid(self): def valid(self):
"""Validates of WebDAV settings. """Validates of WebDAV settings.
@ -243,9 +248,8 @@ class Client(object):
:return: list of nested file or directory names. :return: list of nested file or directory names.
""" """
directory_urn = Urn(remote_path, directory=True) directory_urn = Urn(remote_path, directory=True)
if directory_urn.path() != Client.root: if directory_urn.path() != Client.root and not self.check(directory_urn.path()):
if not self.check(directory_urn.path()): raise RemoteResourceNotFound(directory_urn.path())
raise RemoteResourceNotFound(directory_urn.path())
response = self.execute_request(action='list', path=directory_urn.quote()) response = self.execute_request(action='list', path=directory_urn.quote())
urns = WebDavXmlUtils.parse_get_list_response(response.content) urns = WebDavXmlUtils.parse_get_list_response(response.content)
@ -272,9 +276,9 @@ class Client(object):
:param remote_path: (optional) path to resource on WebDAV server. Defaults is root directory of WebDAV. :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 :return: True if resource is exist or False otherwise
""" """
if self.webdav.disable_check: if self.webdav.disable_check:
return True return True
urn = Urn(remote_path) urn = Urn(remote_path)
try: try:
response = self.execute_request(action='check', path=urn.quote()) response = self.execute_request(action='check', path=urn.quote())
@ -300,7 +304,11 @@ class Client(object):
if not self.check(directory_urn.parent()): if not self.check(directory_urn.parent()):
raise RemoteParentNotFound(directory_urn.path()) 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) return response.status_code in (200, 201)
@wrap_connection_error @wrap_connection_error
@ -457,7 +465,7 @@ class Client(object):
self.mkdir(remote_path) self.mkdir(remote_path)
for resource_name in listdir(local_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) _local_path = os.path.join(local_path, resource_name)
self.upload(local_path=_local_path, remote_path=_remote_path, progress=progress) self.upload(local_path=_local_path, remote_path=_remote_path, progress=progress)
@ -528,7 +536,7 @@ class Client(object):
raise RemoteParentNotFound(urn_to.path()) raise RemoteParentNotFound(urn_to.path())
headers = [ 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()): if self.is_dir(urn_from.path()):
headers.append("Depth: {depth}".format(depth=depth)) headers.append("Depth: {depth}".format(depth=depth))
@ -551,7 +559,7 @@ class Client(object):
if not self.check(urn_to.parent()): if not self.check(urn_to.parent()):
raise RemoteParentNotFound(urn_to.path()) 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") 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]) self.execute_request(action='move', path=urn_from.quote(), headers_ext=[header_destination, header_overwrite])
@ -579,13 +587,16 @@ class Client(object):
`modified`: date of resource modification. `modified`: date of resource modification.
""" """
urn = Urn(remote_path) urn = Urn(remote_path)
if not self.check(urn.path()) and not self.check(Urn(remote_path, directory=True).path()): self._check_remote_resource(remote_path, urn)
raise RemoteResourceNotFound(remote_path)
response = self.execute_request(action='info', path=urn.quote()) response = self.execute_request(action='info', path=urn.quote())
path = self.get_full_path(urn) path = self.get_full_path(urn)
return WebDavXmlUtils.parse_info_response(content=response.content, path=path, hostname=self.webdav.hostname) 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 @wrap_connection_error
def is_dir(self, remote_path): def is_dir(self, remote_path):
"""Checks is the remote resource directory. """Checks is the remote resource directory.
@ -596,8 +607,7 @@ class Client(object):
""" """
urn = Urn(remote_path) urn = Urn(remote_path)
parent_urn = Urn(urn.parent()) parent_urn = Urn(urn.parent())
if not self.check(urn.path()) and not self.check(Urn(remote_path, directory=True).path()): self._check_remote_resource(remote_path, urn)
raise RemoteResourceNotFound(remote_path)
response = self.execute_request(action='info', path=parent_urn.quote()) response = self.execute_request(action='info', path=parent_urn.quote())
path = self.get_full_path(urn) path = self.get_full_path(urn)
@ -662,50 +672,39 @@ class Client(object):
def prune(src, exp): def prune(src, exp):
return [sub(exp, "", item) for item in src] return [sub(exp, "", item) for item in src]
updated = False
urn = Urn(remote_directory, directory=True) urn = Urn(remote_directory, directory=True)
self._validate_remote_directory(urn)
if not self.is_dir(urn.path()): self._validate_local_directory(local_directory)
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)
paths = self.list(urn.path()) paths = self.list(urn.path())
expression = "{begin}{end}".format(begin="^", end=urn.path()) expression = "{begin}{end}".format(begin="^", end=urn.path())
remote_resource_names = prune(paths, expression) remote_resource_names = prune(paths, expression)
for local_resource_name in listdir(local_directory): for local_resource_name in listdir(local_directory):
local_path = os.path.join(local_directory, local_resource_name) local_path = os.path.join(local_directory, local_resource_name)
remote_path = "{remote_directory}{resource_name}".format(remote_directory=urn.path(), remote_path = "{remote_directory}{resource_name}".format(remote_directory=urn.path(), resource_name=local_resource_name)
resource_name=local_resource_name)
if os.path.isdir(local_path): if os.path.isdir(local_path):
if not self.check(remote_path=remote_path): if not self.check(remote_path=remote_path):
self.mkdir(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: else:
if local_resource_name in remote_resource_names: if local_resource_name in remote_resource_names and not self.is_local_more_recent(local_path, remote_path):
continue continue
self.upload_file(remote_path=remote_path, local_path=local_path) self.upload_file(remote_path=remote_path, local_path=local_path)
updated = True
return updated
def pull(self, remote_directory, local_directory): def pull(self, remote_directory, local_directory):
def prune(src, exp): def prune(src, exp):
return [sub(exp, "", item) for item in src] return [sub(exp, "", item) for item in src]
updated = False updated = False
urn = Urn(remote_directory, directory=True) urn = Urn(remote_directory, directory=True)
self._validate_remote_directory(urn)
if not self.is_dir(urn.path()): self._validate_local_directory(local_directory)
raise OptionNotValid(name="remote_path", value=remote_directory)
if not os.path.exists(local_directory):
raise LocalResourceNotFound(local_directory)
local_resource_names = listdir(local_directory) local_resource_names = listdir(local_directory)
@ -714,30 +713,62 @@ class Client(object):
remote_resource_names = prune(paths, expression) remote_resource_names = prune(paths, expression)
for remote_resource_name in remote_resource_names: for remote_resource_name in remote_resource_names:
local_path = os.path.join(local_directory, remote_resource_name) local_path = os.path.join(local_directory, remote_resource_name)
remote_path = "{remote_directory}{resource_name}".format(remote_directory=urn.path(), remote_path = "{remote_directory}{resource_name}".format(remote_directory=urn.path(), resource_name=remote_resource_name)
resource_name=remote_resource_name)
remote_urn = Urn(remote_path) remote_urn = Urn(remote_path)
if remote_urn.path().endswith("/"): if remote_urn.path().endswith("/"):
if not os.path.exists(local_path): if not os.path.exists(local_path):
updated = True updated = True
os.mkdir(local_path) 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: else:
if remote_resource_name in local_resource_names: if remote_resource_name in local_resource_names and self.is_local_more_recent(local_path, remote_path):
continue continue
self.download_file(remote_path=remote_path, local_path=local_path) self.download_file(remote_path=remote_path, local_path=local_path)
updated = True updated = True
return updated 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)
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.timestamp())
local_last_mod_date_unix_ts = int(os.stat(local_path).st_mtime)
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
def sync(self, remote_directory, local_directory):
self.pull(remote_directory=remote_directory, local_directory=local_directory) self.pull(remote_directory=remote_directory, local_directory=local_directory)
self.push(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): class Resource(object):
def __init__(self, client, urn): def __init__(self, client, urn):

View file

@ -6,10 +6,13 @@ from webdav3.urn import Urn
class ConnectionSettings: class ConnectionSettings:
def is_valid(self): def is_valid(self):
"""
Method checks is settings are valid
:return: True if settings are valid otherwise False
"""
pass pass
def valid(self): def valid(self):
try: try:
self.is_valid() self.is_valid()
except OptionNotValid: except OptionNotValid:
@ -22,21 +25,21 @@ class WebDAVSettings(ConnectionSettings):
ns = "webdav:" ns = "webdav:"
prefix = "webdav_" prefix = "webdav_"
keys = {'hostname', 'login', 'password', 'token', 'root', 'cert_path', 'key_path', 'recv_speed', 'send_speed', 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
password = None
token = None
root = None
cert_path = None
key_path = None
recv_speed = None
send_speed = None
verbose = None
disable_check = False
def __init__(self, options): 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() self.options = dict()
@ -47,9 +50,9 @@ class WebDAVSettings(ConnectionSettings):
self.root = Urn(self.root).quote() if self.root else '' self.root = Urn(self.root).quote() if self.root else ''
self.root = self.root.rstrip(Urn.separate) self.root = self.root.rstrip(Urn.separate)
self.hostname = self.hostname.rstrip(Urn.separate)
def is_valid(self): def is_valid(self):
if not self.hostname: if not self.hostname:
raise OptionNotValid(name="hostname", value=self.hostname, ns=self.ns) raise OptionNotValid(name="hostname", value=self.hostname, ns=self.ns)
@ -67,3 +70,4 @@ class WebDAVSettings(ConnectionSettings):
if not self.token and not self.login: if not self.token and not self.login:
raise OptionNotValid(name="login", value=self.login, ns=self.ns) raise OptionNotValid(name="login", value=self.login, ns=self.ns)
return True

View file

@ -71,7 +71,7 @@ class NoConnection(WebDavException):
self.hostname = hostname self.hostname = hostname
def __str__(self): 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. # This exception left only for supporting original library interface.
@ -96,7 +96,7 @@ class ResponseErrorCode(WebDavException):
class NotEnoughSpace(WebDavException): class NotEnoughSpace(WebDavException):
def __init__(self): def __init__(self):
pass self.message = "Not enough space on the server"
def __str__(self): def __str__(self):
return "Not enough space on the server" return self.message

View file

@ -34,13 +34,11 @@ class Urn(object):
return self._path return self._path
def filename(self): def filename(self):
path_split = self._path.split(Urn.separate) path_split = self._path.split(Urn.separate)
name = path_split[-2] + Urn.separate if path_split[-1] == '' else path_split[-1] name = path_split[-2] + Urn.separate if path_split[-1] == '' else path_split[-1]
return unquote(name) return unquote(name)
def parent(self): def parent(self):
path_split = self._path.split(Urn.separate) path_split = self._path.split(Urn.separate)
nesting_level = self.nesting_level() nesting_level = self.nesting_level()
parent_path_split = path_split[:nesting_level] parent_path_split = path_split[:nesting_level]