Skip to content

Commit

Permalink
added easywebdav library
Browse files Browse the repository at this point in the history
  • Loading branch information
bluezed committed Feb 28, 2015
1 parent ef2e6f9 commit 6dbf193
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 0 deletions.
1 change: 1 addition & 0 deletions resources/lib/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

5 changes: 5 additions & 0 deletions resources/lib/easywebdav/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Copyright (c) 2012 year, Amnon Grossman

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
82 changes: 82 additions & 0 deletions resources/lib/easywebdav/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
EasyWebDAV: A WebDAV Client in Python
=====================================

Features
--------

* Basic authentication
* Creating directories, removing directories and files
* Uploading and downloading files
* Directory listing
* Support for client side SSL certificates

Installation
------------

Install using distribute:

easy_install easywebdav

Quick Start
-----------

import easywebdav
# Start off by creating a client object. Username and
# password may be omitted if no authentication is needed.
webdav = easywebdav.connect('webdav.your-domain.com', username='myuser', password='mypass')
# Do some stuff:
webdav.mkdir('some_dir')
webdav.rmdir('another_dir')
webdav.download('remote/path/to/file', 'local/target/file')
webdav.upload('local/path/to/file', 'remote/target/file')

Client object API
-----------------

The API is pretty much self-explanatory:

cd(path)
ls(path=None)
exists(remote_path)
mkdir(path, safe=False)
mkdirs(path)
rmdir(path, safe=False)
delete(file_path)
upload(local_path_or_fileobj, remote_path)
download(remote_path, local_path_or_fileobj)

Using clientside SSL certificate
--------------------------------

webdav = easywebdav.connect('secure.example.net',
username='user',
password='pass',
protocol='https',
cert="/path/to/your/certificate.pem")
# Do some stuff:
print webdav.ls()

