commit e80fe6c108493b87aa078022f46d69a92e13b35b Author: Yuriy Khomyakov Date: Mon Apr 17 00:10:58 2017 +0300 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4729bc5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +/**/*.pyc diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..44f0948 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,22 @@ +COPYRIGHT AND PERMISSION NOTICE + +Copyright (c) 2016, The WDC Project, and many +contributors, see the THANKS file. + +All rights reserved. + +Permission to use, copy, modify, and distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright +notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall not +be used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization of the copyright holder. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0385279 --- /dev/null +++ b/README.md @@ -0,0 +1,410 @@ +webdavclient +============ + +[![PyPI +version](https://badge.fury.io/py/webdavclient.svg)](http://badge.fury.io/py/webdavclient) +[![Requirements +Status](https://requires.io/github/designerror/webdav-client-python/requirements.svg?branch=master&style=flat)](https://requires.io/github/designerror/webdav-client-python/requirements/?branch=master&style=flat) +[![PullReview +stats](https://www.pullreview.com/github/designerror/webdavclient/badges/master.svg?)](https://www.pullreview.com/github/designerror/webdavclient/reviews/master) + +Package webdavclient provides easy and convenient work with WebDAV-servers (Yandex.Drive, Dropbox, Google Drive, Box, 4shared, etc.). The package includes the following components: webdav API, resource API and wdc. + +The source code of the project can be found +[here](https://github.com/designerror/webdavclient) +![Github](https://github.com/favicon.ico) + +Installation and upgrade +====================== + +**Installation** + +> Linux + +```bash +$ sudo apt-get install libxml2-dev libxslt-dev python-dev +$ sudo apt-get install libcurl4-openssl-dev python-pycurl +$ sudo easy_install webdavclient +``` + +> macOS + +```bash +$ curl https://bootstrap.pypa.io/ez_setup.py -o - | python +$ python setup.py install --prefix=/opt/setuptools +$ sudo easy_install webdavclient +``` + +**Update** + +```bash +$ sudo pip install -U webdavclient +``` + +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 +import webdav.client as wc +options = { + 'webdav_hostname': "https://webdav.server.ru", + 'webdav_login': "login", + 'webdav_password': "password" +} +client = wc.Client(options) +``` + +When a proxy server you need to specify settings to connect through it. + +```python +import webdav.client as wc +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 = wc.Client(options) +``` + +If you want to use the certificate path to certificate and private key is defined as follows: + +```python +import webdav.client as wc +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 = wc.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 = wc.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. + +**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 webdav.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) +``` + +wdc +=== + +wdc - a cross-platform utility that provides convenient work with WebDAV-servers right from your console. In addition to full implementations of methods from webdav API, also added methods content sync local and remote directories. + +**Authentication** + +- *Basic authentication* +```bash +$ wdc login https://wedbav.server.ru -p http://127.0.0.1:8080 +webdav_login: w_login +webdav_password: w_password +proxy_login: p_login +proxy_password: p_password +success +``` + +- Authorize the application using OAuth token* +```bash +$ wdc login https://wedbav.server.ru -p http://127.0.0.1:8080 --token xxxxxxxxxxxxxxxxxx +proxy_login: p_login +proxy_password: p_password +success +``` + +There are also additional keys `--root[-r]`, `--cert-path[-c]` and `--key-path[-k]`. + +**Utility** + +```bash +$ wdc check +success +$ wdc check file1 +not success +$ wdc free +245234120344 +$ wdc ls dir1 +file1 +... +fileN +$ wdc mkdir dir2 +$ wdc copy dir1/file1 -t dir2/file1 +$ wdc move dir2/file1 -t dir2/file2 +$ wdc download dir1/file1 -t ~/Downloads/file1 +$ wdc download dir1/ -t ~/Downloads/dir1/ +$ wdc upload dir2/file2 -f ~/Documents/file1 +$ wdc upload dir2/ -f ~/Documents/ +$ wdc publish di2/file2 +https://yadi.sk/i/vWtTUcBucAc6k +$ wdc unpublish dir2/file2 +$ wdc pull dir1/ -t ~/Documents/dir1/ +$ wdc push dir1/ -f ~/Documents/dir1/ +$ wdc info dir1/file1 +{'name': 'file1', 'modified': 'Thu, 23 Oct 2014 16:16:37 GMT', +'size': '3460064', 'created': '2014-10-23T16:16:37Z'} +``` +WebDAV-server +============= + +The most popular cloud-based repositories that support the Protocol WebDAV can be attributed Yandex.Drive, Dropbox, Google Drive, Box and 4shared. Access to data repositories, operating with access to the Internet. If necessary local locations and cloud storage, you can deploy your own WebDAV-server. + +**Local WebDAV-server** + +To deploy a local WebDAV server, using Docker containers +quite easily and quickly. To see an example of a local deploymentWebDAV servers can be on the project [webdav-server-docker](https://github.com/designerror/webdav-server-docker). + +**Supported methods** + +Servers |free|info|list|mkdir|clean|copy|move|download|upload +:------------|:--:|:--:|:--:|:---:|:---:|:--:|:--:|:------:|:----: +Yandex.Disk| \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ +Dropbox| \- | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ +Google Drive| \- | \+ | \+ | \+ | \+ | \- | \- | \+ | \+ +Box| \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ +4shared| \- | \+ | \+ | \+ | \+ | \- | \- | \+ | \+ +Webdavserver| \- | \+ | \+ | \+ | \+ | \- | \- | \+ | \+ + +Publish and unpublish methods supports only Yandex.Disk. + +**Configuring connections** + +To work with cloud storage Dropbox and Google Drive via the WebDAV Protocol, you must use a WebDAV-server DropDAV and DAV-pocket, respectively. + +A list of settings for WebDAV servers: + +```yaml +webdav-servers: + - yandex + hostname: https://webdav.yandex.ru + login: #login_for_yandex + password: #pass_for_yandex + - dropbox + hostname: https://dav.dropdav.com + login: #login_for dropdav + password: #pass_for_dropdav + - google + hostname: https://dav-pocket.appspot.com + root: docso + login: #login_for_dav-pocket + password: #pass_for_dav-pocket + - box + hostname: https://dav.box.com + root: dav + login: #login_for_box + password: #pass_for_box + - 4shared + hostname: https://webdav.4shared.com + login: #login_for_4shared + password: #pass_for_4shared +``` + +Autocompletion +============== + +For macOS, or older Unix systems you need to update bash. + +```bash +$ brew install bash +$ chsh +$ brew install bash-completion +``` + +Autocompletion can be enabled globally + +```bash +$ sudo activate-global-python-argcomplete +``` + +or locally + +```bash +#.bashrc +eval "$(register-python-argcomplete wdc)" +``` diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..07b88a1 --- /dev/null +++ b/README.rst @@ -0,0 +1,446 @@ +webdavclient +============ + +|PyPI version| |Requirements Status| |PullReview stats| + +Package webdavclient provides easy and convenient work with +WebDAV-servers (Yandex.Drive, Dropbox, Google Drive, Box, 4shared, +etc.). The package includes the following components: webdav API, +resource API and wdc. + +The source code of the project can be found +`here `__ |Github| + +Installation and upgrade +======================== + +**Installation** + +- Linux + +.. code:: bash + + $ sudo apt-get install libxml2-dev libxslt-dev python-dev + $ sudo apt-get install libcurl4-openssl-dev python-pycurl + $ sudo easy_install webdavclient + +- macOS + +.. code:: bash + + $ curl https://bootstrap.pypa.io/ez_setup.py -o - | python + $ python setup.py install --prefix=/opt/setuptools + $ sudo easy_install webdavclient + +**Update** + +.. code:: bash + + $ sudo pip install -U webdavclient + +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. + +.. code:: python + + import webdav.client as wc + options = { + 'webdav_hostname': "https://webdav.server.ru", + 'webdav_login': "login", + 'webdav_password': "password" + } + client = wc.Client(options) + +When a proxy server you need to specify settings to connect through it. + +.. code:: python + + import webdav.client as wc + 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 = wc.Client(options) + +If you want to use the certificate path to certificate and private key +is defined as follows: + +.. code:: python + + import webdav.client as wc + 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 = wc.Client(options) + +Or you want to limit the speed or turn on verbose mode: + +.. code:: python + + options = { + ... + 'recv_speed' : 3000000, + 'send_speed' : 3000000, + 'verbose' : True + } + client = wc.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. + +**Synchronous methods** + +.. code:: python + + // Checking existence of the resource + + client.check("dir1/file1") + client.check("dir1") + +.. code:: python + + // Get information about the resource + + client.info("dir1/file1") + client.info("dir1/") + +.. code:: python + + // Check free space + + free_size = client.free() + +.. code:: python + + // Get a list of resources + + files1 = client.list() + files2 = client.list("dir1") + +.. code:: python + + // Create directory + + client.mkdir("dir1/dir2") + +.. code:: python + + // Delete resource + + client.clean("dir1/dir2") + +.. code:: 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") + +.. code:: 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") + +.. code:: 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/") + +.. code:: 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/") + +.. code:: python + + // Publish the resource + + link = client.publish("dir1/file1") + link = client.publish("dir2") + +.. code:: python + + // Unpublish resource + + client.unpublish("dir1/file1") + client.unpublish("dir2") + +.. code:: python + + // Exception handling + + from webdav.client import WebDavException + try: + ... + except WebDavException as exception: + ... + +.. code:: python + + // Get the missing files + + client.pull(remote_directory='dir1', local_directory='~/Documents/dir1') + +.. code:: python + + // Send missing files + + client.push(remote_directory='dir1', local_directory='~/Documents/dir1') + +**Asynchronous methods** + +.. code:: 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) + +.. code:: 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. + +.. code:: python + + // Get a resource + + res1 = client.resource("dir1/file1") + +.. code:: 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) + +wdc +=== + +wdc \-a cross-platform utility that provides convenient work with +WebDAV-servers right from your console. In addition to full +implementations of methods from webdav API, also added methods content +sync local and remote directories. + +**Authentication** + +- *Basic authentication* + +.. code:: bash + + $ wdc login https://wedbav.server.ru -p http://127.0.0.1:8080 + webdav_login: w_login + webdav_password: w_password + proxy_login: p_login + proxy_password: p_password + success + +- Authorize the application using OAuth token\* + +.. code:: bash + + $ wdc login https://wedbav.server.ru -p http://127.0.0.1:8080 --token xxxxxxxxxxxxxxxxxx + proxy_login: p_login + proxy_password: p_password + success + +There are also additional keys ``--root[-r]``, ``--cert-path[-c]`` and +``--key-path[-k]``. + +**Utility** + +.. code:: bash + + $ wdc check + success + $ wdc check file1 + not success + $ wdc free + 245234120344 + $ wdc ls dir1 + file1 + ... + fileN + $ wdc mkdir dir2 + $ wdc copy dir1/file1 -t dir2/file1 + $ wdc move dir2/file1 -t dir2/file2 + $ wdc download dir1/file1 -t ~/Downloads/file1 + $ wdc download dir1/ -t ~/Downloads/dir1/ + $ wdc upload dir2/file2 -f ~/Documents/file1 + $ wdc upload dir2/ -f ~/Documents/ + $ wdc publish di2/file2 + https://yadi.sk/i/vWtTUcBucAc6k + $ wdc unpublish dir2/file2 + $ wdc pull dir1/ -t ~/Documents/dir1/ + $ wdc push dir1/ -f ~/Documents/dir1/ + $ wdc info dir1/file1 + {'name': 'file1', 'modified': 'Thu, 23 Oct 2014 16:16:37 GMT', + 'size': '3460064', 'created': '2014-10-23T16:16:37Z'} + +WebDAV-server +============= + +The most popular cloud-based repositories that support the Protocol +WebDAV can be attributed Yandex.Drive, Dropbox, Google Drive, Box and +4shared. Access to data repositories, operating with access to the +Internet. If necessary local locations and cloud storage, you can deploy +your own WebDAV-server. + +**Local WebDAV-server** + +To deploy a local WebDAV server, using Docker containers quite easily +and quickly. To see an example of a local deploymentWebDAV servers can +be on the project +`webdav-server-docker `__. + +**Supported methods** + ++----------------+--------+--------+--------+---------+---------+--------+--------+------------+----------+ +| Servers | free | info | list | mkdir | clean | copy | move | download | upload | ++================+========+========+========+=========+=========+========+========+============+==========+ +| Yandex.Disk | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ | ++----------------+--------+--------+--------+---------+---------+--------+--------+------------+----------+ +| Dropbox | \- | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ | ++----------------+--------+--------+--------+---------+---------+--------+--------+------------+----------+ +| Google Drive | \- | \+ | \+ | \+ | \+ | \- | \- | \+ | \+ | ++----------------+--------+--------+--------+---------+---------+--------+--------+------------+----------+ +| Box | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ | ++----------------+--------+--------+--------+---------+---------+--------+--------+------------+----------+ +| 4shared | \- | \+ | \+ | \+ | \+ | \- | \- | \+ | \+ | ++----------------+--------+--------+--------+---------+---------+--------+--------+------------+----------+ +| Webdavserver | \- | \+ | \+ | \+ | \+ | \- | \- | \+ | \+ | ++----------------+--------+--------+--------+---------+---------+--------+--------+------------+----------+ + +Publish and unpublish methods supports only Yandex.Disk. + +**Configuring connections** + +To work with cloud storage Dropbox and Google Drive via the WebDAV +Protocol, you must use a WebDAV-server DropDAV and DAV-pocket, +respectively. + +A list of settings for WebDAV servers: + +.. code:: yaml + + webdav-servers: + - yandex + hostname: https://webdav.yandex.ru + login: #login_for_yandex + password: #pass_for_yandex + - dropbox + hostname: https://dav.dropdav.com + login: #login_for dropdav + password: #pass_for_dropdav + - google + hostname: https://dav-pocket.appspot.com + root: docso + login: #login_for_dav-pocket + password: #pass_for_dav-pocket + - box + hostname: https://dav.box.com + root: dav + login: #login_for_box + password: #pass_for_box + - 4shared + hostname: https://webdav.4shared.com + login: #login_for_4shared + password: #pass_for_4shared + +Autocompletion +============== + +For macOS, or older Unix systems you need to update bash. + +.. code:: bash + + $ brew install bash + $ chsh + $ brew install bash-completion + +Autocompletion can be enabled globally + +.. code:: bash + + $ sudo activate-global-python-argcomplete + +or locally + +.. code:: bash + + #.bashrc + eval "$(register-python-argcomplete wdc)" + +.. |PyPI version| image:: https://badge.fury.io/py/webdavclient.svg + :target: http://badge.fury.io/py/webdavclient +.. |Requirements Status| image:: https://requires.io/github/designerror/webdav-client-python/requirements.svg?branch=master&style=flat + :target: https://requires.io/github/designerror/webdav-client-python/requirements/?branch=master&style=flat +.. |PullReview stats| image:: https://www.pullreview.com/github/designerror/webdavclient/badges/master.svg? + :target: https://www.pullreview.com/github/designerror/webdavclient/reviews/master +.. |Github| image:: https://github.com/favicon.ico + diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..e36bf43 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,688 @@ +Тестирование +=== + +В пакет Webdavclient включены следующие компоненты: + +- `webdav API` +- `resource API` +- `wdc` + +Каждый из компонентов имеет свою тестовую базу. + +### webdav API ### + +Компонент webdav API содержит следующие тестовые наборы: + +- настройка подключения +- аутентификация +- методы +- интеграционные тесты + +#### Настройка подключения #### + +Для инициализации клиента используется словарь (настройки подключения), содержащий набор опций подключения к webdav-серверу. + +Настройки подключения имеют обязательные и не обязательные опции. К обязательным опциям относятся `webdav_hostname`, `webdav_login` и `webdav_password`. + +Заполнение полей может быть валидным или не валидным. +Для проверки используются метод `valid?`. + +```python +import webdav.client as wc +options = { + 'webdav_hostname': "https://webdav.server.ru", + 'webdav_login': "login", + 'webdav_password': "password" +} +client = wc.Client(options) +client.valid? +``` + +*Тестовый сценарий 1* + + Идентификатор: 1.1.1 + Название: Обязательные опции подключения + Описание: В случае присутствия каждой из обязательных опций, + настройки подключения клиента будут валидными. + +Красный случай +```python +assert_that(client, is_(not_valid()) +``` + +Зеленый случай +```python +assert_that(client, is_(valid()) +``` + + +*Тестовый сценарий 2* + + Идентификатор: 1.1.2 + Название: Валидность обязательных опций подключения + Описание: В случае валидности обязательных опций, + настройки подключения клиента будут валидными. + +Красный случай +```python +#without webdav_hostname +#without webdav_login +#without webdav_password +#with proxy_login or proxy_password, but without proxy_hostname +#with proxy_password and proxy_hostname, but without proxy_login +assert_that(client, is_(not_valid()) +``` + +Зеленый случай +```python +assert_that(client, is_(valid()) +``` + + +*Тестовый сценарий 3* + + Идентификатор: 1.1.3 + Название: Валидность сертификата + Описание: При указании валидного файла и ключа сертификата, + настройки подключения клиента будут валидными. + +Красный случай +```python +#with key_path, but without cert_path +#key_path or cert_path not exists +assert_that(calling(client.is_valid), raises(CertificateNotValid)) +``` + +Зеленый случай +```python +assert_that(calling(client.is_valid), is_not(raises(CertificateNotValid)) +``` + +#### Аутентификация #### + +При подключении к webdav-серверу, необходимо пройти у него basic-аутентификацию, для этого используются опции `webdav_login` и `webdav_password`. При наличии proxy-сервера, может потребоваться так же пройти аутентификацию и у proxy-сервера. Для proxy-сервера поддерживаются следующие виды аутентификации: + +- `basic` +- `digest` +- `ntlm` +- `negotiate` + + +*Тестовый сценарий 1* + + Идентификатор: 1.2.1 + Название: Аутентификация с webdav-сервером + Описание: При правильной аутентификации клиент подключается к webdav-серверу. + +Красный случай +```python +assert_that(calling(client.check), is_(not_suceess()) +``` + +Зеленый случай +```python +assert_that(calling(client.check), is_(suceess()) +``` + + +*Тестовый сценарий 2* + + Идентификатор: 1.2.2 + Название: Аутентификация с proxy-сервером + Описание: При правильной аутентификации клиент подключается к webdav-серверу. + +Красный случай +```python +assert_that(calling(client.check), is_(not_suceess()) +``` + +Зеленый случай +```python +#basic +#digest +#ntlm +#negotiate +assert_that(calling(client.check), is_(suceess()) +``` + +#### Методы #### + +webdav API реализует следущие методы: `check`, `free`, `info`, `list`, `mkdir`, `clean`, `copy`, `move`, `download`, `upload`, `publish` и `unpublish`. + + +*Тестовый сценарий 1* + + Идентификатор: 1.3.1 + Название: Проверка существования ресурса + Описание: В случае существования ресурса, результат выполнения метода check + будет успешным. + +Красный случай +```python +assert_that(calling(client.check).with_args(remote_path), is_(not_suceess()) +``` + +Зеленый случай +```python +assert_that(calling(client.check).with_args(remote_path), is_(suceess()) +``` + + +*Тестовый сценарий 2* + + Идентификатор: 1.3.2 + Название: Проверка свободного места + Описание: В случае если webdav-сервер поддерживает метод free, метод возвращает + размер свободного места. + +Красный случай +```python +assert_that(calling(client.free), raises(MethodNotSupported)) +``` + +Зеленый случай +```python +assert_that(calling(client.free), greater_than(0)) +``` + + +*Тестовый сценарий 3* + + Идентификатор: 1.3.3 + Название: Получение информации о ресурсе + Описание: В случае если webdav-сервер поддерживает метод info, + метод возвращает информацию следующего типа: + - дата создания; + - дата модификации; + - размер; + - имя. + + +Красный случай +```python +assert_that(calling(client.info).with_args(remote_path), raises(RemoteResourceNotFound)) +``` + +Зеленый случай +```python +info = client(remote_path) +assert_that(info, has_key("data1")) +assert_that(info, has_key("data2")) +assert_that(info, has_key("size")) +assert_that(info, has_key("name")) +``` + + +*Тестовый сценарий 4* + + Идентификатор: 1.3.4 + Название: Получение списка ресурсов + Описание: В случае, если указанный ресурс существует и является директорией, то метод list + возвращает список ресурсов, находящихся в данном ресурсе. + + +Красный случай +```python +assert_that(calling(client.info).with_args(remote_file), raises(RemoteResourceNotFound)) +assert_that(calling(client.list).with_args(remote_path), raises(RemoteResourceNotFound)) +``` + +Зеленый случай +```python +files = client.list(remote_path) +assert_that(files, not_none())) +``` + + +*Тестовый сценарий 5* + + Идентификатор: 1.3.5 + Название: Создание директории + Описание: В случае, если все директории из путевого разбиения для + указанного ресурса существуют, то данный ресурс будет создан. + + +Красный случай +```python +assert_that(calling(client.info).with_args(remote_path), raises(RemoteParentNotFound)) +``` + +Зеленый случай +```python +client.mkdir(remote_path) +assert_that(calling(client.check).with_args(remote_path), is_(success())) +``` + + +*Тестовый сценарий 6* + + Идентификатор: 1.3.6 + Название: Удаление ресурса + Описание: В случае, если указанный ресурс существует и не является корнем, то + метод clean удалит данный ресурс. + + +Красный случай +```python +assert_that(calling(client.clean).with_args(remote_path), raises(RemoteResourceNotFound)) +assert_that(calling(client.clean).with_args(root), raises(InvalidOption)) +``` + +Зеленый случай +```python +client.clean(remote_path) +assert_that(calling(client.check).with_args(remote_path), is_(not_success())) +``` + + +*Тестовый сценарий 7* + + Идентификатор: 1.3.7 + Название: Копирование ресурса + Описание: В случае, если указанный ресурс существует и не является корнем, то + метод copy копирует данный ресурс. + +Красный случай +```python +assert_that(calling(client.copy).with_args(from_path=remote_path, to_path=new_path), raises(RemoteResourceNotFound)) +assert_that(calling(client.copy).with_args(from_path=root, to_path=new_path), raises(InvalidOption)) +assert_that(calling(client.copy).with_args(from_path=remote_path, to_path=root), raises(InvalidOption)) +assert_that(calling(client.copy).with_args(from_path=remote_path, to_path=remote_path), is_not(raises(WebDavException))) +``` + +Зеленый случай +```python +client.copy(from_path=remote_path, to_path=new_path) +assert_that(calling(client.check).with_args(new_path), is_(success())) +``` + + +*Тестовый сценарий 8* + + Идентификатор: 1.3.8 + Название: Перемещение ресурса + Описание: В случае, если указанный ресурс существует и не является корнем, то + метод move переместит данный ресурс. + +Красный случай +```python +assert_that(calling(client.move).with_args(from_path=old_path, to_path=new_path), raises(RemoteResourceNotFound)) +assert_that(calling(client.move).with_args(from_path=root, to_path=new_path), raises(InvalidOption)) +assert_that(calling(client.move).with_args(from_path=old_path, to_path=root), raises(InvalidOption)) +assert_that(calling(client.move).with_args(from_path=old_path, to_path=remote_path), is_not(raises(WebDavException))) +``` + +Зеленый случай +```python +client.move(from_path=old_path, to_path=new_path) +assert_that(calling(client.check).with_args(old_path), is_(not_success())) +assert_that(calling(client.check).with_args(new_path), is_(success())) +``` + + +*Тестовый сценарий 9* + + Идентификатор: 1.3.9 + Название: Загрузка ресурса + Описание: В случае, если указанный ресурс существует, + то метод download загрузит данный ресурс. + +Красный случай +```python +assert_that(calling(client.download).with_args(remote_path=remote_path, local_path=local_path), raises(LocalResourceNotFound)) +assert_that(calling(client.download).with_args(remote_path=remote_path, local_path=local_path), raises(RemoteResourceNotFound)) +assert_that(calling(client.download).with_args(remote_path=remote_file, local_path=local_directory), raises(InvalidOption)) +assert_that(calling(client.download).with_args(remote_path=remote_directory, local_path=local_file), raises(InvalidOption)) +``` + +Зеленый случай +```python +client.download(remote_path=remote_path, local_path=local_path) +assert_that(local_path, is_(exist())) +``` + + +*Тестовый сценарий 10* + + Идентификатор: 1.3.10 + Название: Выгрузка ресурса + Описание: В случае, если родительская директория указанный ресурса + существует, то метод upload выгрузит файл или директорию в + ресурс. + +Красный случай +```python +assert_that(calling(client.upload).with_args(remote_path=remote_path, local_path=local_path), raises(RemoteParentNotFound)) +assert_that(calling(client.upload).with_args(remote_path=remote_file, local_path=local_directory), raises(InvalidOption)) +assert_that(calling(client.upload).with_args(remote_path=remote_directory, local_path=local_file), raises(InvalidOption)) +``` + +Зеленый случай +```python +client.upload(remote_path=remote_path, to_path=local_path) +assert_that(calling(client.check).with_args(remote_path), is_(success())) +``` + + +*Тестовый сценарий 11* + + Идентификатор: 1.3.11 + Название: Публикация ресурса + Описание: В случае, если указанный ресурс существует, то метод publish + возвращает публичную ссылку на ресурс. + +Красный случай +```python +assert_that(calling(client.publish).with_args(remote_path), raises(RemoteResourceNotFound)) +``` + +Зеленый случай +```python +assert_that(calling(client.publish).with_args(remote_path), is_not(raises(RemoteResourceNotFound)) +link = client.publish(remote_path) +assert_that(link, starts_with("http") +``` + + +*Тестовый сценарий 12* + + Идентификатор: 1.3.12 + Название: Отмена публикации ресурса + Описание: В случае, если указанный ресурс существует, + то метод unpublish отменяет публикацию ресурса. + +Красный случай +```python +assert_that(calling(client.unpublish).with_args(remote_path), raises(RemoteResourceNotFound)) +``` + +Зеленый случай +```python +assert_that(calling(client.unpublish).with_args(remote_path), is_not(raises(RemoteResourceNotFound)) +``` + +### resource API ### + +Компонент resource API состоит из следующих тестовых наборов методы: + +- получение ресурса +- методы + +#### Получение ресурса #### + +Для получение ресурса, используется метод `resource`. + + +*Тестовый сценарий 1* + + Идентификатор: 2.1.1 + Название: Получение ресурса + Описание: В случае, если указанный ресурс является директорией и существует, + то метод resource возвращает ресурс. + В случае, если указанный ресурс является файлом, + то метод resource возвращает ресурс. + +Красный случай +```python +assert_that(calling(client.resource).with_args(remote_directory), raises(RemoteResourceNotFound)) +assert_that(calling(client.resource).with_args(remote_file), is_not(raises(RemoteResourceNotFound))) +``` + +Зеленый случай +```python +assert_that(calling(client.check).with_args(remote_path), is_(success()) +res = client.resource(remote_path) +assert_that(res.check()) +``` + +#### Методы #### + +resource API реализует следущие методы: `check`, `clean`, `is_directory`, `rename`, `move`, `copy`, `info`, `read_from`, `read`, `read_async`, `write_to`, `write`, `write_async`, `publish` и `unpublish`. + + +*Тестовый сценарий 1* + + Идентификатор: 2.2.1 + Название: Проверка существования ресурса + Описание: В случае, если указанный ресурс существует, + то результат метода check будет успешным. + +Красный случай +```python +assert_that(calling(client.resource).with_args(remote_path), raises(RemoteResourceNotFound)) +``` + +Зеленый случай +```python +assert_that(calling(client.check).with_args(remote_path), is_(success()) +res = client.resource(remote_path) +assert_that(res.check()) +``` + + +*Тестовый сценарий 2* + + Идентификатор: 2.2.2 + Название: Удаление ресурса + Описание: В случае, если указанный ресурс существует, + то метод clean удалит данный ресурс. + +Красный случай +```python +res = client.resource(remote_path) +assert_that(calling(res.clean), is_not(raises(RemoteResourceNotFound))) +``` + +Зеленый случай +```python +assert_that(calling(client.check).with_args(remote_path), is_(success()) +res = client.resource(remote_path) +assert_that(res.check()) +``` + + +*Тестовый сценарий 3* + + Идентификатор: 2.2.3 + Название: Проверка является ли ресурс директорией + Описание: В случае, если указанный ресурс является директорией, + то результат метода is_directory будет успешным. + +Красный случай +```python +res = client.resource(remote_file) +assert_that(calling(res.is_directory), is_(not_success())) +``` + +Зеленый случай +```python +res = client.resource(remote_directory) +assert_that(calling(res.is_directory), is_(success())) +``` + + +*Тестовый сценарий 4* + + Идентификатор: 2.2.4 + Название: Переименование ресурса + Описание: В случае, если указанный ресурс существует, + то метод rename переименует данный ресурс. + +Красный случай +```python +res = client.resource(remote_path) +assert_that(calling(res.rename).with_args(new_name), raises(RemoteResourceNotFound)) +assert_that(calling(res.rename).with_args(new_name), raises(RemoteResourceAlreadyExists)) +``` + +Зеленый случай +```python +res = client.resource(old_path) +res.rename(new_name) +new_path = res.urn +assert_that(calling(client.check).with_args(old_path), is_(not_success())) +assert_that(calling(client.check).with_args(new_path), is_(success())) +``` + + +*Тестовый сценарий 5* + + Идентификатор: 2.2.5 + Название: Перемещение ресурса + Описание: В случае, если указанный ресурс существует, + то метод move переместит данный ресурс. + +Красный случай +```python +res = client.resource(old_path) +assert_that(calling(res.move).with_args(new_path), raises(RemoteResourceNotFound)) +``` + +Зеленый случай +```python +res = client.resource(old_path) +res.move(new_path) +assert_that(calling(client.check).with_args(old_path), is_(not_success())) +assert_that(calling(client.check).with_args(new_path), is_(success())) +``` + + +*Тестовый сценарий 6* + + Идентификатор: 2.2.6 + Название: Копирование ресурса + Описание: В случае, если указанный ресурс существует, + то метод copy скопирует данный ресурс. + +Красный случай +```python +res = client.resource(remote_path) +assert_that(calling(res.copy).with_args(to_path), raises(RemoteResourceNotFound)) +``` + +Зеленый случай +```python +res = client.resource(remote_path) +res.copy(new_path) +assert_that(calling(client.check).with_args(remote_path), is_(success())) +assert_that(calling(client.check).with_args(new_path), is_(success())) +``` + + +*Тестовый сценарий 7* + + Идентификатор: 2.2.7 + Название: Получение информации о ресурсе + Описание: В случае, если указанный ресурс существует, + то метод info возвращает информацию следующего типа: + - дата создания; + - дата модификации; + - размер; + - имя. + + +Красный случай +```python +res = client.resource(remote_path) +assert_that(calling(res.info), raises(RemoteResourceNotFound)) +``` + +Зеленый случай +```python +res = client.resource(remote_path) +info = res.info() +assert_that(info, has_key("data1")) +assert_that(info, has_key("data2")) +assert_that(info, has_key("size")) +assert_that(info, has_key("name")) +``` + + +*Тестовый сценарий 8* + + Идентификатор: 2.2.8 + Название: Считывание данных с буфера в ресурс + Описание: В случае, если указанный ресурс не является директорией, + то метод read_from считывет содержимое буфера и записывает в ресурс. + +Красный случай +```python +res1 = client.resource(remote_file) +assert_that(buff, is_(empty)) +assert_that(calling(res1.read_from).with_args(buff), raises(BufferIsEmpty)) + +res2 = client.resource(remote_directory) +assert_that(calling(res2.read_from).with_args(buff), raises(ResourceIsNotDirectory)) +``` + +Зеленый случай +```python +res = client.resource(remote_path) +res.read_from(buff) +res_size = res.info("size") +assert_that(buff.size(), equal_to(res_size)) +``` + + +*Тестовый сценарий 9* + + Идентификатор: 2.2.9 + Название: Запись данных в буфер + Описание: В случае, если указанный ресурс не является директорией, + то метод write_to записывает содержимое ресурса в буфер. + +Красный случай +```python +res = client.resource(remote_path) +assert_that(calling(res.write_to).with_args(buff), raises(RemoteResourceNotFound)) +``` + +Зеленый случай +```python +res = client.resource(remote_path) +res.write_to(buff) +res_size = res.info("size") +assert_that(buff.size(), equal_to(res_size)) +``` + + +*Тестовый сценарий 10* + + Идентификатор: 2.2.10 + Название: Публикация ресурса + Описание: В случае, если указанный ресурс существует, то метод publish + возвращает публичную ссылку на ресурс. + +Красный случай +```python +res = client.resource(remote_path) +assert_that(calling(res.publish), raises(RemoteResourceNotFound)) +``` + +Зеленый случай +```python +res = client.resource(remote_path) +assert_that(calling(res.publish), is_not(raises(RemoteResourceNotFound)) +link = res.publish() +assert_that(link, starts_with("http") +``` + + +*Тестовый сценарий 11* + + Идентификатор: 2.2.11 + Название: Отмена публикации ресурса + Описание: В случае, если указанный ресурс существует, + то метод unpublish отменяет публикацию ресурса. + +Красный случай +```python +res = client.resource(remote_path) +assert_that(calling(res.unpublish), raises(RemoteResourceNotFound)) +``` + +Зеленый случай +```python +res = client.resource(remote_path) +assert_that(calling(res.unpublish).with_args(remote_path), is_not(raises(RemoteResourceNotFound)) +``` diff --git a/THANKS.md b/THANKS.md new file mode 100644 index 0000000..0086260 --- /dev/null +++ b/THANKS.md @@ -0,0 +1 @@ +- [cesterlizi](https://github.com/cesterlizi) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..275d8a0 --- /dev/null +++ b/setup.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import sys +from setuptools import setup, find_packages + +from setuptools.command.test import test as TestCommand +from setuptools.command.install import install as InstallCommand + +version = "0.1" +requirements = "libxml2-dev libxslt-dev python-dev" + + +class Install(InstallCommand): + + def run(self): + + #params = "{install_params} {requirements}".format(install_params="install", requirements=requirements) + #cmd = "{command} {params}".format(command="apt-get", params=params) + #proc = subprocess.Popen(cmd, shell=True) + # proc.wait() + InstallCommand.run(self) + + +class Test(TestCommand): + + user_options = [('pytest-args=', 'a', "")] + + def initialize_options(self): + + TestCommand.initialize_options(self) + self.pytest_args = [] + + def finalize_options(self): + + TestCommand.finalize_options(self) + self.test_args = [] + self.test_suite = True + + def run_tests(self): + + import pytest + errno = pytest.main(self.pytest_args) + sys.exit(errno) + +setup( + name='webdavclient2', + version=version, + packages=find_packages(), + requires=['python (>= 2.7.6)'], + install_requires=['requests', 'lxml'], + scripts=['wdc'], + tests_require=['pytest', 'pyhamcrest', 'junit-xml', 'pytest-allure-adaptor'], + cmdclass={'install': Install, 'test': Test}, + description='Webdav API, resource API и wdc для WebDAV-серверов (Yandex.Disk, Dropbox, Google Disk, Box, 4shared и т.д.)', + long_description=open('README.rst').read(), + author='Designerror', + author_email='designerror@yandex.ru', + url='https://github.com/designerror/webdavclient', + download_url='https://github.com/designerror/webdavclient/tarball/master', + license='MIT License', + keywords='webdav, client, python, module, library, packet, Yandex.Disk, Dropbox, Google Disk, Box, 4shared', + classifiers=[ + 'Environment :: Console', + 'Environment :: Web Environment', + 'License :: OSI Approved :: MIT License', + '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', + 'Topic :: Internet', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], +) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..723db66 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,32 @@ +__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/webdav/test_authenticate.py b/tests/webdav/test_authenticate.py new file mode 100644 index 0000000..c03e2b9 --- /dev/null +++ b/tests/webdav/test_authenticate.py @@ -0,0 +1 @@ +__author__ = 'designerror' diff --git a/tests/webdav/test_connection.py b/tests/webdav/test_connection.py new file mode 100644 index 0000000..445483f --- /dev/null +++ b/tests/webdav/test_connection.py @@ -0,0 +1,11 @@ +__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 new file mode 100644 index 0000000..c03e2b9 --- /dev/null +++ b/tests/webdav/test_methods.py @@ -0,0 +1 @@ +__author__ = 'designerror' diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..79b963a --- /dev/null +++ b/tox.ini @@ -0,0 +1,13 @@ +[tox] +envlist = py27 +toxworkdir={toxinidir}/../.tox +[testenv] +deps= + pycurl + lxml + argcomplete + pytest + pyhamcrest + junit-xml + pytest-allure-adaptor +commands=python setup.py test -a "--alluredir /var/tmp/allure" diff --git a/wdc b/wdc new file mode 100644 index 0000000..575d553 --- /dev/null +++ b/wdc @@ -0,0 +1,621 @@ +#!/usr/bin/python +# PYTHON_ARGCOMPLETE_OK + +from __future__ import print_function +import sys +import os +import shlex +import struct +import platform +import subprocess +import getpass +import argparse +import argcomplete +from webdav.client import Client, WebDavException, NotConnection, Urn +from distutils.util import strtobool +from base64 import b64decode, b64encode + + +def get_terminal_size(): + current_os = platform.system() + tuple_xy = None + if current_os == 'Windows': + tuple_xy = _get_terminal_size_windows() + if tuple_xy is None: + tuple_xy = _get_terminal_size_input() + if current_os in ['Linux', 'Darwin'] or current_os.startswith('CYGWIN'): + tuple_xy = _get_terminal_size_linux() + if tuple_xy is None: + tuple_xy = 80, 25 + return tuple_xy + + +def _get_terminal_size_windows(): + try: + from ctypes import windll, create_string_buffer + h = windll.kernel32.GetStdHandle(-12) + csbi = create_string_buffer(22) + res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) + if res: + (bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw) + sizex = right - left + 1 + sizey = bottom - top + 1 + return sizex, sizey + except: + pass + + +def _get_terminal_size_input(): + try: + cols = int(subprocess.check_call(shlex.split('tput cols'))) + rows = int(subprocess.check_call(shlex.split('tput lines'))) + return (cols, rows) + except: + pass + + +def _get_terminal_size_linux(): + + def ioctl_GWINSZ(fd): + try: + import fcntl + import termios + return struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) + except: + pass + cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) + if not cr: + try: + fd = os.open(os.ctermid(), os.O_RDONLY) + cr = ioctl_GWINSZ(fd) + os.close(fd) + except: + pass + if not cr: + try: + cr = (os.environ['LINES'], os.environ['COLUMNS']) + except: + return None + return int(cr[1]), int(cr[0]) + + +class ProgressBar: + + def __init__(self): + self.precise = 0 + self.total = 0 + + def show(self): + progress_line = self._progress_line() + import sys + if self.precise == 100: + print(progress_line, end="") + else: + print(progress_line, end="") + sys.stdout.write("\r") + + def _progress_line(self): + sizex, sizey = get_terminal_size() + available_width = sizex + precise = self._get_field_precise() + ratio = self._get_field_ratio() + available_width -= len(precise) + len(ratio) + progress = self._get_field_progress(available_width-2) + return "{precise} {progress} {ratio}".format(precise=precise, progress=progress, ratio=ratio) + + def _get_field_precise(self): + line = "{precise}%".format(precise=self.precise) + return "{:<4}".format(line) + + def _get_field_ratio(self): + current = float(self.precise * self.total / 100) + total = float(self.total) + current_line = "{:.2f}".format(current) + total_line = "{:.2f}".format(total) + line = "{current}/{total}".format(current=current_line, total=total_line) + available = len(total_line) * 2 + 1 + format_line = "{prefix}{value}{sufix}".format(prefix="{:>", value=available, sufix="}") + line = format_line.format(line) + return line + + def _get_field_progress(self, width): + available_width = width - 2 + current = int(self.precise * available_width / 100) + available_width -= current + 1 + tip = ">" if not self.precise == 100 else "" + progress = "{arrow}{tip}{space}".format(arrow="=" * current, tip=tip, space=" " * available_width) + return "[{progress}]".format(progress=progress) + + def callback(self, current, total): + if total and not self.total: + self.total = total + if not total: + return + precise = int(float(current) * 100 / total) + if self.precise == precise: + return + else: + self.precise = precise + self.show() + +setting_keys = ['webdav_hostname', 'webdav_root', 'webdav_login', 'webdav_password', 'webdav_token', + 'proxy_hostname', 'proxy_login', 'proxy_password', + 'cert_path', 'key_path'] + +crypto_keys = ['webdav_password', 'webdav_token', 'proxy_password'] + + +def encoding(source): + + if not source: + return "" + return b64encode(source.encode('utf-8')) + + +def decoding(source): + + if not source: + return "" + return b64decode(source).decode('utf-8') + + +def import_options(): + + options = dict() + for setting_key in setting_keys: + options[setting_key] = os.environ.get(setting_key.upper()) + + for crypto_key in crypto_keys: + if not options[crypto_key]: + continue + options[crypto_key] = decoding(options[crypto_key]) + + return options + + +def valid(options): + + if 'webdav_hostname' not in options: + return False + + if not options['webdav_token'] and not options['webdav_login']: + return False + + if options['webdav_login'] and not options['webdav_password']: + return False + + return True + + +class Formatter(argparse.RawTextHelpFormatter): + + def _get_default_metavar_for_optional(self, action): + if not action.option_strings: + return action.dest + else: + return "" + + def _format_action_invocation(self, action): + if not action.option_strings: + default = self._get_default_metavar_for_optional(action) + metavar, = self._metavar_formatter(action, default)(1) + return metavar + + else: + parts = [] + + if action.nargs == 0: + parts.extend(action.option_strings) + else: + default = self._get_default_metavar_for_optional(action) + args_string = self._format_args(action, default) + for option_string in action.option_strings: + parts.append(option_string) + + return '%s %s' % (', '.join(parts), args_string) + + return ', '.join(parts) + + def _metavar_formatter(self, action, default_metavar): + if action.metavar is not None: + result = action.metavar + else: + result = default_metavar + + def format(tuple_size): + if isinstance(result, tuple): + return result + else: + return (result, ) * tuple_size + return format + + +def logging_exception(exception): + print(exception) + + +def urn_completer(prefix, **kwargs): + + options = import_options() + try: + client = Client(options) + + prefix_urn = Urn(prefix) + if prefix_urn.is_dir(): + return (prefix+filename for filename in client.list(prefix_urn.path())) + else: + parent = prefix_urn.parent() + prefix_filename = prefix_urn.filename() + prefix_filename_length = len(prefix_filename) + return (prefix + filename[prefix_filename_length:] for filename in client.list(parent) if filename.startswith(prefix_filename)) + except WebDavException: + pass + + return tuple() + +if __name__ == "__main__": + + epilog = """ + Examples: + -------- + $ wdc login https://webdav.server.ru + webdav_login: login + webdav_password: password + success + $ wdc login https://webdav.server.ru --token xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + not success + $ wdc check + success + $ wdc check file1 + not success + $ wdc free + 245234120344 + $ wdc ls dir1 + file1 + ... + fileN + $ wdc mkdir dir2 + $ wdc copy dir1/file1 -t dir2/file1 + $ wdc move dir2/file1 -t dir2/file2 + $ wdc download dir1/file1 -t ~/Downloads/file1 + $ wdc download dir1/ -t ~/Downloads/dir1/ + $ wdc upload dir2/file2 -f ~/Documents/file1 + $ wdc upload dir2/ -f ~/Documents/ + $ wdc publish di2/file2 + https://yadi.sk/i/vWtTUcBucAc6k + $ wdc unpublish dir2/file2 + $ wdc pull dir1/ -t ~/Documents/dir1/ + $ wdc push dir1/ -f ~/Documents/dir1/ + $ wdc info dir1/file1 + {'name': 'file1', 'modified': 'Thu, 23 Oct 2014 16:16:37 GMT', + 'size': '3460064', 'created': '2014-10-23T16:16:37Z'} + """ + + usage = """ + wdc [-h] [-v] + wdc login https://webdav.server.ru [--token] [-r] [-p] [-c] [-k] + wdc [action] [path] [-t] [-f] + """ + + actions = "login logout check info free ls clean mkdir copy move download upload publish unpublish push pull".split() + actions_help = "check, info, free, ls, clean, mkdir, copy, move,\ndownload, upload, publish, unpublish, push, pull" + + 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 + 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") + parser.add_argument("--token", help="example: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + parser.add_argument("-c", "--cert-path", help="example: /etc/ssl/certs/certificate.crt") + parser.add_argument("-k", "--key-path", help="example: /etc/ssl/private/certificate.key") + parser.add_argument("-p", "--proxy", help="example: http://127.0.0.1:8080") + parser.add_argument("path", help="example: dir1/dir2/file1", nargs='?').completer = urn_completer + parser.add_argument("-f", '--from-path', help="example: ~/Documents/file1") + parser.add_argument("-t", "--to-path", help="example for download and pull: ~/Download/file1\nexample for copy and move: dir1/dir2").completer = urn_completer + + argcomplete.autocomplete(parser, exclude=("-h", "--help", "--proxy", "-p", "-r", "--root", "-c", "--cert-path", "-t", "--to-path", "-v", "--version", "-f", "--from-path", "-k", "--key-path")) + args = parser.parse_args() + action = args.action + + if action == 'login': + env = dict() + if not args.path: + try: + env['webdav_hostname'] = raw_input("webdav_hostname: ") + except NameError: + env['webdav_hostname'] = input("webdav_hostname: ") + else: + env['webdav_hostname'] = args.path + + if not args.token: + try: + env['webdav_login'] = raw_input("webdav_login: ") + except NameError: + env['webdav_login'] = input("webdav_login: ") + env['webdav_password'] = getpass.getpass("webdav_password: ") + else: + env['webdav_token'] = args.token + + if args.proxy: + env['proxy_hostname'] = args.proxy + try: + env['proxy_login'] = raw_input("proxy_login: ") + except NameError: + env['proxy_login'] = input("proxy_login: ") + env['proxy_password'] = getpass.getpass("proxy_password: ") + + if args.root: + env['webdav_root'] = args.root + + if args.cert_path: + env['cert_path'] = args.cert_path + + if args.key_path: + env['key_path'] = args.key_path + + try: + client = Client(env) + check = client.check() + text = "success" if check else "not success" + print(text) + + if check: + for crypto_key in crypto_keys: + if crypto_key not in env: + continue + if not env[crypto_key]: + continue + env[crypto_key] = encoding(env[crypto_key]) + + for (key, value) in env.items(): + os.putenv(key.upper(), value) + os.system('bash') + + except WebDavException as e: + print("not success") + sys.exit() + else: + options = import_options() + if not valid(options): + print("First log on webdav server using the following command: wdc login.") + sys.exit() + + elif action == "logout": + os.system("exit") + + elif action == 'check': + options = import_options() + try: + client = Client(options) + connection = client.check() + if not connection: + raise NotConnection(options["webdav_hostname"]) + check = client.check(args.path) if args.path else client.check() + text = "success" if check else "not success" + print(text) + except WebDavException as e: + logging_exception(e) + + elif action == 'free': + options = import_options() + try: + client = Client(options) + connection = client.check() + if not connection: + raise NotConnection(options["webdav_hostname"]) + free_size = client.free() + print(free_size) + except WebDavException as e: + logging_exception(e) + + elif action == 'ls': + options = import_options() + try: + client = Client(options) + connection = client.check() + if not connection: + raise NotConnection(options["webdav_hostname"]) + paths = client.list(args.path) if args.path else client.list() + for path in paths: + print(path) + except WebDavException as e: + logging_exception(e) + + elif action == 'clean': + if not args.path: + parser.print_help() + else: + options = import_options() + try: + client = Client(options) + connection = client.check() + if not connection: + raise NotConnection(options["webdav_hostname"]) + client.clean(args.path) + except WebDavException as e: + logging_exception(e) + + elif action == 'mkdir': + if not args.path: + parser.print_help() + else: + options = import_options() + try: + client = Client(options) + connection = client.check() + if not connection: + raise NotConnection(options["webdav_hostname"]) + client.mkdir(args.path) + except WebDavException as e: + logging_exception(e) + + elif action == 'copy': + if not args.path or not args.to_path: + parser.print_help() + else: + options = import_options() + try: + client = Client(options) + connection = client.check() + if not connection: + raise NotConnection(options["webdav_hostname"]) + client.copy(remote_path_from=args.path, remote_path_to=args.to_path) + except WebDavException as e: + logging_exception(e) + + elif action == 'move': + if not args.path or not args.to_path: + parser.print_help() + else: + options = import_options() + try: + client = Client(options) + connection = client.check() + if not connection: + raise NotConnection(options["webdav_hostname"]) + client.move(remote_path_from=args.path, remote_path_to=args.to_path) + except WebDavException as e: + logging_exception(e) + + elif action == 'download': + if not args.path or not args.to_path: + parser.print_help() + else: + options = import_options() + progress_bar = ProgressBar() + + def download_progress(download_t, download_d, upload_t, upload_d): + progress_bar.callback(current=download_d, total=download_t) + + try: + client = Client(options) + connection = client.check() + if not connection: + raise NotConnection(options["webdav_hostname"]) + if not os.path.exists(path=args.to_path): + client.download(remote_path=args.path, local_path=args.to_path, progress=download_progress) + print("\n") + else: + try: + choice = raw_input("Local path exists, do you want to overwrite it? [Y/n] ") + except NameError: + choice = input("Local path exists, do you want to overwrite it? [Y/n] ") + try: + yes = strtobool(choice.lower()) + if yes: + client.download(remote_path=args.path, local_path=args.to_path, progress=download_progress) + print("\n") + except ValueError: + print("Incorrect answer") + except WebDavException as e: + logging_exception(e) + + elif action == 'upload': + if not args.path or not args.from_path: + parser.print_help() + else: + options = import_options() + progress_bar = ProgressBar() + + def upload_progress(download_t, download_d, upload_t, upload_d): + progress_bar.callback(current=upload_d, total=upload_t) + + try: + client = Client(options) + connection = client.check() + if not connection: + raise NotConnection(options["webdav_hostname"]) + if not client.check(remote_path=args.path): + client.upload(remote_path=args.path, local_path=args.from_path, progress=upload_progress) + print("\n") + else: + try: + choice = raw_input("Remote resource exists, do you want to overwrite it? [Y/n] ") + except NameError: + choice = input("Remote resource exists, do you want to overwrite it? [Y/n] ") + try: + yes = strtobool(choice.lower()) + if yes: + client.upload(remote_path=args.path, local_path=args.from_path, progress=upload_progress) + print("\n") + except ValueError: + print("Incorrect answer") + except WebDavException as e: + logging_exception(e) + + elif action == 'publish': + if not args.path: + parser.print_help() + else: + options = import_options() + try: + client = Client(options) + connection = client.check() + if not connection: + raise NotConnection(options["webdav_hostname"]) + link = client.publish(args.path) + print(link) + except WebDavException as e: + logging_exception(e) + + elif action == 'unpublish': + if not args.path: + parser.print_help() + else: + options = import_options() + try: + client = Client(options) + connection = client.check() + if not connection: + raise NotConnection(options["webdav_hostname"]) + client.unpublish(args.path) + except WebDavException as e: + logging_exception(e) + + elif action == 'push': + if not args.path or not args.from_path: + parser.print_help() + else: + options = import_options() + try: + client = Client(options) + connection = client.check() + if not connection: + raise NotConnection(options["webdav_hostname"]) + client.push(remote_directory=args.path, local_directory=args.from_path) + except WebDavException as e: + logging_exception(e) + + elif action == 'pull': + if not args.path or not args.to_path: + parser.print_help() + else: + options = import_options() + try: + client = Client(options) + connection = client.check() + if not connection: + raise NotConnection(options["webdav_hostname"]) + client.pull(remote_directory=args.path, local_directory=args.to_path) + except WebDavException as e: + logging_exception(e) + + elif action == 'info': + if not args.path: + parser.print_help() + else: + options = import_options() + try: + client = Client(options) + connection = client.check() + if not connection: + raise NotConnection(options["webdav_hostname"]) + info = client.info(args.path) + print(info) + except WebDavException as e: + logging_exception(e) + + else: + parser.print_help() diff --git a/webdav2/__init__.py b/webdav2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/webdav2/client.py b/webdav2/client.py new file mode 100644 index 0000000..598281a --- /dev/null +++ b/webdav2/client.py @@ -0,0 +1,1096 @@ +# -*- coding: utf-8 + +import functools +import logging +import os +import shutil +import threading +from io import BytesIO +from re import sub + +import requests +import pycurl +import lxml.etree as etree + +from webdav2.connection import * +from webdav2.exceptions import * +from webdav2.urn import Urn + +try: + from urllib.parse import unquote +except ImportError: + from urllib import unquote + +__version__ = "0.1" +log = logging.getLogger(__name__) + + +def listdir(directory): + + file_names = list() + for filename in os.listdir(directory): + file_path = os.path.join(directory, filename) + if os.path.isdir(file_path): + filename = "{filename}{separate}".format(filename=filename, separate=os.path.sep) + file_names.append(filename) + return file_names + + +def add_options(request, options): + + for (key, value) in options.items(): + if value is None: + continue + try: + request.setopt(pycurl.__dict__[key], value) + except TypeError: + raise OptionNotValid(key, value) + except pycurl.error: + raise OptionNotValid(key, value) + + +def get_options(type, from_options): + + _options = dict() + + for key in type.keys: + key_with_prefix = "{prefix}{key}".format(prefix=type.prefix, key=key) + if key not in from_options and key_with_prefix not in from_options: + _options[key] = "" + elif key in from_options: + _options[key] = from_options.get(key) + else: + _options[key] = from_options.get(key_with_prefix) + + return _options + + +def wrap_connection_error(fn): + @functools.wraps(fn) + def _wrapper(self, *args, **kw): + log.debug("Requesting %s(%s, %s)", fn, args, kw) + try: + res = fn(self, *args, **kw) + except requests.ConnectionError: + raise NotConnection(self.webdav.hostname) + else: + return res + return _wrapper + + +class Client(object): + + root = '/' + large_size = 2 * 1024 * 1024 * 1024 + + http_header = { + 'list': ["Accept: */*", "Depth: 1"], + 'free': ["Accept: */*", "Depth: 0", "Content-Type: text/xml"], + 'copy': ["Accept: */*"], + 'move': ["Accept: */*"], + 'mkdir': ["Accept: */*", "Connection: Keep-Alive"], + 'clean': ["Accept: */*", "Connection: Keep-Alive"], + 'check': ["Accept: */*"], + 'info': ["Accept: */*", "Depth: 1"], + 'get_metadata': ["Accept: */*", "Depth: 1", "Content-Type: application/x-www-form-urlencoded"], + 'set_metadata': ["Accept: */*", "Depth: 1", "Content-Type: application/x-www-form-urlencoded"] + } + + def get_header(self, method): + + if method in Client.http_header: + try: + header = Client.http_header[method].copy() + except AttributeError: + header = Client.http_header[method][:] + else: + header = list() + + if self.webdav.token: + webdav_token = "Authorization: OAuth {token}".format(token=self.webdav.token) + header.append(webdav_token) + return dict([map(lambda s: s.strip(), i.split(':')) for i in header]) + + requests = { + 'copy': "COPY", + 'move': "MOVE", + 'mkdir': "MKCOL", + 'clean': "DELETE", + 'check': "HEAD", + 'list': "PROPFIND", + 'free': "PROPFIND", + 'info': "PROPFIND", + 'publish': "PROPPATCH", + 'unpublish': "PROPPATCH", + 'published': "PROPPATCH", + 'get_metadata': "PROPFIND", + 'set_metadata': "PROPPATCH" + } + + meta_xmlns = { + 'https://webdav.yandex.ru': "urn:yandex:disk:meta", + } + + def __init__(self, options): + + webdav_options = get_options(type=WebDAVSettings, from_options=options) + proxy_options = get_options(type=ProxySettings, from_options=options) + + self.webdav = WebDAVSettings(webdav_options) + self.proxy = ProxySettings(proxy_options) + # pycurl.global_init(pycurl.GLOBAL_DEFAULT) + self.default_options = {} + + # def __del__(self): + # pycurl.global_cleanup() + + def valid(self): + return True if self.webdav.valid() and self.proxy.valid() else False + + # def Request(self, options=None): + + # curl = pycurl.Curl() + + # self.default_options.update({ + # 'URL': self.webdav.hostname, + # 'NOBODY': 1, + # 'SSLVERSION': pycurl.SSLVERSION_TLSv1, + # }) + + # if not self.webdav.token: + # server_token = '{login}:{password}'.format(login=self.webdav.login, password=self.webdav.password) + # self.default_options.update({ + # 'USERPWD': server_token, + # }) + + # if self.proxy.valid(): + # if self.proxy.hostname: + # self.default_options['PROXY'] = self.proxy.hostname + + # if self.proxy.login: + # if not self.proxy.password: + # self.default_options['PROXYUSERNAME'] = self.proxy.login + # else: + # proxy_token = '{login}:{password}'.format(login=self.proxy.login, password=self.proxy.password) + # self.default_options['PROXYUSERPWD'] = proxy_token + + # if self.webdav.cert_path: + # self.default_options['SSLCERT'] = self.webdav.cert_path + + # if self.webdav.key_path: + # self.default_options['SSLKEY'] = self.webdav.key_path + + # if self.webdav.recv_speed: + # self.default_options['MAX_RECV_SPEED_LARGE'] = self.webdav.recv_speed + + # if self.webdav.send_speed: + # self.default_options['MAX_SEND_SPEED_LARGE'] = self.webdav.send_speed + + # if self.webdav.verbose: + # self.default_options['VERBOSE'] = self.webdav.verbose + + # if self.default_options: + # add_options(curl, self.default_options) + + # if options: + # add_options(curl, options) + + # return curl + + @wrap_connection_error + def list(self, remote_path=root): + + def parse(response): + + try: + response_str = response.content + tree = etree.fromstring(response_str) + hrees = [unquote(hree.text) for hree in tree.findall(".//{DAV:}href")] + return [Urn(hree) for hree in hrees] + except etree.XMLSyntaxError: + return list() + + directory_urn = Urn(remote_path, directory=True) + + if directory_urn.path() != Client.root: + if not self.check(directory_urn.path()): + raise RemoteResourceNotFound(directory_urn.path()) + + url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': directory_urn.quote()} + options = { + 'URL': "{hostname}{root}{path}".format(**url), + 'CUSTOMREQUEST': Client.requests['list'], + 'HTTPHEADER': self.get_header('list'), + 'NOBODY': 0 + } + + response = requests.request( + options["CUSTOMREQUEST"], + options["URL"], + auth=(self.webdav.login, self.webdav.password), + headers=self.get_header('list') + ) + + urns = parse(response) + + path = "{root}{path}".format(root=self.webdav.root, path=directory_urn.path()) + return [urn.filename() for urn in urns if urn.path() != path and urn.path() != path[:-1]] + + @wrap_connection_error + def free(self): + + def parse(response): + + try: + response_str = response.content + tree = etree.fromstring(response_str) + node = tree.find('.//{DAV:}quota-available-bytes') + if node is not None: + return int(node.text) + else: + raise MethodNotSupported(name='free', server=self.webdav.hostname) + except TypeError: + raise MethodNotSupported(name='free', server=self.webdav.hostname) + except etree.XMLSyntaxError: + return str() + + def data(): + + root = etree.Element("propfind", xmlns="DAV:") + prop = etree.SubElement(root, "prop") + etree.SubElement(prop, "quota-available-bytes") + etree.SubElement(prop, "quota-used-bytes") + tree = etree.ElementTree(root) + buff = BytesIO() + tree.write(buff) + return buff.getvalue() + + options = { + 'CUSTOMREQUEST': Client.requests['free'], + 'HTTPHEADER': self.get_header('free'), + 'POSTFIELDS': data(), + 'NOBODY': 0 + } + + response = requests.request( + options["CUSTOMREQUEST"], + options["URL"], + data=data(), + auth=(self.webdav.login, self.webdav.password), + headers=self.get_header('free') + ) + + return parse(response) + + @wrap_connection_error + def check(self, remote_path=root): + urn = Urn(remote_path) + response = BytesIO() + + url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} + options = { + 'URL': "{hostname}{root}{path}".format(**url), + 'CUSTOMREQUEST': Client.requests['check'], + } + + response = requests.request( + options["CUSTOMREQUEST"], + options["URL"], + auth=(self.webdav.login, self.webdav.password), + headers=self.get_header('check') + ) + + if int(response.status_code) == 200: + return True + return False + + @wrap_connection_error + def mkdir(self, remote_path): + + directory_urn = Urn(remote_path, directory=True) + + if not self.check(directory_urn.parent()): + raise RemoteParentNotFound(directory_urn.path()) + + url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': directory_urn.quote()} + options = { + 'URL': "{hostname}{root}{path}".format(**url), + 'CUSTOMREQUEST': Client.requests['mkdir'], + } + + response = requests.request( + options["CUSTOMREQUEST"], + options["URL"], + auth=(self.webdav.login, self.webdav.password), + headers=self.get_header('mkdir') + ) + # TODO: check response status + + @wrap_connection_error + def download_to(self, buff, remote_path): + urn = Urn(remote_path) + + if self.is_dir(urn.path()): + raise OptionNotValid(name="remote_path", value=remote_path) + + if not self.check(urn.path()): + raise RemoteResourceNotFound(urn.path()) + + url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} + options = { + 'URL': "{hostname}{root}{path}".format(**url), + } + + response = requests.request( + "GET", + options["URL"], + auth=(self.webdav.login, self.webdav.password), + headers=self.get_header('download_to') + ) + buff.write(response.content) + + def download(self, remote_path, local_path, progress=None): + + urn = Urn(remote_path) + if self.is_dir(urn.path()): + self.download_directory(local_path=local_path, remote_path=remote_path, progress=progress) + else: + self.download_file(local_path=local_path, remote_path=remote_path, progress=progress) + + def download_directory(self, remote_path, local_path, progress=None): + + urn = Urn(remote_path, directory=True) + + if not self.is_dir(urn.path()): + raise OptionNotValid(name="remote_path", value=remote_path) + + if os.path.exists(local_path): + shutil.rmtree(local_path) + + os.makedirs(local_path) + + for resource_name in self.list(urn.path()): + _remote_path = "{parent}{name}".format(parent=urn.path(), name=resource_name) + _local_path = os.path.join(local_path, resource_name) + self.download(local_path=_local_path, remote_path=_remote_path, progress=progress) + + @wrap_connection_error + def download_file(self, remote_path, local_path): + + urn = Urn(remote_path) + + if self.is_dir(urn.path()): + raise OptionNotValid(name="remote_path", value=remote_path) + + if os.path.isdir(local_path): + raise OptionNotValid(name="local_path", value=local_path) + + if not self.check(urn.path()): + raise RemoteResourceNotFound(urn.path()) + + with open(local_path, 'wb') as local_file: + + url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} + options = { + 'URL': "{hostname}{root}{path}".format(**url), + 'HTTPHEADER': self.get_header('download_file'), + 'WRITEDATA': local_file, + 'NOBODY': 0 + } + + response = requests.request( + "GET", + options["URL"], + auth=(self.webdav.login, self.webdav.password), + headers=self.get_header('download_file') + ) + for block in response.iter_content(1024): + local_file.write(block) + + def download_sync(self, remote_path, local_path, callback=None): + + self.download(local_path=local_path, remote_path=remote_path) + + if callback: + callback() + + # def download_async(self, remote_path, local_path, callback=None): + + # target = (lambda: self.download_sync(local_path=local_path, remote_path=remote_path, callback=callback)) + # threading.Thread(target=target).start() + + @wrap_connection_error + def upload_from(self, buff, remote_path): + + urn = Urn(remote_path) + + if urn.is_dir(): + raise OptionNotValid(name="remote_path", value=remote_path) + + if not self.check(urn.parent()): + raise RemoteParentNotFound(urn.path()) + + url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} + options = { + 'URL': "{hostname}{root}{path}".format(**url), + } + # import ipdb; ipdb.set_trace(); + #FIXME + if buff.tell() == 0: + data = buff.read() + else: + data = buff + response = requests.request( + 'PUT', + options["URL"], + auth=(self.webdav.login, self.webdav.password), + headers=self.get_header('upload_from'), + data=data + ) + if response.status_code == 507: + raise NotEnoughSpace() + log.debug("Response: %s", response) + + def upload(self, remote_path, local_path, progress=None): + + if os.path.isdir(local_path): + self.upload_directory(local_path=local_path, remote_path=remote_path, progress=progress) + else: + self.upload_file(local_path=local_path, remote_path=remote_path, progress=progress) + + def upload_directory(self, remote_path, local_path, progress=None): + + urn = Urn(remote_path, directory=True) + + if not urn.is_dir(): + raise OptionNotValid(name="remote_path", value=remote_path) + + if not os.path.isdir(local_path): + raise OptionNotValid(name="local_path", value=local_path) + + if not os.path.exists(local_path): + raise LocalResourceNotFound(local_path) + + if self.check(urn.path()): + self.clean(urn.path()) + + self.mkdir(remote_path) + + for resource_name in listdir(local_path): + _remote_path = "{parent}{name}".format(parent=urn.path(), name=resource_name) + _local_path = os.path.join(local_path, resource_name) + self.upload(local_path=_local_path, remote_path=_remote_path, progress=progress) + + @wrap_connection_error + def upload_file(self, remote_path, local_path, progress=None): + + if not os.path.exists(local_path): + raise LocalResourceNotFound(local_path) + + urn = Urn(remote_path) + + if urn.is_dir(): + raise OptionNotValid(name="remote_path", value=remote_path) + + if os.path.isdir(local_path): + raise OptionNotValid(name="local_path", value=local_path) + + if not self.check(urn.parent()): + raise RemoteParentNotFound(urn.path()) + + with open(local_path, "rb") as local_file: + + url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} + options = { + 'URL': "{hostname}{root}{path}".format(**url), + 'HTTPHEADER': self.get_header('upload_file'), + 'UPLOAD': 1, + 'READFUNCTION': local_file.read, + # 'NOPROGRESS': 0 if progress else 1 + } + + # if progress: + # options["PROGRESSFUNCTION"] = progress + + file_size = os.path.getsize(local_path) + if file_size > self.large_size: + options['INFILESIZE_LARGE'] = file_size + else: + options['INFILESIZE'] = file_size + + response = requests.request( + 'PUT', + options["URL"], + auth=(self.webdav.login, self.webdav.password), + headers=self.get_header('upload_file'), + data=buff + ) + if request.status_code == 507: + raise NotEnoughSpace() + + # request.close() + + def upload_sync(self, remote_path, local_path, callback=None): + + self.upload(local_path=local_path, remote_path=remote_path) + + if callback: + callback() + + # def upload_async(self, remote_path, local_path, callback=None): + + # target = (lambda: self.upload_sync(local_path=local_path, remote_path=remote_path, callback=callback)) + # threading.Thread(target=target).start() + + @wrap_connection_error + def copy(self, remote_path_from, remote_path_to): + + def header(remote_path_to): + + path = Urn(remote_path_to).path() + destination = "{root}{path}".format(root=self.webdav.root, path=path) + header = self.get_header('copy') + header["Destination"] = destination + + return header + + urn_from = Urn(remote_path_from) + + if not self.check(urn_from.path()): + raise RemoteResourceNotFound(urn_from.path()) + + urn_to = Urn(remote_path_to) + + if not self.check(urn_to.parent()): + raise RemoteParentNotFound(urn_to.path()) + + url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn_from.quote()} + options = { + 'URL': "{hostname}{root}{path}".format(**url), + 'CUSTOMREQUEST': Client.requests['copy'], + 'HTTPHEADER': header(remote_path_to) + } + + h = header(remote_path_to) + response = requests.request( + options["CUSTOMREQUEST"], + options["URL"], + auth=(self.webdav.login, self.webdav.password), + headers=h, + ) + # TODO: check response status + + @wrap_connection_error + def move(self, remote_path_from, remote_path_to, overwrite=False): + + def header(remote_path_to): + + path = Urn(remote_path_to).path() + destination = "{root}{path}".format(root=self.webdav.root, path=path) + header = self.get_header('move') + header["Destination"] = destination + header["Overwrite"] = "T" if overwrite else "F" + return header + + urn_from = Urn(remote_path_from) + + if not self.check(urn_from.path()): + raise RemoteResourceNotFound(urn_from.path()) + + urn_to = Urn(remote_path_to) + + if not self.check(urn_to.parent()): + raise RemoteParentNotFound(urn_to.path()) + + url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn_from.quote()} + options = { + 'URL': "{hostname}{root}{path}".format(**url), + 'CUSTOMREQUEST': Client.requests['move'], + } + h = header(remote_path_to) + response = requests.request( + options["CUSTOMREQUEST"], + options["URL"], + auth=(self.webdav.login, self.webdav.password), + headers=h, + ) + # TODO: check response status + + @wrap_connection_error + def clean(self, remote_path): + + urn = Urn(remote_path) + + url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} + options = { + 'URL': "{hostname}{root}{path}".format(**url), + 'CUSTOMREQUEST': Client.requests['clean'], + 'HTTPHEADER': self.get_header('clean') + } + + response = requests.request( + options["CUSTOMREQUEST"], + options["URL"], + auth=(self.webdav.login, self.webdav.password), + headers=self.get_header('clean'), + ) + # TODO: check response status + + # def publish(self, remote_path): + + # def parse(response): + + # try: + # response_str = response.content + # tree = etree.fromstring(response_str) + # result = tree.xpath("//*[local-name() = 'public_url']") + # public_url = result[0] + # return public_url.text + # except IndexError: + # raise MethodNotSupported(name="publish", server=self.webdav.hostname) + # except etree.XMLSyntaxError: + # return "" + + # def data(for_server): + + # root_node = etree.Element("propertyupdate", xmlns="DAV:") + # set_node = etree.SubElement(root_node, "set") + # prop_node = etree.SubElement(set_node, "prop") + # xmlns = Client.meta_xmlns.get(for_server, "") + # public_url = etree.SubElement(prop_node, "public_url", xmlns=xmlns) + # public_url.text = "true" + # tree = etree.ElementTree(root_node) + + # buff = BytesIO() + # tree.write(buff) + + # return buff.getvalue() + + # try: + # urn = Urn(remote_path) + + # if not self.check(urn.path()): + # raise RemoteResourceNotFound(urn.path()) + + # response = BytesIO() + + # url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} + # options = { + # 'URL': "{hostname}{root}{path}".format(**url), + # 'CUSTOMREQUEST': Client.requests['publish'], + # 'HTTPHEADER': self.get_header('publish'), + # 'POSTFIELDS': data(for_server=self.webdav.hostname), + # 'WRITEDATA': response, + # 'NOBODY': 0 + # } + + # request = self.Request(options=options) + + # request.perform() + # request.close() + + # return parse(response) + + # except pycurl.error: + # raise NotConnection(self.webdav.hostname) + + # def unpublish(self, remote_path): + + # def data(for_server): + + # root = etree.Element("propertyupdate", xmlns="DAV:") + # remove = etree.SubElement(root, "remove") + # prop = etree.SubElement(remove, "prop") + # xmlns = Client.meta_xmlns.get(for_server, "") + # etree.SubElement(prop, "public_url", xmlns=xmlns) + # tree = etree.ElementTree(root) + + # buff = BytesIO() + # tree.write(buff) + + # return buff.getvalue() + + # try: + # urn = Urn(remote_path) + + # if not self.check(urn.path()): + # raise RemoteResourceNotFound(urn.path()) + + # url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} + # options = { + # 'URL': "{hostname}{root}{path}".format(**url), + # 'CUSTOMREQUEST': Client.requests['unpublish'], + # 'HTTPHEADER': self.get_header('unpublish'), + # 'POSTFIELDS': data(for_server=self.webdav.hostname) + # } + + # request = self.Request(options=options) + + # request.perform() + # request.close() + + # except pycurl.error: + # raise NotConnection(self.webdav.hostname) + + @wrap_connection_error + def info(self, remote_path): + + def parse(response, path): + + try: + response_str = response.content + tree = etree.fromstring(response_str) + + find_attributes = { + 'created': ".//{DAV:}creationdate", + 'name': ".//{DAV:}displayname", + 'size': ".//{DAV:}getcontentlength", + 'modified': ".//{DAV:}getlastmodified" + } + + resps = tree.findall("{DAV:}response") + + for resp in resps: + href = resp.findtext("{DAV:}href") + urn = unquote(href) + + if path[-1] == Urn.separate: + if not path == urn: + continue + else: + path_with_sep = "{path}{sep}".format(path=path, sep=Urn.separate) + if not path == urn and not path_with_sep == urn: + continue + + info = dict() + for (name, value) in find_attributes.items(): + info[name] = resp.findtext(value) + return info + + raise RemoteResourceNotFound(path) + except etree.XMLSyntaxError: + raise MethodNotSupported(name="info", server=self.webdav.hostname) + + urn = Urn(remote_path) + # response = BytesIO() + + if not self.check(urn.path()) and not self.check(Urn(remote_path, directory=True).path()): + raise RemoteResourceNotFound(remote_path) + + url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} + options = { + 'URL': "{hostname}{root}{path}".format(**url), + 'CUSTOMREQUEST': Client.requests['info'], + } + + response = requests.request( + options["CUSTOMREQUEST"], + options["URL"], + auth=(self.webdav.login, self.webdav.password), + headers=self.get_header('info') + ) + path = "{root}{path}".format(root=self.webdav.root, path=urn.path()) + return parse(response, path) + + @wrap_connection_error + def is_dir(self, remote_path): + + def parse(response, path): + + try: + response_str = response.content + tree = etree.fromstring(response_str) + + resps = tree.findall("{DAV:}response") + + for resp in resps: + href = resp.findtext("{DAV:}href") + urn = unquote(href) + + if path[-1] == Urn.separate: + if not path == urn: + continue + else: + path_with_sep = "{path}{sep}".format(path=path, sep=Urn.separate) + if not path == urn and not path_with_sep == urn: + continue + type = resp.find(".//{DAV:}resourcetype") + if type is None: + raise MethodNotSupported(name="is_dir", server=self.webdav.hostname) + dir_type = type.find("{DAV:}collection") + + return True if dir_type is not None else False + + raise RemoteResourceNotFound(path) + + except etree.XMLSyntaxError: + raise MethodNotSupported(name="is_dir", server=self.webdav.hostname) + + 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) + + response = BytesIO() + + url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': parent_urn.quote()} + options = { + 'URL': "{hostname}{root}{path}".format(**url), + 'CUSTOMREQUEST': Client.requests['info'], + } + + response = requests.request( + options["CUSTOMREQUEST"], + options["URL"], + auth=(self.webdav.login, self.webdav.password), + headers=self.get_header('info') + ) + + path = "{root}{path}".format(root=self.webdav.root, path=urn.path()) + + return parse(response, path) + + def resource(self, remote_path): + urn = Urn(remote_path) + return Resource(self, urn.path()) + + @wrap_connection_error + def get_property(self, remote_path, option): + + def parse(response, option): + + response_str = response.content + tree = etree.fromstring(response_str) + xpath = "{xpath_prefix}{xpath_exp}".format(xpath_prefix=".//", xpath_exp=option['name']) + return tree.findtext(xpath) + + def data(option): + + root = etree.Element("propfind", xmlns="DAV:") + prop = etree.SubElement(root, "prop") + etree.SubElement(prop, option.get('name', ""), xmlns=option.get('namespace', "")) + tree = etree.ElementTree(root) + + buff = BytesIO() + + tree.write(buff) + return buff.getvalue() + + urn = Urn(remote_path) + + if not self.check(urn.path()): + raise RemoteResourceNotFound(urn.path()) + + url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} + options = { + 'URL': "{hostname}{root}{path}".format(**url), + 'CUSTOMREQUEST': Client.requests['get_metadata'], + 'HTTPHEADER': self.get_header('get_metadata'), + 'POSTFIELDS': data(option), + 'NOBODY': 0 + } + + response = requests.request( + options["CUSTOMREQUEST"], + options["URL"], + auth=(self.webdav.login, self.webdav.password), + headers=self.get_header('get_metadata'), + data=data(option) + ) + return parse(response, option) + + @wrap_connection_error + def set_property(self, remote_path, option): + + def data(option): + + root_node = etree.Element("propertyupdate", xmlns="DAV:") + root_node.set('xmlns:u', option.get('namespace', "")) + set_node = etree.SubElement(root_node, "set") + prop_node = etree.SubElement(set_node, "prop") + opt_node = etree.SubElement(prop_node, "{namespace}:{name}".format(namespace='u', name=option['name'])) + opt_node.text = option.get('value', "") + + tree = etree.ElementTree(root_node) + + buff = BytesIO() + tree.write(buff) + + return buff.getvalue() + + urn = Urn(remote_path) + + if not self.check(urn.path()): + raise RemoteResourceNotFound(urn.path()) + + url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} + options = { + 'URL': "{hostname}{root}{path}".format(**url), + 'CUSTOMREQUEST': Client.requests['set_metadata'], + 'HTTPHEADER': self.get_header('get_metadata'), + 'POSTFIELDS': data(option) + } + + response = requests.request( + options["CUSTOMREQUEST"], + options["URL"], + auth=(self.webdav.login, self.webdav.password), + headers=self.get_header('get_metadata'), + data=data(option) + ) + + def push(self, remote_directory, local_directory): + + def prune(src, exp): + return [sub(exp, "", item) for item in src] + + 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) + + 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) + + 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) + else: + if local_resource_name in remote_resource_names: + continue + self.upload_file(remote_path=remote_path, local_path=local_path) + + def pull(self, remote_directory, local_directory): + + def prune(src, exp): + return [sub(exp, "", item) for item in src] + + 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) + + local_resource_names = listdir(local_directory) + + paths = self.list(urn.path()) + expression = "{begin}{end}".format(begin="^", end=remote_directory) + 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_urn = Urn(remote_path) + + if self.is_dir(remote_urn.path()): + if not os.path.exists(local_path): + os.mkdir(local_path) + 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) + + 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) + + +class Resource(object): + + def __init__(self, client, urn): + self.client = client + self.urn = urn + + def __str__(self): + return "resource {path}".format(path=self.urn.path()) + + def is_dir(self): + return self.client.is_dir(self.urn.path()) + + def rename(self, new_name): + + old_path = self.urn.path() + parent_path = self.urn.parent() + new_name = Urn(new_name).filename() + new_path = "{directory}{filename}".format(directory=parent_path, filename=new_name) + + self.client.move(remote_path_from=old_path, remote_path_to=new_path) + self.urn = Urn(new_path) + + def move(self, remote_path): + + new_urn = Urn(remote_path) + self.client.move(remote_path_from=self.urn.path(), remote_path_to=new_urn.path()) + self.urn = new_urn + + def copy(self, remote_path): + + urn = Urn(remote_path) + self.client.copy(remote_path_from=self.urn.path(), remote_path_to=remote_path) + return Resource(self.client, urn) + + def info(self, params=None): + + info = self.client.info(self.urn.path()) + if not params: + return info + + return {key: value for (key, value) in info.items() if key in params} + + def clean(self): + return self.client.clean(self.urn.path()) + + def check(self): + return self.client.check(self.urn.path()) + + def read_from(self, buff): + self.client.upload_from(buff=buff, remote_path=self.urn.path()) + + def read(self, local_path): + return self.client.upload_sync(local_path=local_path, remote_path=self.urn.path()) + + def read_async(self, local_path, callback=None): + return self.client.upload_async(local_path=local_path, remote_path=self.urn.path(), callback=callback) + + def write_to(self, buff): + return self.client.download_to(buff=buff, remote_path=self.urn.path()) + + def write(self, local_path): + return self.client.download_sync(local_path=local_path, remote_path=self.urn.path()) + + def write_async(self, local_path, callback=None): + return self.client.download_async(local_path=local_path, remote_path=self.urn.path(), callback=callback) + + def publish(self): + return self.client.publish(self.urn.path()) + + def unpublish(self): + return self.client.unpublish(self.urn.path()) + + @property + def property(self, option): + return self.client.get_property(remote_path=self.urn.path(), option=option) + + @property.setter + def property(self, option, value): + option['value'] = value.__str__() + self.client.set_property(remote_path=self.urn.path(), option=option) diff --git a/webdav2/connection.py b/webdav2/connection.py new file mode 100644 index 0000000..fc1b729 --- /dev/null +++ b/webdav2/connection.py @@ -0,0 +1,81 @@ + +from webdav2.exceptions import * +from webdav2.urn import Urn +from os.path import exists + +class ConnectionSettings: + + def is_valid(self): + pass + + def valid(self): + + try: + self.is_valid() + except OptionNotValid: + return False + else: + return True + +class WebDAVSettings(ConnectionSettings): + + ns = "webdav:" + prefix = "webdav_" + keys = {'hostname', 'login', 'password', 'token', 'root', 'cert_path', 'key_path', 'recv_speed', 'send_speed', 'verbose'} + + def __init__(self, options): + + self.options = dict() + + for key in self.keys: + value = options.get(key, '') + self.options[key] = value + self.__dict__[key] = value + + self.root = Urn(self.root).quote() if self.root else '' + 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) + + if self.cert_path and not exists(self.cert_path): + raise OptionNotValid(name="cert_path", value=self.cert_path, ns=self.ns) + + if self.key_path and not exists(self.key_path): + raise OptionNotValid(name="key_path", value=self.key_path, ns=self.ns) + + if self.key_path and not self.cert_path: + raise OptionNotValid(name="cert_path", value=self.cert_path, ns=self.ns) + + if self.password and not self.login: + raise OptionNotValid(name="login", value=self.login, ns=self.ns) + + 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'} + + 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) diff --git a/webdav2/exceptions.py b/webdav2/exceptions.py new file mode 100644 index 0000000..571ba1d --- /dev/null +++ b/webdav2/exceptions.py @@ -0,0 +1,73 @@ +class WebDavException(Exception): + pass + + +class NotValid(WebDavException): + pass + + +class OptionNotValid(NotValid): + def __init__(self, name, value, ns=""): + self.name = name + self.value = value + self.ns = ns + + def __str__(self): + return "Option ({ns}{name}={value}) have invalid name or value".format(ns=self.ns, name=self.name, value=self.value) + + +class CertificateNotValid(NotValid): + pass + + +class NotFound(WebDavException): + pass + + +class LocalResourceNotFound(NotFound): + def __init__(self, path): + self.path = path + + def __str__(self): + return "Local file: {path} not found".format(path=self.path) + + +class RemoteResourceNotFound(NotFound): + def __init__(self, path): + self.path = path + + def __str__(self): + return "Remote resource: {path} not found".format(path=self.path) + + +class RemoteParentNotFound(NotFound): + def __init__(self, path): + self.path = path + + def __str__(self): + return "Remote parent for: {path} not found".format(path=self.path) + + +class MethodNotSupported(WebDavException): + def __init__(self, name, server): + self.name = name + self.server = server + + def __str__(self): + return "Method {name} not supported for {server}".format(name=self.name, server=self.server) + + +class NotConnection(WebDavException): + def __init__(self, hostname): + self.hostname = hostname + + def __str__(self): + return "Not connection with {hostname}".format(hostname=self.hostname) + + +class NotEnoughSpace(WebDavException): + def __init__(self): + pass + + def __str__(self): + return "Not enough space on the server" \ No newline at end of file diff --git a/webdav2/urn.py b/webdav2/urn.py new file mode 100644 index 0000000..4d6b869 --- /dev/null +++ b/webdav2/urn.py @@ -0,0 +1,56 @@ +try: + from urllib.parse import unquote, quote +except ImportError: + from urllib import unquote, quote + +from re import sub + + +class Urn(object): + + separate = "/" + + def __init__(self, path, directory=False): + + self._path = quote(path) + expressions = "/\.+/", "/+" + for expression in expressions: + self._path = sub(expression, Urn.separate, self._path) + + if not self._path.startswith(Urn.separate): + self._path = "{begin}{end}".format(begin=Urn.separate, end=self._path) + + if directory and not self._path.endswith(Urn.separate): + self._path = "{begin}{end}".format(begin=self._path, end=Urn.separate) + + def __str__(self): + return self.path() + + def path(self): + return unquote(self._path) + + def quote(self): + 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] + parent = self.separate.join(parent_path_split) if nesting_level != 1 else Urn.separate + if not parent.endswith(Urn.separate): + return unquote(parent + Urn.separate) + else: + return unquote(parent) + + def nesting_level(self): + return self._path.count(Urn.separate, 0, -1) + + def is_dir(self): + return self._path[-1] == Urn.separate \ No newline at end of file