├── .gitignore ├── LICENSE.md ├── README.md ├── README.rst ├── TESTING.md ├── THANKS.md ├── setup.py ├── tests ├── conftest.py └── webdav │ ├── test_authenticate.py │ ├── test_connection.py │ └── test_methods.py ├── tox.ini ├── wdc └── webdav ├── __init__.py ├── client.py ├── connection.py ├── exceptions.py └── urn.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /**/*.pyc 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | COPYRIGHT AND PERMISSION NOTICE 2 | 3 | Copyright (c) 2016, The WDC Project, and many 4 | contributors, see the THANKS file. 5 | 6 | All rights reserved. 7 | 8 | Permission to use, copy, modify, and distribute this software for any purpose 9 | with or without fee is hereby granted, provided that the above copyright 10 | notice and this permission notice appear in all copies. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN 15 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 16 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 17 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 18 | OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | Except as contained in this notice, the name of a copyright holder shall not 21 | be used in advertising or otherwise to promote the sale, use or other dealings 22 | in this Software without prior written authorization of the copyright holder. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | webdavclient 2 | ============ 3 | 4 | [![PyPI 5 | version](https://badge.fury.io/py/webdavclient.svg)](http://badge.fury.io/py/webdavclient) 6 | [![Requirements 7 | 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) 8 | [![PullReview 9 | stats](https://www.pullreview.com/github/designerror/webdavclient/badges/master.svg?)](https://www.pullreview.com/github/designerror/webdavclient/reviews/master) 10 | 11 | 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. 12 | 13 | The source code of the project can be found 14 | [here](https://github.com/designerror/webdavclient) 15 | ![Github](https://github.com/favicon.ico) 16 | 17 | Installation and upgrade 18 | ====================== 19 | 20 | **Installation** 21 | 22 | > Linux 23 | 24 | ```bash 25 | $ sudo apt-get install libxml2-dev libxslt-dev python-dev 26 | $ sudo apt-get install libcurl4-openssl-dev python-pycurl 27 | $ sudo easy_install webdavclient 28 | ``` 29 | 30 | > macOS 31 | 32 | ```bash 33 | $ curl https://bootstrap.pypa.io/ez_setup.py -o - | python 34 | $ python setup.py install --prefix=/opt/setuptools 35 | $ sudo easy_install webdavclient 36 | ``` 37 | 38 | **Update** 39 | 40 | ```bash 41 | $ sudo pip install -U webdavclient 42 | ``` 43 | 44 | Webdav API 45 | ========== 46 | 47 | 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`. 48 | 49 | **Configuring the client** 50 | 51 | Required keys for configuring client connection with WevDAV-server are webdav\_hostname and webdav\_login, webdav,\_password. 52 | 53 | ```python 54 | import webdav.client as wc 55 | options = { 56 | 'webdav_hostname': "https://webdav.server.ru", 57 | 'webdav_login': "login", 58 | 'webdav_password': "password" 59 | } 60 | client = wc.Client(options) 61 | ``` 62 | 63 | When a proxy server you need to specify settings to connect through it. 64 | 65 | ```python 66 | import webdav.client as wc 67 | options = { 68 | 'webdav_hostname': "https://webdav.server.ru", 69 | 'webdav_login': "w_login", 70 | 'webdav_password': "w_password", 71 | 'proxy_hostname': "http://127.0.0.1:8080", 72 | 'proxy_login': "p_login", 73 | 'proxy_password': "p_password" 74 | } 75 | client = wc.Client(options) 76 | ``` 77 | 78 | If you want to use the certificate path to certificate and private key is defined as follows: 79 | 80 | ```python 81 | import webdav.client as wc 82 | options = { 83 | 'webdav_hostname': "https://webdav.server.ru", 84 | 'webdav_login': "w_login", 85 | 'webdav_password': "w_password", 86 | 'cert_path': "/etc/ssl/certs/certificate.crt", 87 | 'key_path': "/etc/ssl/private/certificate.key" 88 | } 89 | client = wc.Client(options) 90 | ``` 91 | 92 | Or you want to limit the speed or turn on verbose mode: 93 | 94 | ```python 95 | options = { 96 | ... 97 | 'recv_speed' : 3000000, 98 | 'send_speed' : 3000000, 99 | 'verbose' : True 100 | } 101 | client = wc.Client(options) 102 | ``` 103 | 104 | recv_speed: rate limit data download speed in Bytes per second. Defaults to unlimited speed. 105 | send_speed: rate limit data upload speed in Bytes per second. Defaults to unlimited speed. 106 | verbose: set verbose mode on/off. By default verbose mode is off. 107 | 108 | **Synchronous methods** 109 | 110 | ```python 111 | # Checking existence of the resource 112 | 113 | client.check("dir1/file1") 114 | client.check("dir1") 115 | ``` 116 | 117 | ```python 118 | # Get information about the resource 119 | 120 | client.info("dir1/file1") 121 | client.info("dir1/") 122 | ``` 123 | 124 | ```python 125 | # Check free space 126 | 127 | free_size = client.free() 128 | ``` 129 | 130 | ```python 131 | # Get a list of resources 132 | 133 | files1 = client.list() 134 | files2 = client.list("dir1") 135 | ``` 136 | 137 | ```python 138 | # Create directory 139 | 140 | client.mkdir("dir1/dir2") 141 | ``` 142 | 143 | ```python 144 | # Delete resource 145 | 146 | client.clean("dir1/dir2") 147 | ``` 148 | 149 | ```python 150 | # Copy resource 151 | 152 | client.copy(remote_path_from="dir1/file1", remote_path_to="dir2/file1") 153 | client.copy(remote_path_from="dir2", remote_path_to="dir3") 154 | ``` 155 | 156 | ```python 157 | # Move resource 158 | 159 | client.move(remote_path_from="dir1/file1", remote_path_to="dir2/file1") 160 | client.move(remote_path_from="dir2", remote_path_to="dir3") 161 | ``` 162 | 163 | ```python 164 | # Move resource 165 | 166 | client.download_sync(remote_path="dir1/file1", local_path="~/Downloads/file1") 167 | client.download_sync(remote_path="dir1/dir2/", local_path="~/Downloads/dir2/") 168 | ``` 169 | 170 | ```python 171 | # Unload resource 172 | 173 | client.upload_sync(remote_path="dir1/file1", local_path="~/Documents/file1") 174 | client.upload_sync(remote_path="dir1/dir2/", local_path="~/Documents/dir2/") 175 | ``` 176 | 177 | ```python 178 | # Publish the resource 179 | 180 | link = client.publish("dir1/file1") 181 | link = client.publish("dir2") 182 | ``` 183 | 184 | ```python 185 | # Unpublish resource 186 | 187 | client.unpublish("dir1/file1") 188 | client.unpublish("dir2") 189 | ``` 190 | 191 | ```python 192 | # Exception handling 193 | 194 | from webdav.client import WebDavException 195 | try: 196 | ... 197 | except WebDavException as exception: 198 | ... 199 | ``` 200 | 201 | ```python 202 | # Get the missing files 203 | 204 | client.pull(remote_directory='dir1', local_directory='~/Documents/dir1') 205 | ``` 206 | 207 | ```python 208 | # Send missing files 209 | 210 | client.push(remote_directory='dir1', local_directory='~/Documents/dir1') 211 | ``` 212 | 213 | **Asynchronous methods** 214 | 215 | ```python 216 | # Load resource 217 | 218 | kwargs = { 219 | 'remote_path': "dir1/file1", 220 | 'local_path': "~/Downloads/file1", 221 | 'callback': callback 222 | } 223 | client.download_async(**kwargs) 224 | 225 | kwargs = { 226 | 'remote_path': "dir1/dir2/", 227 | 'local_path': "~/Downloads/dir2/", 228 | 'callback': callback 229 | } 230 | client.download_async(**kwargs) 231 | ``` 232 | 233 | ```python 234 | # Unload resource 235 | 236 | kwargs = { 237 | 'remote_path': "dir1/file1", 238 | 'local_path': "~/Downloads/file1", 239 | 'callback': callback 240 | } 241 | client.upload_async(**kwargs) 242 | 243 | kwargs = { 244 | 'remote_path': "dir1/dir2/", 245 | 'local_path': "~/Downloads/dir2/", 246 | 'callback': callback 247 | } 248 | client.upload_async(**kwargs) 249 | ``` 250 | 251 | Resource API 252 | ============ 253 | 254 | Resource API using the concept of OOP that enables cloud-level resources. 255 | 256 | ```python 257 | # Get a resource 258 | 259 | res1 = client.resource("dir1/file1") 260 | ``` 261 | 262 | ```python 263 | # Work with the resource 264 | 265 | res1.rename("file2") 266 | res1.move("dir1/file2") 267 | res1.copy("dir2/file1") 268 | info = res1.info() 269 | res1.read_from(buffer) 270 | res1.read(local_path="~/Documents/file1") 271 | res1.read_async(local_path="~/Documents/file1", callback) 272 | res1.write_to(buffer) 273 | res1.write(local_path="~/Downloads/file1") 274 | res1.write_async(local_path="~/Downloads/file1", callback) 275 | ``` 276 | 277 | wdc 278 | === 279 | 280 | 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. 281 | 282 | **Authentication** 283 | 284 | - *Basic authentication* 285 | ```bash 286 | $ wdc login https://wedbav.server.ru -p http://127.0.0.1:8080 287 | webdav_login: w_login 288 | webdav_password: w_password 289 | proxy_login: p_login 290 | proxy_password: p_password 291 | success 292 | ``` 293 | 294 | - Authorize the application using OAuth token* 295 | ```bash 296 | $ wdc login https://wedbav.server.ru -p http://127.0.0.1:8080 --token xxxxxxxxxxxxxxxxxx 297 | proxy_login: p_login 298 | proxy_password: p_password 299 | success 300 | ``` 301 | 302 | There are also additional keys `--root[-r]`, `--cert-path[-c]` and `--key-path[-k]`. 303 | 304 | **Utility** 305 | 306 | ```bash 307 | $ wdc check 308 | success 309 | $ wdc check file1 310 | not success 311 | $ wdc free 312 | 245234120344 313 | $ wdc ls dir1 314 | file1 315 | ... 316 | fileN 317 | $ wdc mkdir dir2 318 | $ wdc copy dir1/file1 -t dir2/file1 319 | $ wdc move dir2/file1 -t dir2/file2 320 | $ wdc download dir1/file1 -t ~/Downloads/file1 321 | $ wdc download dir1/ -t ~/Downloads/dir1/ 322 | $ wdc upload dir2/file2 -f ~/Documents/file1 323 | $ wdc upload dir2/ -f ~/Documents/ 324 | $ wdc publish di2/file2 325 | https://yadi.sk/i/vWtTUcBucAc6k 326 | $ wdc unpublish dir2/file2 327 | $ wdc pull dir1/ -t ~/Documents/dir1/ 328 | $ wdc push dir1/ -f ~/Documents/dir1/ 329 | $ wdc info dir1/file1 330 | {'name': 'file1', 'modified': 'Thu, 23 Oct 2014 16:16:37 GMT', 331 | 'size': '3460064', 'created': '2014-10-23T16:16:37Z'} 332 | ``` 333 | WebDAV-server 334 | ============= 335 | 336 | 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. 337 | 338 | **Local WebDAV-server** 339 | 340 | To deploy a local WebDAV server, using Docker containers 341 | 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). 342 | 343 | **Supported methods** 344 | 345 | Servers |free|info|list|mkdir|clean|copy|move|download|upload 346 | :------------|:--:|:--:|:--:|:---:|:---:|:--:|:--:|:------:|:----: 347 | Yandex.Disk| \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ 348 | Dropbox| \- | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ 349 | Google Drive| \- | \+ | \+ | \+ | \+ | \- | \- | \+ | \+ 350 | Box| \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ 351 | 4shared| \- | \+ | \+ | \+ | \+ | \- | \- | \+ | \+ 352 | Webdavserver| \- | \+ | \+ | \+ | \+ | \- | \- | \+ | \+ 353 | 354 | Publish and unpublish methods supports only Yandex.Disk. 355 | 356 | **Configuring connections** 357 | 358 | To work with cloud storage Dropbox and Google Drive via the WebDAV Protocol, you must use a WebDAV-server DropDAV and DAV-pocket, respectively. 359 | 360 | A list of settings for WebDAV servers: 361 | 362 | ```yaml 363 | webdav-servers: 364 | - yandex 365 | hostname: https://webdav.yandex.ru 366 | login: #login_for_yandex 367 | password: #pass_for_yandex 368 | - dropbox 369 | hostname: https://dav.dropdav.com 370 | login: #login_for dropdav 371 | password: #pass_for_dropdav 372 | - google 373 | hostname: https://dav-pocket.appspot.com 374 | root: docso 375 | login: #login_for_dav-pocket 376 | password: #pass_for_dav-pocket 377 | - box 378 | hostname: https://dav.box.com 379 | root: dav 380 | login: #login_for_box 381 | password: #pass_for_box 382 | - 4shared 383 | hostname: https://webdav.4shared.com 384 | login: #login_for_4shared 385 | password: #pass_for_4shared 386 | ``` 387 | 388 | Autocompletion 389 | ============== 390 | 391 | For macOS, or older Unix systems you need to update bash. 392 | 393 | ```bash 394 | $ brew install bash 395 | $ chsh 396 | $ brew install bash-completion 397 | ``` 398 | 399 | Autocompletion can be enabled globally 400 | 401 | ```bash 402 | $ sudo activate-global-python-argcomplete 403 | ``` 404 | 405 | or locally 406 | 407 | ```bash 408 | #.bashrc 409 | eval "$(register-python-argcomplete wdc)" 410 | ``` 411 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | webdavclient 2 | ============ 3 | 4 | |PyPI version| |Requirements Status| |PullReview stats| 5 | 6 | Package webdavclient provides easy and convenient work with 7 | WebDAV-servers (Yandex.Drive, Dropbox, Google Drive, Box, 4shared, 8 | etc.). The package includes the following components: webdav API, 9 | resource API and wdc. 10 | 11 | The source code of the project can be found 12 | `here `__ |Github| 13 | 14 | Installation and upgrade 15 | ======================== 16 | 17 | **Installation** 18 | 19 | - Linux 20 | 21 | .. code:: bash 22 | 23 | $ sudo apt-get install libxml2-dev libxslt-dev python-dev 24 | $ sudo apt-get install libcurl4-openssl-dev python-pycurl 25 | $ sudo easy_install webdavclient 26 | 27 | - macOS 28 | 29 | .. code:: bash 30 | 31 | $ curl https://bootstrap.pypa.io/ez_setup.py -o - | python 32 | $ python setup.py install --prefix=/opt/setuptools 33 | $ sudo easy_install webdavclient 34 | 35 | **Update** 36 | 37 | .. code:: bash 38 | 39 | $ sudo pip install -U webdavclient 40 | 41 | Webdav API 42 | ========== 43 | 44 | Webdav API is a set of webdav methods of work with cloud storage. This 45 | set includes the following methods: ``check``, ``free``, ``info``, 46 | ``list``, ``mkdir``, ``clean``, ``copy``, ``move``, ``download``, 47 | ``upload``, ``publish`` and ``unpublish``. 48 | 49 | **Configuring the client** 50 | 51 | Required keys for configuring client connection with WevDAV-server are 52 | webdav\_hostname and webdav\_login, webdav,\_password. 53 | 54 | .. code:: python 55 | 56 | import webdav.client as wc 57 | options = { 58 | 'webdav_hostname': "https://webdav.server.ru", 59 | 'webdav_login': "login", 60 | 'webdav_password': "password" 61 | } 62 | client = wc.Client(options) 63 | 64 | When a proxy server you need to specify settings to connect through it. 65 | 66 | .. code:: python 67 | 68 | import webdav.client as wc 69 | options = { 70 | 'webdav_hostname': "https://webdav.server.ru", 71 | 'webdav_login': "w_login", 72 | 'webdav_password': "w_password", 73 | 'proxy_hostname': "http://127.0.0.1:8080", 74 | 'proxy_login': "p_login", 75 | 'proxy_password': "p_password" 76 | } 77 | client = wc.Client(options) 78 | 79 | If you want to use the certificate path to certificate and private key 80 | is defined as follows: 81 | 82 | .. code:: python 83 | 84 | import webdav.client as wc 85 | options = { 86 | 'webdav_hostname': "https://webdav.server.ru", 87 | 'webdav_login': "w_login", 88 | 'webdav_password': "w_password", 89 | 'cert_path': "/etc/ssl/certs/certificate.crt", 90 | 'key_path': "/etc/ssl/private/certificate.key" 91 | } 92 | client = wc.Client(options) 93 | 94 | Or you want to limit the speed or turn on verbose mode: 95 | 96 | .. code:: python 97 | 98 | options = { 99 | ... 100 | 'recv_speed' : 3000000, 101 | 'send_speed' : 3000000, 102 | 'verbose' : True 103 | } 104 | client = wc.Client(options) 105 | 106 | | recv\_speed: rate limit data download speed in Bytes per second. 107 | Defaults to unlimited speed. 108 | | send\_speed: rate limit data upload speed in Bytes per second. 109 | Defaults to unlimited speed. 110 | | verbose: set verbose mode on/off. By default verbose mode is off. 111 | 112 | **Synchronous methods** 113 | 114 | .. code:: python 115 | 116 | // Checking existence of the resource 117 | 118 | client.check("dir1/file1") 119 | client.check("dir1") 120 | 121 | .. code:: python 122 | 123 | // Get information about the resource 124 | 125 | client.info("dir1/file1") 126 | client.info("dir1/") 127 | 128 | .. code:: python 129 | 130 | // Check free space 131 | 132 | free_size = client.free() 133 | 134 | .. code:: python 135 | 136 | // Get a list of resources 137 | 138 | files1 = client.list() 139 | files2 = client.list("dir1") 140 | 141 | .. code:: python 142 | 143 | // Create directory 144 | 145 | client.mkdir("dir1/dir2") 146 | 147 | .. code:: python 148 | 149 | // Delete resource 150 | 151 | client.clean("dir1/dir2") 152 | 153 | .. code:: python 154 | 155 | // Copy resource 156 | 157 | client.copy(remote_path_from="dir1/file1", remote_path_to="dir2/file1") 158 | client.copy(remote_path_from="dir2", remote_path_to="dir3") 159 | 160 | .. code:: python 161 | 162 | // Move resource 163 | 164 | client.move(remote_path_from="dir1/file1", remote_path_to="dir2/file1") 165 | client.move(remote_path_from="dir2", remote_path_to="dir3") 166 | 167 | .. code:: python 168 | 169 | // Move resource 170 | 171 | client.download_sync(remote_path="dir1/file1", local_path="~/Downloads/file1") 172 | client.download_sync(remote_path="dir1/dir2/", local_path="~/Downloads/dir2/") 173 | 174 | .. code:: python 175 | 176 | // Unload resource 177 | 178 | client.upload_sync(remote_path="dir1/file1", local_path="~/Documents/file1") 179 | client.upload_sync(remote_path="dir1/dir2/", local_path="~/Documents/dir2/") 180 | 181 | .. code:: python 182 | 183 | // Publish the resource 184 | 185 | link = client.publish("dir1/file1") 186 | link = client.publish("dir2") 187 | 188 | .. code:: python 189 | 190 | // Unpublish resource 191 | 192 | client.unpublish("dir1/file1") 193 | client.unpublish("dir2") 194 | 195 | .. code:: python 196 | 197 | // Exception handling 198 | 199 | from webdav.client import WebDavException 200 | try: 201 | ... 202 | except WebDavException as exception: 203 | ... 204 | 205 | .. code:: python 206 | 207 | // Get the missing files 208 | 209 | client.pull(remote_directory='dir1', local_directory='~/Documents/dir1') 210 | 211 | .. code:: python 212 | 213 | // Send missing files 214 | 215 | client.push(remote_directory='dir1', local_directory='~/Documents/dir1') 216 | 217 | **Asynchronous methods** 218 | 219 | .. code:: python 220 | 221 | // Load resource 222 | 223 | kwargs = { 224 | 'remote_path': "dir1/file1", 225 | 'local_path': "~/Downloads/file1", 226 | 'callback': callback 227 | } 228 | client.download_async(**kwargs) 229 | 230 | kwargs = { 231 | 'remote_path': "dir1/dir2/", 232 | 'local_path': "~/Downloads/dir2/", 233 | 'callback': callback 234 | } 235 | client.download_async(**kwargs) 236 | 237 | .. code:: python 238 | 239 | // Unload resource 240 | 241 | kwargs = { 242 | 'remote_path': "dir1/file1", 243 | 'local_path': "~/Downloads/file1", 244 | 'callback': callback 245 | } 246 | client.upload_async(**kwargs) 247 | 248 | kwargs = { 249 | 'remote_path': "dir1/dir2/", 250 | 'local_path': "~/Downloads/dir2/", 251 | 'callback': callback 252 | } 253 | client.upload_async(**kwargs) 254 | 255 | Resource API 256 | ============ 257 | 258 | Resource API using the concept of OOP that enables cloud-level 259 | resources. 260 | 261 | .. code:: python 262 | 263 | // Get a resource 264 | 265 | res1 = client.resource("dir1/file1") 266 | 267 | .. code:: python 268 | 269 | // Work with the resource 270 | 271 | res1.rename("file2") 272 | res1.move("dir1/file2") 273 | res1.copy("dir2/file1") 274 | info = res1.info() 275 | res1.read_from(buffer) 276 | res1.read(local_path="~/Documents/file1") 277 | res1.read_async(local_path="~/Documents/file1", callback) 278 | res1.write_to(buffer) 279 | res1.write(local_path="~/Downloads/file1") 280 | res1.write_async(local_path="~/Downloads/file1", callback) 281 | 282 | wdc 283 | === 284 | 285 | wdc \-a cross-platform utility that provides convenient work with 286 | WebDAV-servers right from your console. In addition to full 287 | implementations of methods from webdav API, also added methods content 288 | sync local and remote directories. 289 | 290 | **Authentication** 291 | 292 | - *Basic authentication* 293 | 294 | .. code:: bash 295 | 296 | $ wdc login https://wedbav.server.ru -p http://127.0.0.1:8080 297 | webdav_login: w_login 298 | webdav_password: w_password 299 | proxy_login: p_login 300 | proxy_password: p_password 301 | success 302 | 303 | - Authorize the application using OAuth token\* 304 | 305 | .. code:: bash 306 | 307 | $ wdc login https://wedbav.server.ru -p http://127.0.0.1:8080 --token xxxxxxxxxxxxxxxxxx 308 | proxy_login: p_login 309 | proxy_password: p_password 310 | success 311 | 312 | There are also additional keys ``--root[-r]``, ``--cert-path[-c]`` and 313 | ``--key-path[-k]``. 314 | 315 | **Utility** 316 | 317 | .. code:: bash 318 | 319 | $ wdc check 320 | success 321 | $ wdc check file1 322 | not success 323 | $ wdc free 324 | 245234120344 325 | $ wdc ls dir1 326 | file1 327 | ... 328 | fileN 329 | $ wdc mkdir dir2 330 | $ wdc copy dir1/file1 -t dir2/file1 331 | $ wdc move dir2/file1 -t dir2/file2 332 | $ wdc download dir1/file1 -t ~/Downloads/file1 333 | $ wdc download dir1/ -t ~/Downloads/dir1/ 334 | $ wdc upload dir2/file2 -f ~/Documents/file1 335 | $ wdc upload dir2/ -f ~/Documents/ 336 | $ wdc publish di2/file2 337 | https://yadi.sk/i/vWtTUcBucAc6k 338 | $ wdc unpublish dir2/file2 339 | $ wdc pull dir1/ -t ~/Documents/dir1/ 340 | $ wdc push dir1/ -f ~/Documents/dir1/ 341 | $ wdc info dir1/file1 342 | {'name': 'file1', 'modified': 'Thu, 23 Oct 2014 16:16:37 GMT', 343 | 'size': '3460064', 'created': '2014-10-23T16:16:37Z'} 344 | 345 | WebDAV-server 346 | ============= 347 | 348 | The most popular cloud-based repositories that support the Protocol 349 | WebDAV can be attributed Yandex.Drive, Dropbox, Google Drive, Box and 350 | 4shared. Access to data repositories, operating with access to the 351 | Internet. If necessary local locations and cloud storage, you can deploy 352 | your own WebDAV-server. 353 | 354 | **Local WebDAV-server** 355 | 356 | To deploy a local WebDAV server, using Docker containers quite easily 357 | and quickly. To see an example of a local deploymentWebDAV servers can 358 | be on the project 359 | `webdav-server-docker `__. 360 | 361 | **Supported methods** 362 | 363 | +----------------+--------+--------+--------+---------+---------+--------+--------+------------+----------+ 364 | | Servers | free | info | list | mkdir | clean | copy | move | download | upload | 365 | +================+========+========+========+=========+=========+========+========+============+==========+ 366 | | Yandex.Disk | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ | 367 | +----------------+--------+--------+--------+---------+---------+--------+--------+------------+----------+ 368 | | Dropbox | \- | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ | 369 | +----------------+--------+--------+--------+---------+---------+--------+--------+------------+----------+ 370 | | Google Drive | \- | \+ | \+ | \+ | \+ | \- | \- | \+ | \+ | 371 | +----------------+--------+--------+--------+---------+---------+--------+--------+------------+----------+ 372 | | Box | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ | \+ | 373 | +----------------+--------+--------+--------+---------+---------+--------+--------+------------+----------+ 374 | | 4shared | \- | \+ | \+ | \+ | \+ | \- | \- | \+ | \+ | 375 | +----------------+--------+--------+--------+---------+---------+--------+--------+------------+----------+ 376 | | Webdavserver | \- | \+ | \+ | \+ | \+ | \- | \- | \+ | \+ | 377 | +----------------+--------+--------+--------+---------+---------+--------+--------+------------+----------+ 378 | 379 | Publish and unpublish methods supports only Yandex.Disk. 380 | 381 | **Configuring connections** 382 | 383 | To work with cloud storage Dropbox and Google Drive via the WebDAV 384 | Protocol, you must use a WebDAV-server DropDAV and DAV-pocket, 385 | respectively. 386 | 387 | A list of settings for WebDAV servers: 388 | 389 | .. code:: yaml 390 | 391 | webdav-servers: 392 | - yandex 393 | hostname: https://webdav.yandex.ru 394 | login: #login_for_yandex 395 | password: #pass_for_yandex 396 | - dropbox 397 | hostname: https://dav.dropdav.com 398 | login: #login_for dropdav 399 | password: #pass_for_dropdav 400 | - google 401 | hostname: https://dav-pocket.appspot.com 402 | root: docso 403 | login: #login_for_dav-pocket 404 | password: #pass_for_dav-pocket 405 | - box 406 | hostname: https://dav.box.com 407 | root: dav 408 | login: #login_for_box 409 | password: #pass_for_box 410 | - 4shared 411 | hostname: https://webdav.4shared.com 412 | login: #login_for_4shared 413 | password: #pass_for_4shared 414 | 415 | Autocompletion 416 | ============== 417 | 418 | For macOS, or older Unix systems you need to update bash. 419 | 420 | .. code:: bash 421 | 422 | $ brew install bash 423 | $ chsh 424 | $ brew install bash-completion 425 | 426 | Autocompletion can be enabled globally 427 | 428 | .. code:: bash 429 | 430 | $ sudo activate-global-python-argcomplete 431 | 432 | or locally 433 | 434 | .. code:: bash 435 | 436 | #.bashrc 437 | eval "$(register-python-argcomplete wdc)" 438 | 439 | .. |PyPI version| image:: https://badge.fury.io/py/webdavclient.svg 440 | :target: http://badge.fury.io/py/webdavclient 441 | .. |Requirements Status| image:: https://requires.io/github/designerror/webdav-client-python/requirements.svg?branch=master&style=flat 442 | :target: https://requires.io/github/designerror/webdav-client-python/requirements/?branch=master&style=flat 443 | .. |PullReview stats| image:: https://www.pullreview.com/github/designerror/webdavclient/badges/master.svg? 444 | :target: https://www.pullreview.com/github/designerror/webdavclient/reviews/master 445 | .. |Github| image:: https://github.com/favicon.ico 446 | 447 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | Тестирование 2 | === 3 | 4 | В пакет Webdavclient включены следующие компоненты: 5 | 6 | - `webdav API` 7 | - `resource API` 8 | - `wdc` 9 | 10 | Каждый из компонентов имеет свою тестовую базу. 11 | 12 | ### webdav API ### 13 | 14 | Компонент webdav API содержит следующие тестовые наборы: 15 | 16 | - настройка подключения 17 | - аутентификация 18 | - методы 19 | - интеграционные тесты 20 | 21 | #### Настройка подключения #### 22 | 23 | Для инициализации клиента используется словарь (настройки подключения), содержащий набор опций подключения к webdav-серверу. 24 | 25 | Настройки подключения имеют обязательные и не обязательные опции. К обязательным опциям относятся `webdav_hostname`, `webdav_login` и `webdav_password`. 26 | 27 | Заполнение полей может быть валидным или не валидным. 28 | Для проверки используются метод `valid?`. 29 | 30 | ```python 31 | import webdav.client as wc 32 | options = { 33 | 'webdav_hostname': "https://webdav.server.ru", 34 | 'webdav_login': "login", 35 | 'webdav_password': "password" 36 | } 37 | client = wc.Client(options) 38 | client.valid? 39 | ``` 40 | 41 | *Тестовый сценарий 1* 42 | 43 | Идентификатор: 1.1.1 44 | Название: Обязательные опции подключения 45 | Описание: В случае присутствия каждой из обязательных опций, 46 | настройки подключения клиента будут валидными. 47 | 48 | Красный случай 49 | ```python 50 | assert_that(client, is_(not_valid()) 51 | ``` 52 | 53 | Зеленый случай 54 | ```python 55 | assert_that(client, is_(valid()) 56 | ``` 57 | 58 | 59 | *Тестовый сценарий 2* 60 | 61 | Идентификатор: 1.1.2 62 | Название: Валидность обязательных опций подключения 63 | Описание: В случае валидности обязательных опций, 64 | настройки подключения клиента будут валидными. 65 | 66 | Красный случай 67 | ```python 68 | #without webdav_hostname 69 | #without webdav_login 70 | #without webdav_password 71 | #with proxy_login or proxy_password, but without proxy_hostname 72 | #with proxy_password and proxy_hostname, but without proxy_login 73 | assert_that(client, is_(not_valid()) 74 | ``` 75 | 76 | Зеленый случай 77 | ```python 78 | assert_that(client, is_(valid()) 79 | ``` 80 | 81 | 82 | *Тестовый сценарий 3* 83 | 84 | Идентификатор: 1.1.3 85 | Название: Валидность сертификата 86 | Описание: При указании валидного файла и ключа сертификата, 87 | настройки подключения клиента будут валидными. 88 | 89 | Красный случай 90 | ```python 91 | #with key_path, but without cert_path 92 | #key_path or cert_path not exists 93 | assert_that(calling(client.is_valid), raises(CertificateNotValid)) 94 | ``` 95 | 96 | Зеленый случай 97 | ```python 98 | assert_that(calling(client.is_valid), is_not(raises(CertificateNotValid)) 99 | ``` 100 | 101 | #### Аутентификация #### 102 | 103 | При подключении к webdav-серверу, необходимо пройти у него basic-аутентификацию, для этого используются опции `webdav_login` и `webdav_password`. При наличии proxy-сервера, может потребоваться так же пройти аутентификацию и у proxy-сервера. Для proxy-сервера поддерживаются следующие виды аутентификации: 104 | 105 | - `basic` 106 | - `digest` 107 | - `ntlm` 108 | - `negotiate` 109 | 110 | 111 | *Тестовый сценарий 1* 112 | 113 | Идентификатор: 1.2.1 114 | Название: Аутентификация с webdav-сервером 115 | Описание: При правильной аутентификации клиент подключается к webdav-серверу. 116 | 117 | Красный случай 118 | ```python 119 | assert_that(calling(client.check), is_(not_suceess()) 120 | ``` 121 | 122 | Зеленый случай 123 | ```python 124 | assert_that(calling(client.check), is_(suceess()) 125 | ``` 126 | 127 | 128 | *Тестовый сценарий 2* 129 | 130 | Идентификатор: 1.2.2 131 | Название: Аутентификация с proxy-сервером 132 | Описание: При правильной аутентификации клиент подключается к webdav-серверу. 133 | 134 | Красный случай 135 | ```python 136 | assert_that(calling(client.check), is_(not_suceess()) 137 | ``` 138 | 139 | Зеленый случай 140 | ```python 141 | #basic 142 | #digest 143 | #ntlm 144 | #negotiate 145 | assert_that(calling(client.check), is_(suceess()) 146 | ``` 147 | 148 | #### Методы #### 149 | 150 | webdav API реализует следущие методы: `check`, `free`, `info`, `list`, `mkdir`, `clean`, `copy`, `move`, `download`, `upload`, `publish` и `unpublish`. 151 | 152 | 153 | *Тестовый сценарий 1* 154 | 155 | Идентификатор: 1.3.1 156 | Название: Проверка существования ресурса 157 | Описание: В случае существования ресурса, результат выполнения метода check 158 | будет успешным. 159 | 160 | Красный случай 161 | ```python 162 | assert_that(calling(client.check).with_args(remote_path), is_(not_suceess()) 163 | ``` 164 | 165 | Зеленый случай 166 | ```python 167 | assert_that(calling(client.check).with_args(remote_path), is_(suceess()) 168 | ``` 169 | 170 | 171 | *Тестовый сценарий 2* 172 | 173 | Идентификатор: 1.3.2 174 | Название: Проверка свободного места 175 | Описание: В случае если webdav-сервер поддерживает метод free, метод возвращает 176 | размер свободного места. 177 | 178 | Красный случай 179 | ```python 180 | assert_that(calling(client.free), raises(MethodNotSupported)) 181 | ``` 182 | 183 | Зеленый случай 184 | ```python 185 | assert_that(calling(client.free), greater_than(0)) 186 | ``` 187 | 188 | 189 | *Тестовый сценарий 3* 190 | 191 | Идентификатор: 1.3.3 192 | Название: Получение информации о ресурсе 193 | Описание: В случае если webdav-сервер поддерживает метод info, 194 | метод возвращает информацию следующего типа: 195 | - дата создания; 196 | - дата модификации; 197 | - размер; 198 | - имя. 199 | 200 | 201 | Красный случай 202 | ```python 203 | assert_that(calling(client.info).with_args(remote_path), raises(RemoteResourceNotFound)) 204 | ``` 205 | 206 | Зеленый случай 207 | ```python 208 | info = client(remote_path) 209 | assert_that(info, has_key("data1")) 210 | assert_that(info, has_key("data2")) 211 | assert_that(info, has_key("size")) 212 | assert_that(info, has_key("name")) 213 | ``` 214 | 215 | 216 | *Тестовый сценарий 4* 217 | 218 | Идентификатор: 1.3.4 219 | Название: Получение списка ресурсов 220 | Описание: В случае, если указанный ресурс существует и является директорией, то метод list 221 | возвращает список ресурсов, находящихся в данном ресурсе. 222 | 223 | 224 | Красный случай 225 | ```python 226 | assert_that(calling(client.info).with_args(remote_file), raises(RemoteResourceNotFound)) 227 | assert_that(calling(client.list).with_args(remote_path), raises(RemoteResourceNotFound)) 228 | ``` 229 | 230 | Зеленый случай 231 | ```python 232 | files = client.list(remote_path) 233 | assert_that(files, not_none())) 234 | ``` 235 | 236 | 237 | *Тестовый сценарий 5* 238 | 239 | Идентификатор: 1.3.5 240 | Название: Создание директории 241 | Описание: В случае, если все директории из путевого разбиения для 242 | указанного ресурса существуют, то данный ресурс будет создан. 243 | 244 | 245 | Красный случай 246 | ```python 247 | assert_that(calling(client.info).with_args(remote_path), raises(RemoteParentNotFound)) 248 | ``` 249 | 250 | Зеленый случай 251 | ```python 252 | client.mkdir(remote_path) 253 | assert_that(calling(client.check).with_args(remote_path), is_(success())) 254 | ``` 255 | 256 | 257 | *Тестовый сценарий 6* 258 | 259 | Идентификатор: 1.3.6 260 | Название: Удаление ресурса 261 | Описание: В случае, если указанный ресурс существует и не является корнем, то 262 | метод clean удалит данный ресурс. 263 | 264 | 265 | Красный случай 266 | ```python 267 | assert_that(calling(client.clean).with_args(remote_path), raises(RemoteResourceNotFound)) 268 | assert_that(calling(client.clean).with_args(root), raises(InvalidOption)) 269 | ``` 270 | 271 | Зеленый случай 272 | ```python 273 | client.clean(remote_path) 274 | assert_that(calling(client.check).with_args(remote_path), is_(not_success())) 275 | ``` 276 | 277 | 278 | *Тестовый сценарий 7* 279 | 280 | Идентификатор: 1.3.7 281 | Название: Копирование ресурса 282 | Описание: В случае, если указанный ресурс существует и не является корнем, то 283 | метод copy копирует данный ресурс. 284 | 285 | Красный случай 286 | ```python 287 | assert_that(calling(client.copy).with_args(from_path=remote_path, to_path=new_path), raises(RemoteResourceNotFound)) 288 | assert_that(calling(client.copy).with_args(from_path=root, to_path=new_path), raises(InvalidOption)) 289 | assert_that(calling(client.copy).with_args(from_path=remote_path, to_path=root), raises(InvalidOption)) 290 | assert_that(calling(client.copy).with_args(from_path=remote_path, to_path=remote_path), is_not(raises(WebDavException))) 291 | ``` 292 | 293 | Зеленый случай 294 | ```python 295 | client.copy(from_path=remote_path, to_path=new_path) 296 | assert_that(calling(client.check).with_args(new_path), is_(success())) 297 | ``` 298 | 299 | 300 | *Тестовый сценарий 8* 301 | 302 | Идентификатор: 1.3.8 303 | Название: Перемещение ресурса 304 | Описание: В случае, если указанный ресурс существует и не является корнем, то 305 | метод move переместит данный ресурс. 306 | 307 | Красный случай 308 | ```python 309 | assert_that(calling(client.move).with_args(from_path=old_path, to_path=new_path), raises(RemoteResourceNotFound)) 310 | assert_that(calling(client.move).with_args(from_path=root, to_path=new_path), raises(InvalidOption)) 311 | assert_that(calling(client.move).with_args(from_path=old_path, to_path=root), raises(InvalidOption)) 312 | assert_that(calling(client.move).with_args(from_path=old_path, to_path=remote_path), is_not(raises(WebDavException))) 313 | ``` 314 | 315 | Зеленый случай 316 | ```python 317 | client.move(from_path=old_path, to_path=new_path) 318 | assert_that(calling(client.check).with_args(old_path), is_(not_success())) 319 | assert_that(calling(client.check).with_args(new_path), is_(success())) 320 | ``` 321 | 322 | 323 | *Тестовый сценарий 9* 324 | 325 | Идентификатор: 1.3.9 326 | Название: Загрузка ресурса 327 | Описание: В случае, если указанный ресурс существует, 328 | то метод download загрузит данный ресурс. 329 | 330 | Красный случай 331 | ```python 332 | assert_that(calling(client.download).with_args(remote_path=remote_path, local_path=local_path), raises(LocalResourceNotFound)) 333 | assert_that(calling(client.download).with_args(remote_path=remote_path, local_path=local_path), raises(RemoteResourceNotFound)) 334 | assert_that(calling(client.download).with_args(remote_path=remote_file, local_path=local_directory), raises(InvalidOption)) 335 | assert_that(calling(client.download).with_args(remote_path=remote_directory, local_path=local_file), raises(InvalidOption)) 336 | ``` 337 | 338 | Зеленый случай 339 | ```python 340 | client.download(remote_path=remote_path, local_path=local_path) 341 | assert_that(local_path, is_(exist())) 342 | ``` 343 | 344 | 345 | *Тестовый сценарий 10* 346 | 347 | Идентификатор: 1.3.10 348 | Название: Выгрузка ресурса 349 | Описание: В случае, если родительская директория указанный ресурса 350 | существует, то метод upload выгрузит файл или директорию в 351 | ресурс. 352 | 353 | Красный случай 354 | ```python 355 | assert_that(calling(client.upload).with_args(remote_path=remote_path, local_path=local_path), raises(RemoteParentNotFound)) 356 | assert_that(calling(client.upload).with_args(remote_path=remote_file, local_path=local_directory), raises(InvalidOption)) 357 | assert_that(calling(client.upload).with_args(remote_path=remote_directory, local_path=local_file), raises(InvalidOption)) 358 | ``` 359 | 360 | Зеленый случай 361 | ```python 362 | client.upload(remote_path=remote_path, to_path=local_path) 363 | assert_that(calling(client.check).with_args(remote_path), is_(success())) 364 | ``` 365 | 366 | 367 | *Тестовый сценарий 11* 368 | 369 | Идентификатор: 1.3.11 370 | Название: Публикация ресурса 371 | Описание: В случае, если указанный ресурс существует, то метод publish 372 | возвращает публичную ссылку на ресурс. 373 | 374 | Красный случай 375 | ```python 376 | assert_that(calling(client.publish).with_args(remote_path), raises(RemoteResourceNotFound)) 377 | ``` 378 | 379 | Зеленый случай 380 | ```python 381 | assert_that(calling(client.publish).with_args(remote_path), is_not(raises(RemoteResourceNotFound)) 382 | link = client.publish(remote_path) 383 | assert_that(link, starts_with("http") 384 | ``` 385 | 386 | 387 | *Тестовый сценарий 12* 388 | 389 | Идентификатор: 1.3.12 390 | Название: Отмена публикации ресурса 391 | Описание: В случае, если указанный ресурс существует, 392 | то метод unpublish отменяет публикацию ресурса. 393 | 394 | Красный случай 395 | ```python 396 | assert_that(calling(client.unpublish).with_args(remote_path), raises(RemoteResourceNotFound)) 397 | ``` 398 | 399 | Зеленый случай 400 | ```python 401 | assert_that(calling(client.unpublish).with_args(remote_path), is_not(raises(RemoteResourceNotFound)) 402 | ``` 403 | 404 | ### resource API ### 405 | 406 | Компонент resource API состоит из следующих тестовых наборов методы: 407 | 408 | - получение ресурса 409 | - методы 410 | 411 | #### Получение ресурса #### 412 | 413 | Для получение ресурса, используется метод `resource`. 414 | 415 | 416 | *Тестовый сценарий 1* 417 | 418 | Идентификатор: 2.1.1 419 | Название: Получение ресурса 420 | Описание: В случае, если указанный ресурс является директорией и существует, 421 | то метод resource возвращает ресурс. 422 | В случае, если указанный ресурс является файлом, 423 | то метод resource возвращает ресурс. 424 | 425 | Красный случай 426 | ```python 427 | assert_that(calling(client.resource).with_args(remote_directory), raises(RemoteResourceNotFound)) 428 | assert_that(calling(client.resource).with_args(remote_file), is_not(raises(RemoteResourceNotFound))) 429 | ``` 430 | 431 | Зеленый случай 432 | ```python 433 | assert_that(calling(client.check).with_args(remote_path), is_(success()) 434 | res = client.resource(remote_path) 435 | assert_that(res.check()) 436 | ``` 437 | 438 | #### Методы #### 439 | 440 | resource API реализует следущие методы: `check`, `clean`, `is_directory`, `rename`, `move`, `copy`, `info`, `read_from`, `read`, `read_async`, `write_to`, `write`, `write_async`, `publish` и `unpublish`. 441 | 442 | 443 | *Тестовый сценарий 1* 444 | 445 | Идентификатор: 2.2.1 446 | Название: Проверка существования ресурса 447 | Описание: В случае, если указанный ресурс существует, 448 | то результат метода check будет успешным. 449 | 450 | Красный случай 451 | ```python 452 | assert_that(calling(client.resource).with_args(remote_path), raises(RemoteResourceNotFound)) 453 | ``` 454 | 455 | Зеленый случай 456 | ```python 457 | assert_that(calling(client.check).with_args(remote_path), is_(success()) 458 | res = client.resource(remote_path) 459 | assert_that(res.check()) 460 | ``` 461 | 462 | 463 | *Тестовый сценарий 2* 464 | 465 | Идентификатор: 2.2.2 466 | Название: Удаление ресурса 467 | Описание: В случае, если указанный ресурс существует, 468 | то метод clean удалит данный ресурс. 469 | 470 | Красный случай 471 | ```python 472 | res = client.resource(remote_path) 473 | assert_that(calling(res.clean), is_not(raises(RemoteResourceNotFound))) 474 | ``` 475 | 476 | Зеленый случай 477 | ```python 478 | assert_that(calling(client.check).with_args(remote_path), is_(success()) 479 | res = client.resource(remote_path) 480 | assert_that(res.check()) 481 | ``` 482 | 483 | 484 | *Тестовый сценарий 3* 485 | 486 | Идентификатор: 2.2.3 487 | Название: Проверка является ли ресурс директорией 488 | Описание: В случае, если указанный ресурс является директорией, 489 | то результат метода is_directory будет успешным. 490 | 491 | Красный случай 492 | ```python 493 | res = client.resource(remote_file) 494 | assert_that(calling(res.is_directory), is_(not_success())) 495 | ``` 496 | 497 | Зеленый случай 498 | ```python 499 | res = client.resource(remote_directory) 500 | assert_that(calling(res.is_directory), is_(success())) 501 | ``` 502 | 503 | 504 | *Тестовый сценарий 4* 505 | 506 | Идентификатор: 2.2.4 507 | Название: Переименование ресурса 508 | Описание: В случае, если указанный ресурс существует, 509 | то метод rename переименует данный ресурс. 510 | 511 | Красный случай 512 | ```python 513 | res = client.resource(remote_path) 514 | assert_that(calling(res.rename).with_args(new_name), raises(RemoteResourceNotFound)) 515 | assert_that(calling(res.rename).with_args(new_name), raises(RemoteResourceAlreadyExists)) 516 | ``` 517 | 518 | Зеленый случай 519 | ```python 520 | res = client.resource(old_path) 521 | res.rename(new_name) 522 | new_path = res.urn 523 | assert_that(calling(client.check).with_args(old_path), is_(not_success())) 524 | assert_that(calling(client.check).with_args(new_path), is_(success())) 525 | ``` 526 | 527 | 528 | *Тестовый сценарий 5* 529 | 530 | Идентификатор: 2.2.5 531 | Название: Перемещение ресурса 532 | Описание: В случае, если указанный ресурс существует, 533 | то метод move переместит данный ресурс. 534 | 535 | Красный случай 536 | ```python 537 | res = client.resource(old_path) 538 | assert_that(calling(res.move).with_args(new_path), raises(RemoteResourceNotFound)) 539 | ``` 540 | 541 | Зеленый случай 542 | ```python 543 | res = client.resource(old_path) 544 | res.move(new_path) 545 | assert_that(calling(client.check).with_args(old_path), is_(not_success())) 546 | assert_that(calling(client.check).with_args(new_path), is_(success())) 547 | ``` 548 | 549 | 550 | *Тестовый сценарий 6* 551 | 552 | Идентификатор: 2.2.6 553 | Название: Копирование ресурса 554 | Описание: В случае, если указанный ресурс существует, 555 | то метод copy скопирует данный ресурс. 556 | 557 | Красный случай 558 | ```python 559 | res = client.resource(remote_path) 560 | assert_that(calling(res.copy).with_args(to_path), raises(RemoteResourceNotFound)) 561 | ``` 562 | 563 | Зеленый случай 564 | ```python 565 | res = client.resource(remote_path) 566 | res.copy(new_path) 567 | assert_that(calling(client.check).with_args(remote_path), is_(success())) 568 | assert_that(calling(client.check).with_args(new_path), is_(success())) 569 | ``` 570 | 571 | 572 | *Тестовый сценарий 7* 573 | 574 | Идентификатор: 2.2.7 575 | Название: Получение информации о ресурсе 576 | Описание: В случае, если указанный ресурс существует, 577 | то метод info возвращает информацию следующего типа: 578 | - дата создания; 579 | - дата модификации; 580 | - размер; 581 | - имя. 582 | 583 | 584 | Красный случай 585 | ```python 586 | res = client.resource(remote_path) 587 | assert_that(calling(res.info), raises(RemoteResourceNotFound)) 588 | ``` 589 | 590 | Зеленый случай 591 | ```python 592 | res = client.resource(remote_path) 593 | info = res.info() 594 | assert_that(info, has_key("data1")) 595 | assert_that(info, has_key("data2")) 596 | assert_that(info, has_key("size")) 597 | assert_that(info, has_key("name")) 598 | ``` 599 | 600 | 601 | *Тестовый сценарий 8* 602 | 603 | Идентификатор: 2.2.8 604 | Название: Считывание данных с буфера в ресурс 605 | Описание: В случае, если указанный ресурс не является директорией, 606 | то метод read_from считывет содержимое буфера и записывает в ресурс. 607 | 608 | Красный случай 609 | ```python 610 | res1 = client.resource(remote_file) 611 | assert_that(buff, is_(empty)) 612 | assert_that(calling(res1.read_from).with_args(buff), raises(BufferIsEmpty)) 613 | 614 | res2 = client.resource(remote_directory) 615 | assert_that(calling(res2.read_from).with_args(buff), raises(ResourceIsNotDirectory)) 616 | ``` 617 | 618 | Зеленый случай 619 | ```python 620 | res = client.resource(remote_path) 621 | res.read_from(buff) 622 | res_size = res.info("size") 623 | assert_that(buff.size(), equal_to(res_size)) 624 | ``` 625 | 626 | 627 | *Тестовый сценарий 9* 628 | 629 | Идентификатор: 2.2.9 630 | Название: Запись данных в буфер 631 | Описание: В случае, если указанный ресурс не является директорией, 632 | то метод write_to записывает содержимое ресурса в буфер. 633 | 634 | Красный случай 635 | ```python 636 | res = client.resource(remote_path) 637 | assert_that(calling(res.write_to).with_args(buff), raises(RemoteResourceNotFound)) 638 | ``` 639 | 640 | Зеленый случай 641 | ```python 642 | res = client.resource(remote_path) 643 | res.write_to(buff) 644 | res_size = res.info("size") 645 | assert_that(buff.size(), equal_to(res_size)) 646 | ``` 647 | 648 | 649 | *Тестовый сценарий 10* 650 | 651 | Идентификатор: 2.2.10 652 | Название: Публикация ресурса 653 | Описание: В случае, если указанный ресурс существует, то метод publish 654 | возвращает публичную ссылку на ресурс. 655 | 656 | Красный случай 657 | ```python 658 | res = client.resource(remote_path) 659 | assert_that(calling(res.publish), raises(RemoteResourceNotFound)) 660 | ``` 661 | 662 | Зеленый случай 663 | ```python 664 | res = client.resource(remote_path) 665 | assert_that(calling(res.publish), is_not(raises(RemoteResourceNotFound)) 666 | link = res.publish() 667 | assert_that(link, starts_with("http") 668 | ``` 669 | 670 | 671 | *Тестовый сценарий 11* 672 | 673 | Идентификатор: 2.2.11 674 | Название: Отмена публикации ресурса 675 | Описание: В случае, если указанный ресурс существует, 676 | то метод unpublish отменяет публикацию ресурса. 677 | 678 | Красный случай 679 | ```python 680 | res = client.resource(remote_path) 681 | assert_that(calling(res.unpublish), raises(RemoteResourceNotFound)) 682 | ``` 683 | 684 | Зеленый случай 685 | ```python 686 | res = client.resource(remote_path) 687 | assert_that(calling(res.unpublish).with_args(remote_path), is_not(raises(RemoteResourceNotFound)) 688 | ``` 689 | -------------------------------------------------------------------------------- /THANKS.md: -------------------------------------------------------------------------------- 1 | - [cesterlizi](https://github.com/cesterlizi) 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | import subprocess 5 | from setuptools import setup, find_packages 6 | 7 | from setuptools.command.test import test as TestCommand 8 | from setuptools.command.install import install as InstallCommand 9 | 10 | version = "1.0.8" 11 | requirements = "libxml2-dev libxslt-dev python-dev libcurl4-openssl-dev python-pycurl" 12 | 13 | class Install(InstallCommand): 14 | 15 | def run(self): 16 | 17 | #params = "{install_params} {requirements}".format(install_params="install", requirements=requirements) 18 | #cmd = "{command} {params}".format(command="apt-get", params=params) 19 | #proc = subprocess.Popen(cmd, shell=True) 20 | #proc.wait() 21 | InstallCommand.run(self) 22 | 23 | class Test(TestCommand): 24 | 25 | user_options = [('pytest-args=', 'a', "")] 26 | 27 | def initialize_options(self): 28 | 29 | TestCommand.initialize_options(self) 30 | self.pytest_args = [] 31 | 32 | def finalize_options(self): 33 | 34 | TestCommand.finalize_options(self) 35 | self.test_args = [] 36 | self.test_suite = True 37 | 38 | def run_tests(self): 39 | 40 | import pytest 41 | errno = pytest.main(self.pytest_args) 42 | sys.exit(errno) 43 | 44 | setup( 45 | name = 'webdavclient', 46 | version = version, 47 | packages = find_packages(), 48 | requires = ['python (>= 2.7.6)'], 49 | install_requires=['pycurl', 'lxml', 'argcomplete'], 50 | scripts = ['wdc'], 51 | tests_require=['pytest', 'pyhamcrest', 'junit-xml', 'pytest-allure-adaptor'], 52 | cmdclass = {'install': Install, 'test': Test}, 53 | description = 'Webdav API, resource API и wdc для WebDAV-серверов (Yandex.Disk, Dropbox, Google Disk, Box, 4shared и т.д.)', 54 | long_description = open('README.rst').read(), 55 | author = 'Designerror', 56 | author_email = 'designerror@yandex.ru', 57 | url = 'https://github.com/designerror/webdavclient', 58 | download_url = 'https://github.com/designerror/webdavclient/tarball/master', 59 | license = 'MIT License', 60 | keywords = 'webdav, client, python, module, library, packet, Yandex.Disk, Dropbox, Google Disk, Box, 4shared', 61 | classifiers = [ 62 | 'Environment :: Console', 63 | 'Environment :: Web Environment', 64 | 'License :: OSI Approved :: MIT License', 65 | 'Operating System :: MacOS', 66 | 'Operating System :: Microsoft', 67 | 'Operating System :: Unix', 68 | 'Programming Language :: Python :: 2.6', 69 | 'Programming Language :: Python :: 2.7', 70 | 'Programming Language :: Python :: 3.0', 71 | 'Programming Language :: Python :: 3.1', 72 | 'Programming Language :: Python :: 3.2', 73 | 'Programming Language :: Python :: 3.3', 74 | 'Programming Language :: Python :: 3.4', 75 | 'Topic :: Internet', 76 | 'Topic :: Software Development :: Libraries :: Python Modules', 77 | ], 78 | ) 79 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | __author__ = 'designerror' 2 | 3 | from hamcrest.core.base_matcher import BaseMatcher 4 | from hamcrest.core.helpers.hasmethod import hasmethod 5 | 6 | class Valid(BaseMatcher): 7 | def _matches(self, item): 8 | return False if not hasmethod(item, 'valid') else item.valid() 9 | 10 | class NotValid(BaseMatcher): 11 | def _matches(self, item): 12 | return False if not hasmethod(item, 'valid') else not item.valid() 13 | 14 | class Success(BaseMatcher): 15 | def _matches(self, item): 16 | return item 17 | 18 | class NotSuccess(BaseMatcher): 19 | def _matches(self, item): 20 | return not item 21 | 22 | def valid(): 23 | return Valid() 24 | 25 | def not_valid(): 26 | return NotValid() 27 | 28 | def success(): 29 | return Success() 30 | 31 | def not_success(): 32 | return NotSuccess() -------------------------------------------------------------------------------- /tests/webdav/test_authenticate.py: -------------------------------------------------------------------------------- 1 | __author__ = 'designerror' 2 | -------------------------------------------------------------------------------- /tests/webdav/test_connection.py: -------------------------------------------------------------------------------- 1 | __author__ = 'designerror' 2 | 3 | import allure 4 | #from hamcrest import * 5 | 6 | class TestRequiredOptions: 7 | 8 | def test_without_webdav_hostname(self): 9 | options = { 'webdav_server': "https://webdav.yandex.ru"} 10 | allure.attach('options', options.__str__()) 11 | assert 1 12 | -------------------------------------------------------------------------------- /tests/webdav/test_methods.py: -------------------------------------------------------------------------------- 1 | __author__ = 'designerror' 2 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27 3 | toxworkdir={toxinidir}/../.tox 4 | [testenv] 5 | deps= 6 | pycurl 7 | lxml 8 | argcomplete 9 | pytest 10 | pyhamcrest 11 | junit-xml 12 | pytest-allure-adaptor 13 | commands=python setup.py test -a "--alluredir /var/tmp/allure" 14 | -------------------------------------------------------------------------------- /wdc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # PYTHON_ARGCOMPLETE_OK 3 | 4 | from __future__ import print_function 5 | import sys 6 | import os 7 | import shlex 8 | import struct 9 | import platform 10 | import subprocess 11 | import getpass 12 | import argparse 13 | import argcomplete 14 | from webdav.client import Client, WebDavException, NotConnection, Urn 15 | from distutils.util import strtobool 16 | from base64 import b64decode, b64encode 17 | 18 | 19 | def get_terminal_size(): 20 | current_os = platform.system() 21 | tuple_xy = None 22 | if current_os == 'Windows': 23 | tuple_xy = _get_terminal_size_windows() 24 | if tuple_xy is None: 25 | tuple_xy = _get_terminal_size_input() 26 | if current_os in ['Linux', 'Darwin'] or current_os.startswith('CYGWIN'): 27 | tuple_xy = _get_terminal_size_linux() 28 | if tuple_xy is None: 29 | tuple_xy = 80, 25 30 | return tuple_xy 31 | 32 | 33 | def _get_terminal_size_windows(): 34 | try: 35 | from ctypes import windll, create_string_buffer 36 | h = windll.kernel32.GetStdHandle(-12) 37 | csbi = create_string_buffer(22) 38 | res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) 39 | if res: 40 | (bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw) 41 | sizex = right - left + 1 42 | sizey = bottom - top + 1 43 | return sizex, sizey 44 | except: 45 | pass 46 | 47 | 48 | def _get_terminal_size_input(): 49 | try: 50 | cols = int(subprocess.check_call(shlex.split('tput cols'))) 51 | rows = int(subprocess.check_call(shlex.split('tput lines'))) 52 | return (cols, rows) 53 | except: 54 | pass 55 | 56 | 57 | def _get_terminal_size_linux(): 58 | 59 | def ioctl_GWINSZ(fd): 60 | try: 61 | import fcntl 62 | import termios 63 | return struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) 64 | except: 65 | pass 66 | cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) 67 | if not cr: 68 | try: 69 | fd = os.open(os.ctermid(), os.O_RDONLY) 70 | cr = ioctl_GWINSZ(fd) 71 | os.close(fd) 72 | except: 73 | pass 74 | if not cr: 75 | try: 76 | cr = (os.environ['LINES'], os.environ['COLUMNS']) 77 | except: 78 | return None 79 | return int(cr[1]), int(cr[0]) 80 | 81 | 82 | class ProgressBar: 83 | 84 | def __init__(self): 85 | self.precise = 0 86 | self.total = 0 87 | 88 | def show(self): 89 | progress_line = self._progress_line() 90 | import sys 91 | if self.precise == 100: 92 | print(progress_line, end="") 93 | else: 94 | print(progress_line, end="") 95 | sys.stdout.write("\r") 96 | 97 | def _progress_line(self): 98 | sizex, sizey = get_terminal_size() 99 | available_width = sizex 100 | precise = self._get_field_precise() 101 | ratio = self._get_field_ratio() 102 | available_width -= len(precise) + len(ratio) 103 | progress = self._get_field_progress(available_width-2) 104 | return "{precise} {progress} {ratio}".format(precise=precise, progress=progress, ratio=ratio) 105 | 106 | def _get_field_precise(self): 107 | line = "{precise}%".format(precise=self.precise) 108 | return "{:<4}".format(line) 109 | 110 | def _get_field_ratio(self): 111 | current = float(self.precise * self.total / 100) 112 | total = float(self.total) 113 | current_line = "{:.2f}".format(current) 114 | total_line = "{:.2f}".format(total) 115 | line = "{current}/{total}".format(current=current_line, total=total_line) 116 | available = len(total_line) * 2 + 1 117 | format_line = "{prefix}{value}{sufix}".format(prefix="{:>", value=available, sufix="}") 118 | line = format_line.format(line) 119 | return line 120 | 121 | def _get_field_progress(self, width): 122 | available_width = width - 2 123 | current = int(self.precise * available_width / 100) 124 | available_width -= current + 1 125 | tip = ">" if not self.precise == 100 else "" 126 | progress = "{arrow}{tip}{space}".format(arrow="=" * current, tip=tip, space=" " * available_width) 127 | return "[{progress}]".format(progress=progress) 128 | 129 | def callback(self, current, total): 130 | if total and not self.total: 131 | self.total = total 132 | if not total: 133 | return 134 | precise = int(float(current) * 100 / total) 135 | if self.precise == precise: 136 | return 137 | else: 138 | self.precise = precise 139 | self.show() 140 | 141 | setting_keys = ['webdav_hostname', 'webdav_root', 'webdav_login', 'webdav_password', 'webdav_token', 142 | 'proxy_hostname', 'proxy_login', 'proxy_password', 143 | 'cert_path', 'key_path'] 144 | 145 | crypto_keys = ['webdav_password', 'webdav_token', 'proxy_password'] 146 | 147 | 148 | def encoding(source): 149 | 150 | if not source: 151 | return "" 152 | return b64encode(source.encode('utf-8')) 153 | 154 | 155 | def decoding(source): 156 | 157 | if not source: 158 | return "" 159 | return b64decode(source).decode('utf-8') 160 | 161 | 162 | def import_options(): 163 | 164 | options = dict() 165 | for setting_key in setting_keys: 166 | options[setting_key] = os.environ.get(setting_key.upper()) 167 | 168 | for crypto_key in crypto_keys: 169 | if not options[crypto_key]: 170 | continue 171 | options[crypto_key] = decoding(options[crypto_key]) 172 | 173 | return options 174 | 175 | 176 | def valid(options): 177 | 178 | if 'webdav_hostname' not in options: 179 | return False 180 | 181 | if not options['webdav_token'] and not options['webdav_login']: 182 | return False 183 | 184 | if options['webdav_login'] and not options['webdav_password']: 185 | return False 186 | 187 | return True 188 | 189 | 190 | class Formatter(argparse.RawTextHelpFormatter): 191 | 192 | def _get_default_metavar_for_optional(self, action): 193 | if not action.option_strings: 194 | return action.dest 195 | else: 196 | return "" 197 | 198 | def _format_action_invocation(self, action): 199 | if not action.option_strings: 200 | default = self._get_default_metavar_for_optional(action) 201 | metavar, = self._metavar_formatter(action, default)(1) 202 | return metavar 203 | 204 | else: 205 | parts = [] 206 | 207 | if action.nargs == 0: 208 | parts.extend(action.option_strings) 209 | else: 210 | default = self._get_default_metavar_for_optional(action) 211 | args_string = self._format_args(action, default) 212 | for option_string in action.option_strings: 213 | parts.append(option_string) 214 | 215 | return '%s %s' % (', '.join(parts), args_string) 216 | 217 | return ', '.join(parts) 218 | 219 | def _metavar_formatter(self, action, default_metavar): 220 | if action.metavar is not None: 221 | result = action.metavar 222 | else: 223 | result = default_metavar 224 | 225 | def format(tuple_size): 226 | if isinstance(result, tuple): 227 | return result 228 | else: 229 | return (result, ) * tuple_size 230 | return format 231 | 232 | 233 | def logging_exception(exception): 234 | print(exception) 235 | 236 | 237 | def urn_completer(prefix, **kwargs): 238 | 239 | options = import_options() 240 | try: 241 | client = Client(options) 242 | 243 | prefix_urn = Urn(prefix) 244 | if prefix_urn.is_dir(): 245 | return (prefix+filename for filename in client.list(prefix_urn.path())) 246 | else: 247 | parent = prefix_urn.parent() 248 | prefix_filename = prefix_urn.filename() 249 | prefix_filename_length = len(prefix_filename) 250 | return (prefix + filename[prefix_filename_length:] for filename in client.list(parent) if filename.startswith(prefix_filename)) 251 | except WebDavException: 252 | pass 253 | 254 | return tuple() 255 | 256 | if __name__ == "__main__": 257 | 258 | epilog = """ 259 | Examples: 260 | -------- 261 | $ wdc login https://webdav.server.ru 262 | webdav_login: login 263 | webdav_password: password 264 | success 265 | $ wdc login https://webdav.server.ru --token xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 266 | not success 267 | $ wdc check 268 | success 269 | $ wdc check file1 270 | not success 271 | $ wdc free 272 | 245234120344 273 | $ wdc ls dir1 274 | file1 275 | ... 276 | fileN 277 | $ wdc mkdir dir2 278 | $ wdc copy dir1/file1 -t dir2/file1 279 | $ wdc move dir2/file1 -t dir2/file2 280 | $ wdc download dir1/file1 -t ~/Downloads/file1 281 | $ wdc download dir1/ -t ~/Downloads/dir1/ 282 | $ wdc upload dir2/file2 -f ~/Documents/file1 283 | $ wdc upload dir2/ -f ~/Documents/ 284 | $ wdc publish di2/file2 285 | https://yadi.sk/i/vWtTUcBucAc6k 286 | $ wdc unpublish dir2/file2 287 | $ wdc pull dir1/ -t ~/Documents/dir1/ 288 | $ wdc push dir1/ -f ~/Documents/dir1/ 289 | $ wdc info dir1/file1 290 | {'name': 'file1', 'modified': 'Thu, 23 Oct 2014 16:16:37 GMT', 291 | 'size': '3460064', 'created': '2014-10-23T16:16:37Z'} 292 | """ 293 | 294 | usage = """ 295 | wdc [-h] [-v] 296 | wdc login https://webdav.server.ru [--token] [-r] [-p] [-c] [-k] 297 | wdc [action] [path] [-t] [-f] 298 | """ 299 | 300 | actions = "login logout check info free ls clean mkdir copy move download upload publish unpublish push pull".split() 301 | actions_help = "check, info, free, ls, clean, mkdir, copy, move,\ndownload, upload, publish, unpublish, push, pull" 302 | 303 | parser = argparse.ArgumentParser(prog='wdc', formatter_class=Formatter, epilog=epilog, usage=usage) 304 | parser.add_argument("action", help=actions_help, choices=actions) 305 | 306 | from webdav.client import __version__ as version 307 | version_text = "{name} {version}".format(name="%(prog)s", version=version) 308 | parser.add_argument("-v", '--version', action='version', version=version_text) 309 | parser.add_argument("-r", "--root", help="example: dir1/dir2") 310 | parser.add_argument("--token", help="example: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") 311 | parser.add_argument("-c", "--cert-path", help="example: /etc/ssl/certs/certificate.crt") 312 | parser.add_argument("-k", "--key-path", help="example: /etc/ssl/private/certificate.key") 313 | parser.add_argument("-p", "--proxy", help="example: http://127.0.0.1:8080") 314 | parser.add_argument("path", help="example: dir1/dir2/file1", nargs='?').completer = urn_completer 315 | parser.add_argument("-f", '--from-path', help="example: ~/Documents/file1") 316 | parser.add_argument("-t", "--to-path", help="example for download and pull: ~/Download/file1\nexample for copy and move: dir1/dir2").completer = urn_completer 317 | 318 | argcomplete.autocomplete(parser, exclude=("-h", "--help", "--proxy", "-p", "-r", "--root", "-c", "--cert-path", "-t", "--to-path", "-v", "--version", "-f", "--from-path", "-k", "--key-path")) 319 | args = parser.parse_args() 320 | action = args.action 321 | 322 | if action == 'login': 323 | env = dict() 324 | if not args.path: 325 | try: 326 | env['webdav_hostname'] = raw_input("webdav_hostname: ") 327 | except NameError: 328 | env['webdav_hostname'] = input("webdav_hostname: ") 329 | else: 330 | env['webdav_hostname'] = args.path 331 | 332 | if not args.token: 333 | try: 334 | env['webdav_login'] = raw_input("webdav_login: ") 335 | except NameError: 336 | env['webdav_login'] = input("webdav_login: ") 337 | env['webdav_password'] = getpass.getpass("webdav_password: ") 338 | else: 339 | env['webdav_token'] = args.token 340 | 341 | if args.proxy: 342 | env['proxy_hostname'] = args.proxy 343 | try: 344 | env['proxy_login'] = raw_input("proxy_login: ") 345 | except NameError: 346 | env['proxy_login'] = input("proxy_login: ") 347 | env['proxy_password'] = getpass.getpass("proxy_password: ") 348 | 349 | if args.root: 350 | env['webdav_root'] = args.root 351 | 352 | if args.cert_path: 353 | env['cert_path'] = args.cert_path 354 | 355 | if args.key_path: 356 | env['key_path'] = args.key_path 357 | 358 | try: 359 | client = Client(env) 360 | check = client.check() 361 | text = "success" if check else "not success" 362 | print(text) 363 | 364 | if check: 365 | for crypto_key in crypto_keys: 366 | if crypto_key not in env: 367 | continue 368 | if not env[crypto_key]: 369 | continue 370 | env[crypto_key] = encoding(env[crypto_key]) 371 | 372 | for (key, value) in env.items(): 373 | os.putenv(key.upper(), value) 374 | os.system('bash') 375 | 376 | except WebDavException as e: 377 | print("not success") 378 | sys.exit() 379 | else: 380 | options = import_options() 381 | if not valid(options): 382 | print("First log on webdav server using the following command: wdc login.") 383 | sys.exit() 384 | 385 | elif action == "logout": 386 | os.system("exit") 387 | 388 | elif action == 'check': 389 | options = import_options() 390 | try: 391 | client = Client(options) 392 | connection = client.check() 393 | if not connection: 394 | raise NotConnection(options["webdav_hostname"]) 395 | check = client.check(args.path) if args.path else client.check() 396 | text = "success" if check else "not success" 397 | print(text) 398 | except WebDavException as e: 399 | logging_exception(e) 400 | 401 | elif action == 'free': 402 | options = import_options() 403 | try: 404 | client = Client(options) 405 | connection = client.check() 406 | if not connection: 407 | raise NotConnection(options["webdav_hostname"]) 408 | free_size = client.free() 409 | print(free_size) 410 | except WebDavException as e: 411 | logging_exception(e) 412 | 413 | elif action == 'ls': 414 | options = import_options() 415 | try: 416 | client = Client(options) 417 | connection = client.check() 418 | if not connection: 419 | raise NotConnection(options["webdav_hostname"]) 420 | paths = client.list(args.path) if args.path else client.list() 421 | for path in paths: 422 | print(path) 423 | except WebDavException as e: 424 | logging_exception(e) 425 | 426 | elif action == 'clean': 427 | if not args.path: 428 | parser.print_help() 429 | else: 430 | options = import_options() 431 | try: 432 | client = Client(options) 433 | connection = client.check() 434 | if not connection: 435 | raise NotConnection(options["webdav_hostname"]) 436 | client.clean(args.path) 437 | except WebDavException as e: 438 | logging_exception(e) 439 | 440 | elif action == 'mkdir': 441 | if not args.path: 442 | parser.print_help() 443 | else: 444 | options = import_options() 445 | try: 446 | client = Client(options) 447 | connection = client.check() 448 | if not connection: 449 | raise NotConnection(options["webdav_hostname"]) 450 | client.mkdir(args.path) 451 | except WebDavException as e: 452 | logging_exception(e) 453 | 454 | elif action == 'copy': 455 | if not args.path or not args.to_path: 456 | parser.print_help() 457 | else: 458 | options = import_options() 459 | try: 460 | client = Client(options) 461 | connection = client.check() 462 | if not connection: 463 | raise NotConnection(options["webdav_hostname"]) 464 | client.copy(remote_path_from=args.path, remote_path_to=args.to_path) 465 | except WebDavException as e: 466 | logging_exception(e) 467 | 468 | elif action == 'move': 469 | if not args.path or not args.to_path: 470 | parser.print_help() 471 | else: 472 | options = import_options() 473 | try: 474 | client = Client(options) 475 | connection = client.check() 476 | if not connection: 477 | raise NotConnection(options["webdav_hostname"]) 478 | client.move(remote_path_from=args.path, remote_path_to=args.to_path) 479 | except WebDavException as e: 480 | logging_exception(e) 481 | 482 | elif action == 'download': 483 | if not args.path or not args.to_path: 484 | parser.print_help() 485 | else: 486 | options = import_options() 487 | progress_bar = ProgressBar() 488 | 489 | def download_progress(download_t, download_d, upload_t, upload_d): 490 | progress_bar.callback(current=download_d, total=download_t) 491 | 492 | try: 493 | client = Client(options) 494 | connection = client.check() 495 | if not connection: 496 | raise NotConnection(options["webdav_hostname"]) 497 | if not os.path.exists(path=args.to_path): 498 | client.download(remote_path=args.path, local_path=args.to_path, progress=download_progress) 499 | print("\n") 500 | else: 501 | try: 502 | choice = raw_input("Local path exists, do you want to overwrite it? [Y/n] ") 503 | except NameError: 504 | choice = input("Local path exists, do you want to overwrite it? [Y/n] ") 505 | try: 506 | yes = strtobool(choice.lower()) 507 | if yes: 508 | client.download(remote_path=args.path, local_path=args.to_path, progress=download_progress) 509 | print("\n") 510 | except ValueError: 511 | print("Incorrect answer") 512 | except WebDavException as e: 513 | logging_exception(e) 514 | 515 | elif action == 'upload': 516 | if not args.path or not args.from_path: 517 | parser.print_help() 518 | else: 519 | options = import_options() 520 | progress_bar = ProgressBar() 521 | 522 | def upload_progress(download_t, download_d, upload_t, upload_d): 523 | progress_bar.callback(current=upload_d, total=upload_t) 524 | 525 | try: 526 | client = Client(options) 527 | connection = client.check() 528 | if not connection: 529 | raise NotConnection(options["webdav_hostname"]) 530 | if not client.check(remote_path=args.path): 531 | client.upload(remote_path=args.path, local_path=args.from_path, progress=upload_progress) 532 | print("\n") 533 | else: 534 | try: 535 | choice = raw_input("Remote resource exists, do you want to overwrite it? [Y/n] ") 536 | except NameError: 537 | choice = input("Remote resource exists, do you want to overwrite it? [Y/n] ") 538 | try: 539 | yes = strtobool(choice.lower()) 540 | if yes: 541 | client.upload(remote_path=args.path, local_path=args.from_path, progress=upload_progress) 542 | print("\n") 543 | except ValueError: 544 | print("Incorrect answer") 545 | except WebDavException as e: 546 | logging_exception(e) 547 | 548 | elif action == 'publish': 549 | if not args.path: 550 | parser.print_help() 551 | else: 552 | options = import_options() 553 | try: 554 | client = Client(options) 555 | connection = client.check() 556 | if not connection: 557 | raise NotConnection(options["webdav_hostname"]) 558 | link = client.publish(args.path) 559 | print(link) 560 | except WebDavException as e: 561 | logging_exception(e) 562 | 563 | elif action == 'unpublish': 564 | if not args.path: 565 | parser.print_help() 566 | else: 567 | options = import_options() 568 | try: 569 | client = Client(options) 570 | connection = client.check() 571 | if not connection: 572 | raise NotConnection(options["webdav_hostname"]) 573 | client.unpublish(args.path) 574 | except WebDavException as e: 575 | logging_exception(e) 576 | 577 | elif action == 'push': 578 | if not args.path or not args.from_path: 579 | parser.print_help() 580 | else: 581 | options = import_options() 582 | try: 583 | client = Client(options) 584 | connection = client.check() 585 | if not connection: 586 | raise NotConnection(options["webdav_hostname"]) 587 | client.push(remote_directory=args.path, local_directory=args.from_path) 588 | except WebDavException as e: 589 | logging_exception(e) 590 | 591 | elif action == 'pull': 592 | if not args.path or not args.to_path: 593 | parser.print_help() 594 | else: 595 | options = import_options() 596 | try: 597 | client = Client(options) 598 | connection = client.check() 599 | if not connection: 600 | raise NotConnection(options["webdav_hostname"]) 601 | client.pull(remote_directory=args.path, local_directory=args.to_path) 602 | except WebDavException as e: 603 | logging_exception(e) 604 | 605 | elif action == 'info': 606 | if not args.path: 607 | parser.print_help() 608 | else: 609 | options = import_options() 610 | try: 611 | client = Client(options) 612 | connection = client.check() 613 | if not connection: 614 | raise NotConnection(options["webdav_hostname"]) 615 | info = client.info(args.path) 616 | print(info) 617 | except WebDavException as e: 618 | logging_exception(e) 619 | 620 | else: 621 | parser.print_help() 622 | -------------------------------------------------------------------------------- /webdav/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudPolis/webdav-client-python/2f011c543264f44d49bea56aebd2815791e7542e/webdav/__init__.py -------------------------------------------------------------------------------- /webdav/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | 3 | import pycurl 4 | import os 5 | import shutil 6 | import threading 7 | import lxml.etree as etree 8 | from io import BytesIO 9 | from re import sub 10 | from webdav.connection import * 11 | from webdav.exceptions import * 12 | from webdav.urn import Urn 13 | 14 | try: 15 | from urllib.parse import unquote 16 | except ImportError: 17 | from urllib import unquote 18 | 19 | __version__ = "1.0.8" 20 | 21 | 22 | def listdir(directory): 23 | 24 | file_names = list() 25 | for filename in os.listdir(directory): 26 | file_path = os.path.join(directory, filename) 27 | if os.path.isdir(file_path): 28 | filename = "{filename}{separate}".format(filename=filename, separate=os.path.sep) 29 | file_names.append(filename) 30 | return file_names 31 | 32 | 33 | def add_options(request, options): 34 | 35 | for (key, value) in options.items(): 36 | if value is None: 37 | continue 38 | try: 39 | request.setopt(pycurl.__dict__[key], value) 40 | except TypeError: 41 | raise OptionNotValid(key, value) 42 | except pycurl.error: 43 | raise OptionNotValid(key, value) 44 | 45 | 46 | def get_options(type, from_options): 47 | 48 | _options = dict() 49 | 50 | for key in type.keys: 51 | key_with_prefix = "{prefix}{key}".format(prefix=type.prefix, key=key) 52 | if key not in from_options and key_with_prefix not in from_options: 53 | _options[key] = "" 54 | elif key in from_options: 55 | _options[key] = from_options.get(key) 56 | else: 57 | _options[key] = from_options.get(key_with_prefix) 58 | 59 | return _options 60 | 61 | 62 | class Client(object): 63 | 64 | root = '/' 65 | large_size = 2 * 1024 * 1024 * 1024 66 | 67 | http_header = { 68 | 'list': ["Accept: */*", "Depth: 1"], 69 | 'free': ["Accept: */*", "Depth: 0", "Content-Type: text/xml"], 70 | 'copy': ["Accept: */*"], 71 | 'move': ["Accept: */*"], 72 | 'mkdir': ["Accept: */*", "Connection: Keep-Alive"], 73 | 'clean': ["Accept: */*", "Connection: Keep-Alive"], 74 | 'check': ["Accept: */*"], 75 | 'info': ["Accept: */*", "Depth: 1"], 76 | 'get_metadata': ["Accept: */*", "Depth: 1", "Content-Type: application/x-www-form-urlencoded"], 77 | 'set_metadata': ["Accept: */*", "Depth: 1", "Content-Type: application/x-www-form-urlencoded"] 78 | } 79 | 80 | def get_header(self, method): 81 | 82 | if method in Client.http_header: 83 | try: 84 | header = Client.http_header[method].copy() 85 | except AttributeError: 86 | header = Client.http_header[method][:] 87 | else: 88 | header = list() 89 | 90 | if self.webdav.token: 91 | webdav_token = "Authorization: OAuth {token}".format(token=self.webdav.token) 92 | header.append(webdav_token) 93 | 94 | return header 95 | 96 | requests = { 97 | 'copy': "COPY", 98 | 'move': "MOVE", 99 | 'mkdir': "MKCOL", 100 | 'clean': "DELETE", 101 | 'check': "HEAD", 102 | 'list': "PROPFIND", 103 | 'free': "PROPFIND", 104 | 'info': "PROPFIND", 105 | 'publish': "PROPPATCH", 106 | 'unpublish': "PROPPATCH", 107 | 'published': "PROPPATCH", 108 | 'get_metadata': "PROPFIND", 109 | 'set_metadata': "PROPPATCH" 110 | } 111 | 112 | meta_xmlns = { 113 | 'https://webdav.yandex.ru': "urn:yandex:disk:meta", 114 | } 115 | 116 | def __init__(self, options): 117 | 118 | webdav_options = get_options(type=WebDAVSettings, from_options=options) 119 | proxy_options = get_options(type=ProxySettings, from_options=options) 120 | 121 | self.webdav = WebDAVSettings(webdav_options) 122 | self.proxy = ProxySettings(proxy_options) 123 | 124 | pycurl.global_init(pycurl.GLOBAL_DEFAULT) 125 | 126 | self.default_options = {} 127 | 128 | def __del__(self): 129 | pycurl.global_cleanup() 130 | 131 | def valid(self): 132 | return True if self.webdav.valid() and self.proxy.valid() else False 133 | 134 | def Request(self, options=None): 135 | 136 | curl = pycurl.Curl() 137 | 138 | self.default_options.update({ 139 | 'URL': self.webdav.hostname, 140 | 'NOBODY': 1, 141 | 'SSLVERSION': pycurl.SSLVERSION_TLSv1, 142 | }) 143 | 144 | if not self.webdav.token: 145 | server_token = '{login}:{password}'.format(login=self.webdav.login, password=self.webdav.password) 146 | self.default_options.update({ 147 | 'USERPWD': server_token, 148 | }) 149 | 150 | if self.proxy.valid(): 151 | if self.proxy.hostname: 152 | self.default_options['PROXY'] = self.proxy.hostname 153 | 154 | if self.proxy.login: 155 | if not self.proxy.password: 156 | self.default_options['PROXYUSERNAME'] = self.proxy.login 157 | else: 158 | proxy_token = '{login}:{password}'.format(login=self.proxy.login, password=self.proxy.password) 159 | self.default_options['PROXYUSERPWD'] = proxy_token 160 | 161 | if self.webdav.cert_path: 162 | self.default_options['SSLCERT'] = self.webdav.cert_path 163 | 164 | if self.webdav.key_path: 165 | self.default_options['SSLKEY'] = self.webdav.key_path 166 | 167 | if self.webdav.recv_speed: 168 | self.default_options['MAX_RECV_SPEED_LARGE'] = self.webdav.recv_speed 169 | 170 | if self.webdav.send_speed: 171 | self.default_options['MAX_SEND_SPEED_LARGE'] = self.webdav.send_speed 172 | 173 | if self.webdav.verbose: 174 | self.default_options['VERBOSE'] = self.webdav.verbose 175 | 176 | if self.default_options: 177 | add_options(curl, self.default_options) 178 | 179 | if options: 180 | add_options(curl, options) 181 | 182 | return curl 183 | 184 | def list(self, remote_path=root): 185 | 186 | def parse(response): 187 | 188 | try: 189 | response_str = response.getvalue() 190 | tree = etree.fromstring(response_str) 191 | hrees = [unquote(hree.text) for hree in tree.findall(".//{DAV:}href")] 192 | return [Urn(hree) for hree in hrees] 193 | except etree.XMLSyntaxError: 194 | return list() 195 | 196 | try: 197 | directory_urn = Urn(remote_path, directory=True) 198 | 199 | if directory_urn.path() != Client.root: 200 | if not self.check(directory_urn.path()): 201 | raise RemoteResourceNotFound(directory_urn.path()) 202 | 203 | response = BytesIO() 204 | 205 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': directory_urn.quote()} 206 | options = { 207 | 'URL': "{hostname}{root}{path}".format(**url), 208 | 'CUSTOMREQUEST': Client.requests['list'], 209 | 'HTTPHEADER': self.get_header('list'), 210 | 'WRITEDATA': response, 211 | 'NOBODY': 0 212 | } 213 | 214 | request = self.Request(options=options) 215 | 216 | request.perform() 217 | request.close() 218 | 219 | urns = parse(response) 220 | 221 | path = "{root}{path}".format(root=self.webdav.root, path=directory_urn.path()) 222 | return [urn.filename() for urn in urns if urn.path() != path and urn.path() != path[:-1]] 223 | 224 | except pycurl.error: 225 | raise NotConnection(self.webdav.hostname) 226 | 227 | def free(self): 228 | 229 | def parse(response): 230 | 231 | try: 232 | response_str = response.getvalue() 233 | tree = etree.fromstring(response_str) 234 | node = tree.find('.//{DAV:}quota-available-bytes') 235 | if node is not None: 236 | return int(node.text) 237 | else: 238 | raise MethodNotSupported(name='free', server=self.webdav.hostname) 239 | except TypeError: 240 | raise MethodNotSupported(name='free', server=self.webdav.hostname) 241 | except etree.XMLSyntaxError: 242 | return str() 243 | 244 | def data(): 245 | 246 | root = etree.Element("propfind", xmlns="DAV:") 247 | prop = etree.SubElement(root, "prop") 248 | etree.SubElement(prop, "quota-available-bytes") 249 | etree.SubElement(prop, "quota-used-bytes") 250 | tree = etree.ElementTree(root) 251 | buff = BytesIO() 252 | tree.write(buff) 253 | return buff.getvalue() 254 | 255 | try: 256 | response = BytesIO() 257 | 258 | options = { 259 | 'CUSTOMREQUEST': Client.requests['free'], 260 | 'HTTPHEADER': self.get_header('free'), 261 | 'POSTFIELDS': data(), 262 | 'WRITEDATA': response, 263 | 'NOBODY': 0 264 | } 265 | 266 | request = self.Request(options=options) 267 | 268 | request.perform() 269 | request.close() 270 | 271 | return parse(response) 272 | 273 | except pycurl.error: 274 | raise NotConnection(self.webdav.hostname) 275 | 276 | def check(self, remote_path=root): 277 | 278 | try: 279 | urn = Urn(remote_path) 280 | response = BytesIO() 281 | 282 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} 283 | options = { 284 | 'URL': "{hostname}{root}{path}".format(**url), 285 | 'CUSTOMREQUEST': Client.requests['check'], 286 | 'HTTPHEADER': self.get_header('check'), 287 | 'WRITEDATA': response, 288 | 'NOBODY': 1 289 | } 290 | 291 | request = self.Request(options=options) 292 | 293 | request.perform() 294 | code = request.getinfo(pycurl.HTTP_CODE) 295 | request.close() 296 | 297 | if int(code) == 200: 298 | return True 299 | 300 | return False 301 | 302 | except pycurl.error: 303 | raise NotConnection(self.webdav.hostname) 304 | 305 | def mkdir(self, remote_path): 306 | 307 | try: 308 | directory_urn = Urn(remote_path, directory=True) 309 | 310 | if not self.check(directory_urn.parent()): 311 | raise RemoteParentNotFound(directory_urn.path()) 312 | 313 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': directory_urn.quote()} 314 | options = { 315 | 'URL': "{hostname}{root}{path}".format(**url), 316 | 'CUSTOMREQUEST': Client.requests['mkdir'], 317 | 'HTTPHEADER': self.get_header('mkdir') 318 | } 319 | 320 | request = self.Request(options=options) 321 | 322 | request.perform() 323 | request.close() 324 | 325 | except pycurl.error: 326 | raise NotConnection(self.webdav.hostname) 327 | 328 | def download_to(self, buff, remote_path): 329 | 330 | try: 331 | urn = Urn(remote_path) 332 | 333 | if self.is_dir(urn.path()): 334 | raise OptionNotValid(name="remote_path", value=remote_path) 335 | 336 | if not self.check(urn.path()): 337 | raise RemoteResourceNotFound(urn.path()) 338 | 339 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} 340 | options = { 341 | 'URL': "{hostname}{root}{path}".format(**url), 342 | 'WRITEFUNCTION': buff.write, 343 | 'HTTPHEADER': self.get_header('download_to'), 344 | 'NOBODY': 0 345 | } 346 | 347 | request = self.Request(options=options) 348 | 349 | request.perform() 350 | request.close() 351 | 352 | except pycurl.error: 353 | raise NotConnection(self.webdav.hostname) 354 | 355 | def download(self, remote_path, local_path, progress=None): 356 | 357 | urn = Urn(remote_path) 358 | if self.is_dir(urn.path()): 359 | self.download_directory(local_path=local_path, remote_path=remote_path, progress=progress) 360 | else: 361 | self.download_file(local_path=local_path, remote_path=remote_path, progress=progress) 362 | 363 | def download_directory(self, remote_path, local_path, progress=None): 364 | 365 | urn = Urn(remote_path, directory=True) 366 | 367 | if not self.is_dir(urn.path()): 368 | raise OptionNotValid(name="remote_path", value=remote_path) 369 | 370 | if os.path.exists(local_path): 371 | shutil.rmtree(local_path) 372 | 373 | os.makedirs(local_path) 374 | 375 | for resource_name in self.list(urn.path()): 376 | _remote_path = "{parent}{name}".format(parent=urn.path(), name=resource_name) 377 | _local_path = os.path.join(local_path, resource_name) 378 | self.download(local_path=_local_path, remote_path=_remote_path, progress=progress) 379 | 380 | def download_file(self, remote_path, local_path, progress=None): 381 | 382 | try: 383 | urn = Urn(remote_path) 384 | 385 | if self.is_dir(urn.path()): 386 | raise OptionNotValid(name="remote_path", value=remote_path) 387 | 388 | if os.path.isdir(local_path): 389 | raise OptionNotValid(name="local_path", value=local_path) 390 | 391 | if not self.check(urn.path()): 392 | raise RemoteResourceNotFound(urn.path()) 393 | 394 | with open(local_path, 'wb') as local_file: 395 | 396 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} 397 | options = { 398 | 'URL': "{hostname}{root}{path}".format(**url), 399 | 'HTTPHEADER': self.get_header('download_file'), 400 | 'WRITEDATA': local_file, 401 | 'NOPROGRESS': 0 if progress else 1, 402 | 'NOBODY': 0 403 | } 404 | 405 | if progress: 406 | options["PROGRESSFUNCTION"] = progress 407 | 408 | request = self.Request(options=options) 409 | 410 | request.perform() 411 | request.close() 412 | 413 | except pycurl.error: 414 | raise NotConnection(self.webdav.hostname) 415 | 416 | def download_sync(self, remote_path, local_path, callback=None): 417 | 418 | self.download(local_path=local_path, remote_path=remote_path) 419 | 420 | if callback: 421 | callback() 422 | 423 | def download_async(self, remote_path, local_path, callback=None): 424 | 425 | target = (lambda: self.download_sync(local_path=local_path, remote_path=remote_path, callback=callback)) 426 | threading.Thread(target=target).start() 427 | 428 | def upload_from(self, buff, remote_path): 429 | 430 | try: 431 | urn = Urn(remote_path) 432 | 433 | if urn.is_dir(): 434 | raise OptionNotValid(name="remote_path", value=remote_path) 435 | 436 | if not self.check(urn.parent()): 437 | raise RemoteParentNotFound(urn.path()) 438 | 439 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} 440 | options = { 441 | 'URL': "{hostname}{root}{path}".format(**url), 442 | 'HTTPHEADER': self.get_header('upload_from'), 443 | 'UPLOAD': 1, 444 | 'READFUNCTION': buff.read, 445 | } 446 | 447 | request = self.Request(options=options) 448 | 449 | request.perform() 450 | code = request.getinfo(pycurl.HTTP_CODE) 451 | if code == "507": 452 | raise NotEnoughSpace() 453 | 454 | request.close() 455 | 456 | except pycurl.error: 457 | raise NotConnection(self.webdav.hostname) 458 | 459 | def upload(self, remote_path, local_path, progress=None): 460 | 461 | if os.path.isdir(local_path): 462 | self.upload_directory(local_path=local_path, remote_path=remote_path, progress=progress) 463 | else: 464 | self.upload_file(local_path=local_path, remote_path=remote_path, progress=progress) 465 | 466 | def upload_directory(self, remote_path, local_path, progress=None): 467 | 468 | urn = Urn(remote_path, directory=True) 469 | 470 | if not urn.is_dir(): 471 | raise OptionNotValid(name="remote_path", value=remote_path) 472 | 473 | if not os.path.isdir(local_path): 474 | raise OptionNotValid(name="local_path", value=local_path) 475 | 476 | if not os.path.exists(local_path): 477 | raise LocalResourceNotFound(local_path) 478 | 479 | if self.check(urn.path()): 480 | self.clean(urn.path()) 481 | 482 | self.mkdir(remote_path) 483 | 484 | for resource_name in listdir(local_path): 485 | _remote_path = "{parent}{name}".format(parent=urn.path(), name=resource_name) 486 | _local_path = os.path.join(local_path, resource_name) 487 | self.upload(local_path=_local_path, remote_path=_remote_path, progress=progress) 488 | 489 | def upload_file(self, remote_path, local_path, progress=None): 490 | 491 | try: 492 | if not os.path.exists(local_path): 493 | raise LocalResourceNotFound(local_path) 494 | 495 | urn = Urn(remote_path) 496 | 497 | if urn.is_dir(): 498 | raise OptionNotValid(name="remote_path", value=remote_path) 499 | 500 | if os.path.isdir(local_path): 501 | raise OptionNotValid(name="local_path", value=local_path) 502 | 503 | if not self.check(urn.parent()): 504 | raise RemoteParentNotFound(urn.path()) 505 | 506 | with open(local_path, "rb") as local_file: 507 | 508 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} 509 | options = { 510 | 'URL': "{hostname}{root}{path}".format(**url), 511 | 'HTTPHEADER': self.get_header('upload_file'), 512 | 'UPLOAD': 1, 513 | 'READFUNCTION': local_file.read, 514 | 'NOPROGRESS': 0 if progress else 1 515 | } 516 | 517 | if progress: 518 | options["PROGRESSFUNCTION"] = progress 519 | 520 | file_size = os.path.getsize(local_path) 521 | if file_size > self.large_size: 522 | options['INFILESIZE_LARGE'] = file_size 523 | else: 524 | options['INFILESIZE'] = file_size 525 | 526 | request = self.Request(options=options) 527 | 528 | request.perform() 529 | code = request.getinfo(pycurl.HTTP_CODE) 530 | if code == "507": 531 | raise NotEnoughSpace() 532 | 533 | request.close() 534 | 535 | except pycurl.error: 536 | raise NotConnection(self.webdav.hostname) 537 | 538 | def upload_sync(self, remote_path, local_path, callback=None): 539 | 540 | self.upload(local_path=local_path, remote_path=remote_path) 541 | 542 | if callback: 543 | callback() 544 | 545 | def upload_async(self, remote_path, local_path, callback=None): 546 | 547 | target = (lambda: self.upload_sync(local_path=local_path, remote_path=remote_path, callback=callback)) 548 | threading.Thread(target=target).start() 549 | 550 | def copy(self, remote_path_from, remote_path_to): 551 | 552 | def header(remote_path_to): 553 | 554 | path = Urn(remote_path_to).path() 555 | destination = "{root}{path}".format(root=self.webdav.root, path=path) 556 | header_item = "Destination: {destination}".format(destination=destination) 557 | 558 | header = self.get_header('copy') 559 | header.append(header_item) 560 | 561 | return header 562 | 563 | try: 564 | urn_from = Urn(remote_path_from) 565 | 566 | if not self.check(urn_from.path()): 567 | raise RemoteResourceNotFound(urn_from.path()) 568 | 569 | urn_to = Urn(remote_path_to) 570 | 571 | if not self.check(urn_to.parent()): 572 | raise RemoteParentNotFound(urn_to.path()) 573 | 574 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn_from.quote()} 575 | options = { 576 | 'URL': "{hostname}{root}{path}".format(**url), 577 | 'CUSTOMREQUEST': Client.requests['copy'], 578 | 'HTTPHEADER': header(remote_path_to) 579 | } 580 | 581 | request = self.Request(options=options) 582 | 583 | request.perform() 584 | request.close() 585 | 586 | except pycurl.error: 587 | raise NotConnection(self.webdav.hostname) 588 | 589 | def move(self, remote_path_from, remote_path_to): 590 | 591 | def header(remote_path_to): 592 | 593 | path = Urn(remote_path_to).path() 594 | destination = "{root}{path}".format(root=self.webdav.root, path=path) 595 | header_item = "Destination: {destination}".format(destination=destination) 596 | header = self.get_header('move') 597 | header.append(header_item) 598 | return header 599 | 600 | try: 601 | urn_from = Urn(remote_path_from) 602 | 603 | if not self.check(urn_from.path()): 604 | raise RemoteResourceNotFound(urn_from.path()) 605 | 606 | urn_to = Urn(remote_path_to) 607 | 608 | if not self.check(urn_to.parent()): 609 | raise RemoteParentNotFound(urn_to.path()) 610 | 611 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn_from.quote()} 612 | options = { 613 | 'URL': "{hostname}{root}{path}".format(**url), 614 | 'CUSTOMREQUEST': Client.requests['move'], 615 | 'HTTPHEADER': header(remote_path_to) 616 | } 617 | 618 | request = self.Request(options=options) 619 | 620 | request.perform() 621 | request.close() 622 | 623 | except pycurl.error: 624 | raise NotConnection(self.webdav.hostname) 625 | 626 | def clean(self, remote_path): 627 | 628 | try: 629 | urn = Urn(remote_path) 630 | 631 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} 632 | options = { 633 | 'URL': "{hostname}{root}{path}".format(**url), 634 | 'CUSTOMREQUEST': Client.requests['clean'], 635 | 'HTTPHEADER': self.get_header('clean') 636 | } 637 | 638 | request = self.Request(options=options) 639 | 640 | request.perform() 641 | request.close() 642 | 643 | except pycurl.error: 644 | raise NotConnection(self.webdav.hostname) 645 | 646 | def publish(self, remote_path): 647 | 648 | def parse(response): 649 | 650 | try: 651 | response_str = response.getvalue() 652 | tree = etree.fromstring(response_str) 653 | result = tree.xpath("//*[local-name() = 'public_url']") 654 | public_url = result[0] 655 | return public_url.text 656 | except IndexError: 657 | raise MethodNotSupported(name="publish", server=self.webdav.hostname) 658 | except etree.XMLSyntaxError: 659 | return "" 660 | 661 | def data(for_server): 662 | 663 | root_node = etree.Element("propertyupdate", xmlns="DAV:") 664 | set_node = etree.SubElement(root_node, "set") 665 | prop_node = etree.SubElement(set_node, "prop") 666 | xmlns = Client.meta_xmlns.get(for_server, "") 667 | public_url = etree.SubElement(prop_node, "public_url", xmlns=xmlns) 668 | public_url.text = "true" 669 | tree = etree.ElementTree(root_node) 670 | 671 | buff = BytesIO() 672 | tree.write(buff) 673 | 674 | return buff.getvalue() 675 | 676 | try: 677 | urn = Urn(remote_path) 678 | 679 | if not self.check(urn.path()): 680 | raise RemoteResourceNotFound(urn.path()) 681 | 682 | response = BytesIO() 683 | 684 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} 685 | options = { 686 | 'URL': "{hostname}{root}{path}".format(**url), 687 | 'CUSTOMREQUEST': Client.requests['publish'], 688 | 'HTTPHEADER': self.get_header('publish'), 689 | 'POSTFIELDS': data(for_server=self.webdav.hostname), 690 | 'WRITEDATA': response, 691 | 'NOBODY': 0 692 | } 693 | 694 | request = self.Request(options=options) 695 | 696 | request.perform() 697 | request.close() 698 | 699 | return parse(response) 700 | 701 | except pycurl.error: 702 | raise NotConnection(self.webdav.hostname) 703 | 704 | def unpublish(self, remote_path): 705 | 706 | def data(for_server): 707 | 708 | root = etree.Element("propertyupdate", xmlns="DAV:") 709 | remove = etree.SubElement(root, "remove") 710 | prop = etree.SubElement(remove, "prop") 711 | xmlns = Client.meta_xmlns.get(for_server, "") 712 | etree.SubElement(prop, "public_url", xmlns=xmlns) 713 | tree = etree.ElementTree(root) 714 | 715 | buff = BytesIO() 716 | tree.write(buff) 717 | 718 | return buff.getvalue() 719 | 720 | try: 721 | urn = Urn(remote_path) 722 | 723 | if not self.check(urn.path()): 724 | raise RemoteResourceNotFound(urn.path()) 725 | 726 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} 727 | options = { 728 | 'URL': "{hostname}{root}{path}".format(**url), 729 | 'CUSTOMREQUEST': Client.requests['unpublish'], 730 | 'HTTPHEADER': self.get_header('unpublish'), 731 | 'POSTFIELDS': data(for_server=self.webdav.hostname) 732 | } 733 | 734 | request = self.Request(options=options) 735 | 736 | request.perform() 737 | request.close() 738 | 739 | except pycurl.error: 740 | raise NotConnection(self.webdav.hostname) 741 | 742 | def info(self, remote_path): 743 | 744 | def parse(response, path): 745 | 746 | try: 747 | response_str = response.getvalue() 748 | tree = etree.fromstring(response_str) 749 | 750 | find_attributes = { 751 | 'created': ".//{DAV:}creationdate", 752 | 'name': ".//{DAV:}displayname", 753 | 'size': ".//{DAV:}getcontentlength", 754 | 'modified': ".//{DAV:}getlastmodified" 755 | } 756 | 757 | resps = tree.findall("{DAV:}response") 758 | 759 | for resp in resps: 760 | href = resp.findtext("{DAV:}href") 761 | urn = unquote(href) 762 | 763 | if path[-1] == Urn.separate: 764 | if not path == urn: 765 | continue 766 | else: 767 | path_with_sep = "{path}{sep}".format(path=path, sep=Urn.separate) 768 | if not path == urn and not path_with_sep == urn: 769 | continue 770 | 771 | info = dict() 772 | for (name, value) in find_attributes.items(): 773 | info[name] = resp.findtext(value) 774 | return info 775 | 776 | raise RemoteResourceNotFound(path) 777 | except etree.XMLSyntaxError: 778 | raise MethodNotSupported(name="info", server=self.webdav.hostname) 779 | 780 | try: 781 | urn = Urn(remote_path) 782 | response = BytesIO() 783 | 784 | if not self.check(urn.path()) and not self.check(Urn(remote_path, directory=True).path()): 785 | raise RemoteResourceNotFound(remote_path) 786 | 787 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} 788 | options = { 789 | 'URL': "{hostname}{root}{path}".format(**url), 790 | 'CUSTOMREQUEST': Client.requests['info'], 791 | 'HTTPHEADER': self.get_header('info'), 792 | 'WRITEDATA': response, 793 | 'NOBODY': 0 794 | } 795 | 796 | request = self.Request(options=options) 797 | 798 | request.perform() 799 | request.close() 800 | 801 | path = "{root}{path}".format(root=self.webdav.root, path=urn.path()) 802 | 803 | return parse(response, path) 804 | 805 | except pycurl.error: 806 | raise NotConnection(self.webdav.hostname) 807 | 808 | def is_dir(self, remote_path): 809 | 810 | def parse(response, path): 811 | 812 | try: 813 | response_str = response.getvalue() 814 | tree = etree.fromstring(response_str) 815 | 816 | resps = tree.findall("{DAV:}response") 817 | 818 | for resp in resps: 819 | href = resp.findtext("{DAV:}href") 820 | urn = unquote(href) 821 | 822 | if path[-1] == Urn.separate: 823 | if not path == urn: 824 | continue 825 | else: 826 | path_with_sep = "{path}{sep}".format(path=path, sep=Urn.separate) 827 | if not path == urn and not path_with_sep == urn: 828 | continue 829 | type = resp.find(".//{DAV:}resourcetype") 830 | if type is None: 831 | raise MethodNotSupported(name="is_dir", server=self.webdav.hostname) 832 | dir_type = type.find("{DAV:}collection") 833 | 834 | return True if dir_type is not None else False 835 | 836 | raise RemoteResourceNotFound(path) 837 | 838 | except etree.XMLSyntaxError: 839 | raise MethodNotSupported(name="is_dir", server=self.webdav.hostname) 840 | 841 | try: 842 | urn = Urn(remote_path) 843 | parent_urn = Urn(urn.parent()) 844 | if not self.check(urn.path()) and not self.check(Urn(remote_path, directory=True).path()): 845 | raise RemoteResourceNotFound(remote_path) 846 | 847 | response = BytesIO() 848 | 849 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': parent_urn.quote()} 850 | options = { 851 | 'URL': "{hostname}{root}{path}".format(**url), 852 | 'CUSTOMREQUEST': Client.requests['info'], 853 | 'HTTPHEADER': self.get_header('info'), 854 | 'WRITEDATA': response, 855 | 'NOBODY': 0 856 | } 857 | 858 | request = self.Request(options=options) 859 | 860 | request.perform() 861 | request.close() 862 | 863 | path = "{root}{path}".format(root=self.webdav.root, path=urn.path()) 864 | 865 | return parse(response, path) 866 | 867 | except pycurl.error: 868 | raise NotConnection(self.webdav.hostname) 869 | 870 | def resource(self, remote_path): 871 | 872 | urn = Urn(remote_path) 873 | return Resource(self, urn.path()) 874 | 875 | def get_property(self, remote_path, option): 876 | 877 | def parse(response, option): 878 | 879 | response_str = response.getvalue() 880 | tree = etree.fromstring(response_str) 881 | xpath = "{xpath_prefix}{xpath_exp}".format(xpath_prefix=".//", xpath_exp=option['name']) 882 | return tree.findtext(xpath) 883 | 884 | def data(option): 885 | 886 | root = etree.Element("propfind", xmlns="DAV:") 887 | prop = etree.SubElement(root, "prop") 888 | etree.SubElement(prop, option.get('name', ""), xmlns=option.get('namespace', "")) 889 | tree = etree.ElementTree(root) 890 | 891 | buff = BytesIO() 892 | 893 | tree.write(buff) 894 | return buff.getvalue() 895 | 896 | try: 897 | urn = Urn(remote_path) 898 | 899 | if not self.check(urn.path()): 900 | raise RemoteResourceNotFound(urn.path()) 901 | 902 | response = BytesIO() 903 | 904 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} 905 | options = { 906 | 'URL': "{hostname}{root}{path}".format(**url), 907 | 'CUSTOMREQUEST': Client.requests['get_metadata'], 908 | 'HTTPHEADER': self.get_header('get_metadata'), 909 | 'POSTFIELDS': data(option), 910 | 'WRITEDATA': response, 911 | 'NOBODY': 0 912 | } 913 | 914 | request = self.Request(options=options) 915 | 916 | request.perform() 917 | request.close() 918 | 919 | return parse(response, option) 920 | 921 | except pycurl.error: 922 | raise NotConnection(self.webdav.hostname) 923 | 924 | def set_property(self, remote_path, option): 925 | 926 | def data(option): 927 | 928 | root_node = etree.Element("propertyupdate", xmlns="DAV:") 929 | root_node.set('xmlns:u', option.get('namespace', "")) 930 | set_node = etree.SubElement(root_node, "set") 931 | prop_node = etree.SubElement(set_node, "prop") 932 | opt_node = etree.SubElement(prop_node, "{namespace}:{name}".format(namespace='u', name=option['name'])) 933 | opt_node.text = option.get('value', "") 934 | 935 | tree = etree.ElementTree(root_node) 936 | 937 | buff = BytesIO() 938 | tree.write(buff) 939 | 940 | return buff.getvalue() 941 | 942 | try: 943 | urn = Urn(remote_path) 944 | 945 | if not self.check(urn.path()): 946 | raise RemoteResourceNotFound(urn.path()) 947 | 948 | url = {'hostname': self.webdav.hostname, 'root': self.webdav.root, 'path': urn.quote()} 949 | options = { 950 | 'URL': "{hostname}{root}{path}".format(**url), 951 | 'CUSTOMREQUEST': Client.requests['set_metadata'], 952 | 'HTTPHEADER': self.get_header('get_metadata'), 953 | 'POSTFIELDS': data(option) 954 | } 955 | 956 | request = self.Request(options=options) 957 | 958 | request.perform() 959 | request.close() 960 | 961 | except pycurl.error: 962 | raise NotConnection(self.webdav.hostname) 963 | 964 | def push(self, remote_directory, local_directory): 965 | 966 | def prune(src, exp): 967 | return [sub(exp, "", item) for item in src] 968 | 969 | urn = Urn(remote_directory, directory=True) 970 | 971 | if not self.is_dir(urn.path()): 972 | raise OptionNotValid(name="remote_path", value=remote_directory) 973 | 974 | if not os.path.isdir(local_directory): 975 | raise OptionNotValid(name="local_path", value=local_directory) 976 | 977 | if not os.path.exists(local_directory): 978 | raise LocalResourceNotFound(local_directory) 979 | 980 | paths = self.list(urn.path()) 981 | expression = "{begin}{end}".format(begin="^", end=urn.path()) 982 | remote_resource_names = prune(paths, expression) 983 | 984 | for local_resource_name in listdir(local_directory): 985 | 986 | local_path = os.path.join(local_directory, local_resource_name) 987 | remote_path = "{remote_directory}{resource_name}".format(remote_directory=urn.path(), resource_name=local_resource_name) 988 | 989 | if os.path.isdir(local_path): 990 | if not self.check(remote_path=remote_path): 991 | self.mkdir(remote_path=remote_path) 992 | self.push(remote_directory=remote_path, local_directory=local_path) 993 | else: 994 | if local_resource_name in remote_resource_names: 995 | continue 996 | self.upload_file(remote_path=remote_path, local_path=local_path) 997 | 998 | def pull(self, remote_directory, local_directory): 999 | 1000 | def prune(src, exp): 1001 | return [sub(exp, "", item) for item in src] 1002 | 1003 | urn = Urn(remote_directory, directory=True) 1004 | 1005 | if not self.is_dir(urn.path()): 1006 | raise OptionNotValid(name="remote_path", value=remote_directory) 1007 | 1008 | if not os.path.exists(local_directory): 1009 | raise LocalResourceNotFound(local_directory) 1010 | 1011 | local_resource_names = listdir(local_directory) 1012 | 1013 | paths = self.list(urn.path()) 1014 | expression = "{begin}{end}".format(begin="^", end=remote_directory) 1015 | remote_resource_names = prune(paths, expression) 1016 | 1017 | for remote_resource_name in remote_resource_names: 1018 | 1019 | local_path = os.path.join(local_directory, remote_resource_name) 1020 | remote_path = "{remote_directory}{resource_name}".format(remote_directory=urn.path(), resource_name=remote_resource_name) 1021 | 1022 | remote_urn = Urn(remote_path) 1023 | 1024 | if self.is_dir(remote_urn.path()): 1025 | if not os.path.exists(local_path): 1026 | os.mkdir(local_path) 1027 | self.pull(remote_directory=remote_path, local_directory=local_path) 1028 | else: 1029 | if remote_resource_name in local_resource_names: 1030 | continue 1031 | self.download_file(remote_path=remote_path, local_path=local_path) 1032 | 1033 | def sync(self, remote_directory, local_directory): 1034 | 1035 | self.pull(remote_directory=remote_directory, local_directory=local_directory) 1036 | self.push(remote_directory=remote_directory, local_directory=local_directory) 1037 | 1038 | 1039 | class Resource(object): 1040 | 1041 | def __init__(self, client, urn): 1042 | self.client = client 1043 | self.urn = urn 1044 | 1045 | def __str__(self): 1046 | return "resource {path}".format(path=self.urn.path()) 1047 | 1048 | def is_dir(self): 1049 | return self.client.is_dir(self.urn.path()) 1050 | 1051 | def rename(self, new_name): 1052 | 1053 | old_path = self.urn.path() 1054 | parent_path = self.urn.parent() 1055 | new_name = Urn(new_name).filename() 1056 | new_path = "{directory}{filename}".format(directory=parent_path, filename=new_name) 1057 | 1058 | self.client.move(remote_path_from=old_path, remote_path_to=new_path) 1059 | self.urn = Urn(new_path) 1060 | 1061 | def move(self, remote_path): 1062 | 1063 | new_urn = Urn(remote_path) 1064 | self.client.move(remote_path_from=self.urn.path(), remote_path_to=new_urn.path()) 1065 | self.urn = new_urn 1066 | 1067 | def copy(self, remote_path): 1068 | 1069 | urn = Urn(remote_path) 1070 | self.client.copy(remote_path_from=self.urn.path(), remote_path_to=remote_path) 1071 | return Resource(self.client, urn) 1072 | 1073 | def info(self, params=None): 1074 | 1075 | info = self.client.info(self.urn.path()) 1076 | if not params: 1077 | return info 1078 | 1079 | return {key: value for (key, value) in info.items() if key in params} 1080 | 1081 | def clean(self): 1082 | return self.client.clean(self.urn.path()) 1083 | 1084 | def check(self): 1085 | return self.client.check(self.urn.path()) 1086 | 1087 | def read_from(self, buff): 1088 | self.client.upload_from(buff=buff, remote_path=self.urn.path()) 1089 | 1090 | def read(self, local_path): 1091 | return self.client.upload_sync(local_path=local_path, remote_path=self.urn.path()) 1092 | 1093 | def read_async(self, local_path, callback=None): 1094 | return self.client.upload_async(local_path=local_path, remote_path=self.urn.path(), callback=callback) 1095 | 1096 | def write_to(self, buff): 1097 | return self.client.download_to(buff=buff, remote_path=self.urn.path()) 1098 | 1099 | def write(self, local_path): 1100 | return self.client.download_sync(local_path=local_path, remote_path=self.urn.path()) 1101 | 1102 | def write_async(self, local_path, callback=None): 1103 | return self.client.download_async(local_path=local_path, remote_path=self.urn.path(), callback=callback) 1104 | 1105 | def publish(self): 1106 | return self.client.publish(self.urn.path()) 1107 | 1108 | def unpublish(self): 1109 | return self.client.unpublish(self.urn.path()) 1110 | 1111 | @property 1112 | def property(self, option): 1113 | return self.client.get_property(remote_path=self.urn.path(), option=option) 1114 | 1115 | @property.setter 1116 | def property(self, option, value): 1117 | option['value'] = value.__str__() 1118 | self.client.set_property(remote_path=self.urn.path(), option=option) 1119 | -------------------------------------------------------------------------------- /webdav/connection.py: -------------------------------------------------------------------------------- 1 | 2 | from webdav.exceptions import * 3 | from webdav.urn import Urn 4 | from os.path import exists 5 | 6 | class ConnectionSettings: 7 | 8 | def is_valid(self): 9 | pass 10 | 11 | def valid(self): 12 | 13 | try: 14 | self.is_valid() 15 | except OptionNotValid: 16 | return False 17 | else: 18 | return True 19 | 20 | class WebDAVSettings(ConnectionSettings): 21 | 22 | ns = "webdav:" 23 | prefix = "webdav_" 24 | keys = {'hostname', 'login', 'password', 'token', 'root', 'cert_path', 'key_path', 'recv_speed', 'send_speed', 'verbose'} 25 | 26 | def __init__(self, options): 27 | 28 | self.options = dict() 29 | 30 | for key in self.keys: 31 | value = options.get(key, '') 32 | self.options[key] = value 33 | self.__dict__[key] = value 34 | 35 | self.root = Urn(self.root).quote() if self.root else '' 36 | self.root = self.root.rstrip(Urn.separate) 37 | 38 | def is_valid(self): 39 | 40 | if not self.hostname: 41 | raise OptionNotValid(name="hostname", value=self.hostname, ns=self.ns) 42 | 43 | if self.cert_path and not exists(self.cert_path): 44 | raise OptionNotValid(name="cert_path", value=self.cert_path, ns=self.ns) 45 | 46 | if self.key_path and not exists(self.key_path): 47 | raise OptionNotValid(name="key_path", value=self.key_path, ns=self.ns) 48 | 49 | if self.key_path and not self.cert_path: 50 | raise OptionNotValid(name="cert_path", value=self.cert_path, ns=self.ns) 51 | 52 | if self.password and not self.login: 53 | raise OptionNotValid(name="login", value=self.login, ns=self.ns) 54 | 55 | if not self.token and not self.login: 56 | raise OptionNotValid(name="login", value=self.login, ns=self.ns) 57 | 58 | 59 | class ProxySettings(ConnectionSettings): 60 | 61 | ns = "proxy:" 62 | prefix = "proxy_" 63 | keys = {'hostname', 'login', 'password'} 64 | 65 | def __init__(self, options): 66 | 67 | self.options = dict() 68 | 69 | for key in self.keys: 70 | value = options.get(key, '') 71 | self.options[key] = value 72 | self.__dict__[key] = value 73 | 74 | def is_valid(self): 75 | 76 | if self.password and not self.login: 77 | raise OptionNotValid(name="login", value=self.login, ns=self.ns) 78 | 79 | if self.login or self.password: 80 | if not self.hostname: 81 | raise OptionNotValid(name="hostname", value=self.hostname, ns=self.ns) 82 | -------------------------------------------------------------------------------- /webdav/exceptions.py: -------------------------------------------------------------------------------- 1 | class WebDavException(Exception): 2 | pass 3 | 4 | 5 | class NotValid(WebDavException): 6 | pass 7 | 8 | 9 | class OptionNotValid(NotValid): 10 | def __init__(self, name, value, ns=""): 11 | self.name = name 12 | self.value = value 13 | self.ns = ns 14 | 15 | def __str__(self): 16 | return "Option ({ns}{name}={value}) have invalid name or value".format(ns=self.ns, name=self.name, value=self.value) 17 | 18 | 19 | class CertificateNotValid(NotValid): 20 | pass 21 | 22 | 23 | class NotFound(WebDavException): 24 | pass 25 | 26 | 27 | class LocalResourceNotFound(NotFound): 28 | def __init__(self, path): 29 | self.path = path 30 | 31 | def __str__(self): 32 | return "Local file: {path} not found".format(path=self.path) 33 | 34 | 35 | class RemoteResourceNotFound(NotFound): 36 | def __init__(self, path): 37 | self.path = path 38 | 39 | def __str__(self): 40 | return "Remote resource: {path} not found".format(path=self.path) 41 | 42 | 43 | class RemoteParentNotFound(NotFound): 44 | def __init__(self, path): 45 | self.path = path 46 | 47 | def __str__(self): 48 | return "Remote parent for: {path} not found".format(path=self.path) 49 | 50 | 51 | class MethodNotSupported(WebDavException): 52 | def __init__(self, name, server): 53 | self.name = name 54 | self.server = server 55 | 56 | def __str__(self): 57 | return "Method {name} not supported for {server}".format(name=self.name, server=self.server) 58 | 59 | 60 | class NotConnection(WebDavException): 61 | def __init__(self, hostname): 62 | self.hostname = hostname 63 | 64 | def __str__(self): 65 | return "Not connection with {hostname}".format(hostname=self.hostname) 66 | 67 | 68 | class NotEnoughSpace(WebDavException): 69 | def __init__(self): 70 | pass 71 | 72 | def __str__(self): 73 | return "Not enough space on the server" -------------------------------------------------------------------------------- /webdav/urn.py: -------------------------------------------------------------------------------- 1 | try: 2 | from urllib.parse import unquote, quote 3 | except ImportError: 4 | from urllib import unquote, quote 5 | 6 | from re import sub 7 | 8 | 9 | class Urn(object): 10 | 11 | separate = "/" 12 | 13 | def __init__(self, path, directory=False): 14 | 15 | self._path = quote(path) 16 | expressions = "/\.+/", "/+" 17 | for expression in expressions: 18 | self._path = sub(expression, Urn.separate, self._path) 19 | 20 | if not self._path.startswith(Urn.separate): 21 | self._path = "{begin}{end}".format(begin=Urn.separate, end=self._path) 22 | 23 | if directory and not self._path.endswith(Urn.separate): 24 | self._path = "{begin}{end}".format(begin=self._path, end=Urn.separate) 25 | 26 | def __str__(self): 27 | return self.path() 28 | 29 | def path(self): 30 | return unquote(self._path) 31 | 32 | def quote(self): 33 | return self._path 34 | 35 | def filename(self): 36 | 37 | path_split = self._path.split(Urn.separate) 38 | name = path_split[-2] + Urn.separate if path_split[-1] == '' else path_split[-1] 39 | return unquote(name) 40 | 41 | def parent(self): 42 | 43 | path_split = self._path.split(Urn.separate) 44 | nesting_level = self.nesting_level() 45 | parent_path_split = path_split[:nesting_level] 46 | parent = self.separate.join(parent_path_split) if nesting_level != 1 else Urn.separate 47 | if not parent.endswith(Urn.separate): 48 | return unquote(parent + Urn.separate) 49 | else: 50 | return unquote(parent) 51 | 52 | def nesting_level(self): 53 | return self._path.count(Urn.separate, 0, -1) 54 | 55 | def is_dir(self): 56 | return self._path[-1] == Urn.separate --------------------------------------------------------------------------------