Please note that all options and restriction regarding the "cert" parameter from
[Requests API](http://docs.python-requests.org/en/latest/api/) apply here as the parameter is only passed through!

Developing EasyWebDAV
---------------------

Working with a virtual environment is highly recommended:

virtualenv --no-site-packages easywebdav_dev
source easywebdav_dev/bin/activate

Installing the library in development-mode:

EASYWEBDAV_DEV=1 python setup.py develop

The first part of the command causes setup.py to install development dependencies, in addition to the normal dependencies.

Running the tests:

nosetests --with-yanc --nologcapture --nocapture tests

Running the tests with WebDAV server logs:

WEBDAV_LOGS=1 nosetests --with-yanc --nologcapture --nocapture -v tests
5 changes: 5 additions & 0 deletions resources/lib/easywebdav/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .client import *

def connect(*args, **kwargs):
"""connect(host, port=0, auth=None, username=None, password=None, protocol='http', path="/")"""
return Client(*args, **kwargs)
1 change: 1 addition & 0 deletions resources/lib/easywebdav/__version__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "1.2.0"
188 changes: 188 additions & 0 deletions resources/lib/easywebdav/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import requests
import platform
from numbers import Number
import xml.etree.cElementTree as xml
from collections import namedtuple

py_majversion, py_minversion, py_revversion = platform.python_version_tuple()

if py_majversion == '2':
from httplib import responses as HTTP_CODES
from urlparse import urlparse
else:
from http.client import responses as HTTP_CODES
from urllib.parse import urlparse

DOWNLOAD_CHUNK_SIZE_BYTES = 1 * 1024 * 1024

class WebdavException(Exception):
pass

class ConnectionFailed(WebdavException):
pass


def codestr(code):
return HTTP_CODES.get(code, 'UNKNOWN')


File = namedtuple('File', ['name', 'size', 'mtime', 'ctime', 'contenttype'])


def prop(elem, name, default=None):
child = elem.find('.//{DAV:}' + name)
return default if child is None else child.text


def elem2file(elem):
return File(
prop(elem, 'href'),
int(prop(elem, 'getcontentlength', 0)),
prop(elem, 'getlastmodified', ''),
prop(elem, 'creationdate', ''),
prop(elem, 'getcontenttype', ''),
)


class OperationFailed(WebdavException):
_OPERATIONS = dict(
HEAD = "get header",
GET = "download",
PUT = "upload",
DELETE = "delete",
MKCOL = "create directory",
PROPFIND = "list directory",
)

def __init__(self, method, path, expected_code, actual_code):
self.method = method
self.path = path
self.expected_code = expected_code
self.actual_code = actual_code
operation_name = self._OPERATIONS[method]
self.reason = 'Failed to {operation_name} "{path}"'.format(**locals())
expected_codes = (expected_code,) if isinstance(expected_code, Number) else expected_code
expected_codes_str = ", ".join('{0} {1}'.format(code, codestr(code)) for code in expected_codes)
actual_code_str = codestr(actual_code)
msg = '''\
{self.reason}.
Operation : {method} {path}
Expected code : {expected_codes_str}
Actual code : {actual_code} {actual_code_str}'''.format(**locals())
super(OperationFailed, self).__init__(msg)

class Client(object):
def __init__(self, host, port=0, auth=None, username=None, password=None,
protocol='http', verify_ssl=True, path=None, cert=None):
if not port:
port = 443 if protocol == 'https' else 80
self.baseurl = '{0}://{1}:{2}'.format(protocol, host, port)
if path:
self.baseurl = '{0}/{1}'.format(self.baseurl, path)
self.cwd = '/'
self.session = requests.session()
self.session.verify = verify_ssl
self.session.stream = True

if cert:
self.session.cert = cert

if auth:
self.session.auth = auth
elif username and password:
self.session.auth = (username, password)

def _send(self, method, path, expected_code, **kwargs):
url = self._get_url(path)
response = self.session.request(method, url, allow_redirects=False, **kwargs)
if isinstance(expected_code, Number) and response.status_code != expected_code \
or not isinstance(expected_code, Number) and response.status_code not in expected_code:
raise OperationFailed(method, path, expected_code, response.status_code)
return response

def _get_url(self, path):
path = str(path).strip()
if path.startswith('/'):
return self.baseurl + path
return "".join((self.baseurl, self.cwd, path))

def cd(self, path):
path = path.strip()
if not path:
return
stripped_path = '/'.join(part for part in path.split('/') if part) + '/'
if stripped_path == '/':
self.cwd = stripped_path
elif path.startswith('/'):
self.cwd = '/' + stripped_path
else:
self.cwd += stripped_path

def mkdir(self, path, safe=False):
expected_codes = 201 if not safe else (201, 301, 405)
self._send('MKCOL', path, expected_codes)

def mkdirs(self, path):
dirs = [d for d in path.split('/') if d]
if not dirs:
return
if path.startswith('/'):
dirs[0] = '/' + dirs[0]
old_cwd = self.cwd
try:
for dir in dirs:
try:
self.mkdir(dir, safe=True)
except Exception as e:
if e.actual_code == 409:
raise
finally:
self.cd(dir)
finally:
self.cd(old_cwd)

def rmdir(self, path, safe=False):
path = str(path).rstrip('/') + '/'
expected_codes = 204 if not safe else (204, 404)
self._send('DELETE', path, expected_codes)

def delete(self, path):
self._send('DELETE', path, 204)

def upload(self, local_path_or_fileobj, remote_path):
if isinstance(local_path_or_fileobj, basestring):
with open(local_path_or_fileobj, 'rb') as f:
self._upload(f, remote_path)
else:
self._upload(local_path_or_fileobj, remote_path)

def _upload(self, fileobj, remote_path):
self._send('PUT', remote_path, (200, 201, 204), data=fileobj)

def download(self, remote_path, local_path_or_fileobj):
response = self._send('GET', remote_path, 200, stream=True)
if isinstance(local_path_or_fileobj, basestring):
with open(local_path_or_fileobj, 'wb') as f:
self._download(f, response)
else:
self._download(local_path_or_fileobj, response)

def _download(self, fileobj, response):
for chunk in response.iter_content(DOWNLOAD_CHUNK_SIZE_BYTES):
fileobj.write(chunk)

def ls(self, remote_path='.'):
headers = {'Depth': '1'}
response = self._send('PROPFIND', remote_path, (207, 301), headers=headers)

# Redirect
if response.status_code == 301:
url = urlparse(response.headers['location'])
return self.ls(url.path)

tree = xml.fromstring(response.content)
return [elem2file(elem) for elem in tree.findall('{DAV:}response')]

def exists(self, remote_path):
response = self._send('HEAD', remote_path, (200, 301, 404))
return True if response.status_code != 404 else False

0 comments on commit 6dbf193

Please sign in to comment.