Example #1
0
 def test_connection_settings_no_token_and_no_login(self):
     options = {'webdav_hostname': 'http://localhost:8585'}
     webdav_options = get_options(option_type=WebDAVSettings,
                                  from_options=options)
     settings = WebDAVSettings(webdav_options)
     self.assertRaises(OptionNotValid, settings.is_valid)
     self.assertFalse(settings.valid())
Example #2
0
    def __init__(self, options):
        """Constructor of WebDAV client

        :param options: the dictionary of connection options to WebDAV.
            WebDev settings:
            `webdav_hostname`: url for WebDAV server should contain protocol and ip address or domain name.
                               Example: `https://webdav.server.com`.
            `webdav_login`: (optional) login name for WebDAV server can be empty in case using of token auth.
            `webdav_password`: (optional) password for WebDAV server can be empty in case using of token auth.
            `webdav_token': (optional) token for WebDAV server can be empty in case using of login/password auth.
            `webdav_root`: (optional) root directory of WebDAV server. Defaults is `/`.
            `webdav_cert_path`: (optional) path to certificate.
            `webdav_key_path`: (optional) path to private key.
            `webdav_recv_speed`: (optional) rate limit data download speed in Bytes per second.
                                 Defaults to unlimited speed.
            `webdav_send_speed`: (optional) rate limit data upload speed in Bytes per second.
                                 Defaults to unlimited speed.
            `webdav_verbose`: (optional) set verbose mode on.off. By default verbose mode is off.

        """
        self.session = requests.Session()
        self.http_header = Client.default_http_header.copy()
        self.requests = Client.default_requests.copy()
        webdav_options = get_options(option_type=WebDAVSettings,
                                     from_options=options)

        self.webdav = WebDAVSettings(webdav_options)
        self.requests.update(self.webdav.override_methods)
        self.default_options = {}
        self.timeout = self.webdav.timeout
Example #3
0
 def test_connection_settings_no_hostname(self):
     options = {'webdav_login': '******', 'webdav_password': '******'}
     webdav_options = get_options(option_type=WebDAVSettings,
                                  from_options=options)
     settings = WebDAVSettings(webdav_options)
     self.assertRaises(OptionNotValid, settings.is_valid)
     self.assertFalse(settings.valid())
 def test_connection_settings_anonymous_login(self):
     options = {
         'webdav_hostname': 'http://localhost:8585'
     }
     webdav_options = get_options(option_type=WebDAVSettings, from_options=options)
     settings = WebDAVSettings(webdav_options)
     self.assertTrue(settings.valid())
 def test_connection_settings_valid(self):
     options = {
         'webdav_hostname': 'http://localhost:8585',
         'webdav_login': '******',
         'webdav_password': '******'
     }
     webdav_options = get_options(option_type=WebDAVSettings, from_options=options)
     settings = WebDAVSettings(webdav_options)
     self.assertTrue(settings.is_valid())
     self.assertTrue(settings.valid())
 def test_connection_settings_with_key_path_an_no_cert_path(self):
     options = {
         'webdav_hostname': 'http://localhost:8585',
         'webdav_login': '******',
         'key_path': './publish.sh',
         'webdav_password': '******'
     }
     webdav_options = get_options(option_type=WebDAVSettings, from_options=options)
     settings = WebDAVSettings(webdav_options)
     self.assertRaises(OptionNotValid, settings.is_valid)
     self.assertFalse(settings.valid())
 def test_connection_settings_timeout_default(self):
     options = {
         'webdav_hostname': 'http://localhost:8585',
         'webdav_login': '******',
         'webdav_password': '******'
     }
     webdav_options = get_options(option_type=WebDAVSettings, from_options=options)
     settings = WebDAVSettings(webdav_options)
     self.assertEqual(30, settings.timeout)
Example #8
0
class Client(object):
    """The client for WebDAV servers provides an ability to control files on remote WebDAV server.
    """
    # path to root directory of WebDAV
    root = '/'

    # controls whether to verify the server's TLS certificate or not
    verify = True

    # HTTP headers for different actions
    default_http_header = {
        'list': ["Accept: */*", "Depth: 1"],
        'free': ["Accept: */*", "Depth: 0", "Content-Type: text/xml"],
        'copy': ["Accept: */*"],
        'move': ["Accept: */*"],
        'mkdir': ["Accept: */*", "Connection: Keep-Alive"],
        'clean': ["Accept: */*", "Connection: Keep-Alive"],
        'check': ["Accept: */*"],
        'info': ["Accept: */*", "Depth: 1"],
        'get_property': [
            "Accept: */*", "Depth: 1",
            "Content-Type: application/x-www-form-urlencoded"
        ],
        'set_property': [
            "Accept: */*", "Depth: 1",
            "Content-Type: application/x-www-form-urlencoded"
        ]
    }

    # mapping of actions to WebDAV methods
    default_requests = {
        'options': 'OPTIONS',
        'download': "GET",
        'upload': "PUT",
        'copy': "COPY",
        'move': "MOVE",
        'mkdir': "MKCOL",
        'clean': "DELETE",
        'check': "HEAD",
        'list': "PROPFIND",
        'free': "PROPFIND",
        'info': "PROPFIND",
        'publish': "PROPPATCH",
        'unpublish': "PROPPATCH",
        'published': "PROPPATCH",
        'get_property': "PROPFIND",
        'set_property': "PROPPATCH"
    }

    meta_xmlns = {
        'https://webdav.yandex.ru': "urn:yandex:disk:meta",
    }

    def __init__(self, options):
        """Constructor of WebDAV client

        :param options: the dictionary of connection options to WebDAV.
            WebDev settings:
            `webdav_hostname`: url for WebDAV server should contain protocol and ip address or domain name.
                               Example: `https://webdav.server.com`.
            `webdav_login`: (optional) login name for WebDAV server can be empty in case using of token auth.
            `webdav_password`: (optional) password for WebDAV server can be empty in case using of token auth.
            `webdav_token': (optional) token for WebDAV server can be empty in case using of login/password auth.
            `webdav_root`: (optional) root directory of WebDAV server. Defaults is `/`.
            `webdav_cert_path`: (optional) path to certificate.
            `webdav_key_path`: (optional) path to private key.
            `webdav_recv_speed`: (optional) rate limit data download speed in Bytes per second.
                                 Defaults to unlimited speed.
            `webdav_send_speed`: (optional) rate limit data upload speed in Bytes per second.
                                 Defaults to unlimited speed.
            `webdav_verbose`: (optional) set verbose mode on.off. By default verbose mode is off.

        """
        self.session = requests.Session()
        self.http_header = Client.default_http_header.copy()
        self.requests = Client.default_requests.copy()
        webdav_options = get_options(option_type=WebDAVSettings,
                                     from_options=options)

        self.webdav = WebDAVSettings(webdav_options)
        self.requests.update(self.webdav.override_methods)
        self.default_options = {}
        self.timeout = self.webdav.timeout

    def get_headers(self, action, headers_ext=None):
        """Returns HTTP headers of specified WebDAV actions.

        :param action: the identifier of action.
        :param headers_ext: (optional) the addition headers list witch sgould be added to basic HTTP headers for
                            the specified action.
        :return: the dictionary of headers for specified action.
        """
        if action in self.http_header:
            try:
                headers = self.http_header[action].copy()
            except AttributeError:
                headers = self.http_header[action][:]
        else:
            headers = list()

        if headers_ext:
            headers.extend(headers_ext)

        if self.webdav.token:
            webdav_token = "Authorization: Bearer {token}".format(
                token=self.webdav.token)
            headers.append(webdav_token)
        return dict(
            [map(lambda s: s.strip(), i.split(':', 1)) for i in headers])

    def get_url(self, path):
        """Generates url by uri path.

        :param path: uri path.
        :return: the url string.
        """
        url = {
            'hostname': self.webdav.hostname,
            'root': self.webdav.root,
            'path': path
        }
        return "{hostname}{root}{path}".format(**url)

    def get_full_path(self, urn):
        """Generates full path to remote resource exclude hostname.

        :param urn: the URN to resource.
        :return: full path to resource with root path.
        """
        return "{root}{path}".format(root=self.webdav.root, path=urn.path())

    def execute_request(self, action, path, data=None, headers_ext=None):
        """Generate request to WebDAV server for specified action and path and execute it.

        :param action: the action for WebDAV server which should be executed.
        :param path: the path to resource for action
        :param data: (optional) Dictionary or list of tuples ``[(key, value)]`` (will be form-encoded), bytes,
                     or file-like object to send in the body of the :class:`Request`.
        :param headers_ext: (optional) the addition headers list witch should be added to basic HTTP headers for
                            the specified action.
        :return: HTTP response of request.
        """
        if self.session.auth:
            self.session.request(
                method="GET",
                url=self.webdav.hostname,
                verify=self.verify,
                timeout=self.timeout)  # (Re)Authenticates against the proxy
        response = self.session.request(
            method=self.requests[action],
            url=self.get_url(path),
            auth=(self.webdav.login, self.webdav.password) if
            (not self.webdav.token and not self.session.auth) else None,
            headers=self.get_headers(action, headers_ext),
            timeout=self.timeout,
            cert=(self.webdav.cert_path, self.webdav.key_path) if
            (self.webdav.cert_path and self.webdav.key_path) else None,
            data=data,
            stream=True,
            verify=self.verify)
        if response.status_code == 507:
            raise NotEnoughSpace()
        if response.status_code == 404:
            raise RemoteResourceNotFound(path=path)
        if response.status_code == 405:
            raise MethodNotSupported(name=action, server=self.webdav.hostname)
        if response.status_code >= 400:
            raise ResponseErrorCode(url=self.get_url(path),
                                    code=response.status_code,
                                    message=response.content)
        return response

    def valid(self):
        """Validates of WebDAV settings.

        :return: True in case settings are valid and False otherwise.
        """
        return True if self.webdav.valid() else False

    @wrap_connection_error
    def list(self, remote_path=root, get_info=False):
        """Returns list of nested files and directories for remote WebDAV directory by path.
        More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_PROPFIND

        :param remote_path: path to remote directory.
        :param get_info: path and element info to remote directory, like cmd 'ls -l'.
        :return: if get_info=False it returns list of nested file or directory names, otherwise it returns
                 list of information, the information is a dictionary and it values with following keys:
                 `created`: date of resource creation,
                 `name`: name of resource,
                 `size`: size of resource,
                 `modified`: date of resource modification,
                 `etag`: etag of resource,
                 `isdir`: type of resource,
                 `path`: path of resource.
                 
        """
        directory_urn = Urn(remote_path, directory=True)
        if directory_urn.path() != Client.root and not self.check(
                directory_urn.path()):
            raise RemoteResourceNotFound(directory_urn.path())

        path = Urn.normalize_path(self.get_full_path(directory_urn))
        response = self.execute_request(action='list',
                                        path=directory_urn.quote())
        if get_info:
            subfiles = WebDavXmlUtils.parse_get_list_info_response(
                response.content)
            return [
                subfile for subfile in subfiles
                if Urn.compare_path(path, subfile.get('path')) is False
            ]

        urns = WebDavXmlUtils.parse_get_list_response(response.content)

        return [
            urn.filename() for urn in urns
            if Urn.compare_path(path, urn.path()) is False
        ]

    @wrap_connection_error
    def free(self):
        """Returns an amount of free space on remote WebDAV server.
        More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_PROPFIND

        :return: an amount of free space in bytes.
        """
        data = WebDavXmlUtils.create_free_space_request_content()
        response = self.execute_request(action='free', path='', data=data)
        return WebDavXmlUtils.parse_free_space_response(
            response.content, self.webdav.hostname)

    @wrap_connection_error
    def check(self, remote_path=root):
        """Checks an existence of remote resource on WebDAV server by remote path.
        More information you can find by link http://webdav.org/specs/rfc4918.html#rfc.section.9.4

        :param remote_path: (optional) path to resource on WebDAV server. Defaults is root directory of WebDAV.
        :return: True if resource is exist or False otherwise
        """
        if self.webdav.disable_check:
            return True

        urn = Urn(remote_path)
        try:
            response = self.execute_request(action='check', path=urn.quote())
        except RemoteResourceNotFound:
            return False
        except ResponseErrorCode:
            return False

        if int(response.status_code) == 200:
            return True
        return False

    @wrap_connection_error
    def mkdir(self, remote_path):
        """Makes new directory on WebDAV server.
        More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_MKCOL

        :param remote_path: path to directory
        :return: True if request executed with code 200 or 201 and False otherwise.

        """
        directory_urn = Urn(remote_path, directory=True)
        if not self.check(directory_urn.parent()):
            raise RemoteParentNotFound(directory_urn.path())

        try:
            response = self.execute_request(action='mkdir',
                                            path=directory_urn.quote())
        except MethodNotSupported:
            # Yandex WebDAV returns 405 status code when directory already exists
            return True
        return response.status_code in (200, 201)

    @wrap_connection_error
    def download_from(self, buff, remote_path):
        """Downloads file from WebDAV and writes it in buffer.

        :param buff: buffer object for writing of downloaded file content.
        :param remote_path: path to file on WebDAV server.
        """
        urn = Urn(remote_path)
        if self.is_dir(urn.path()):
            raise OptionNotValid(name="remote_path", value=remote_path)

        if not self.check(urn.path()):
            raise RemoteResourceNotFound(urn.path())

        response = self.execute_request(action='download', path=urn.quote())
        for chunk in response.iter_content(chunk_size=128):
            buff.write(chunk)

    def download(self, remote_path, local_path, progress=None):
        """Downloads remote resource from WebDAV and save it in local path.
        More information you can find by link http://webdav.org/specs/rfc4918.html#rfc.section.9.4

        :param remote_path: the path to remote resource for downloading can be file and directory.
        :param local_path: the path to save resource locally.
        :param progress: progress function. Not supported now.
        """
        urn = Urn(remote_path)
        if self.is_dir(urn.path()):
            self.download_directory(local_path=local_path,
                                    remote_path=remote_path,
                                    progress=progress)
        else:
            self.download_file(local_path=local_path,
                               remote_path=remote_path,
                               progress=progress)

    def download_directory(self, remote_path, local_path, progress=None):
        """Downloads directory and downloads all nested files and directories from remote WebDAV to local.
        If there is something on local path it deletes directories and files then creates new.

        :param remote_path: the path to directory for downloading form WebDAV server.
        :param local_path: the path to local directory for saving downloaded files and directories.
        :param progress: Progress function. Not supported now.
        """
        urn = Urn(remote_path, directory=True)
        if not self.is_dir(urn.path()):
            raise OptionNotValid(name="remote_path", value=remote_path)

        if os.path.exists(local_path):
            shutil.rmtree(local_path)

        os.makedirs(local_path)

        for resource_name in self.list(urn.path()):
            if urn.path().endswith(resource_name):
                continue
            _remote_path = "{parent}{name}".format(parent=urn.path(),
                                                   name=resource_name)
            _local_path = os.path.join(local_path, resource_name)
            self.download(local_path=_local_path,
                          remote_path=_remote_path,
                          progress=progress)

    @wrap_connection_error
    def download_file(self, remote_path, local_path, progress=None):
        """Downloads file from WebDAV server and save it locally.
        More information you can find by link http://webdav.org/specs/rfc4918.html#rfc.section.9.4

        :param remote_path: the path to remote file for downloading.
        :param local_path: the path to save file locally.
        :param progress: progress function. Not supported now.
        """
        urn = Urn(remote_path)
        if self.is_dir(urn.path()):
            raise OptionNotValid(name="remote_path", value=remote_path)

        if os.path.isdir(local_path):
            raise OptionNotValid(name="local_path", value=local_path)

        if not self.check(urn.path()):
            raise RemoteResourceNotFound(urn.path())

        with open(local_path, 'wb') as local_file:
            response = self.execute_request('download', urn.quote())
            for block in response.iter_content(1024):
                local_file.write(block)

    def download_sync(self, remote_path, local_path, callback=None):
        """Downloads remote resources from WebDAV server synchronously.

        :param remote_path: the path to remote resource on WebDAV server. Can be file and directory.
        :param local_path: the path to save resource locally.
        :param callback: the callback which will be invoked when downloading is complete.
        """
        self.download(local_path=local_path, remote_path=remote_path)
        if callback:
            callback()

    def download_async(self, remote_path, local_path, callback=None):
        """Downloads remote resources from WebDAV server asynchronously

        :param remote_path: the path to remote resource on WebDAV server. Can be file and directory.
        :param local_path: the path to save resource locally.
        :param callback: the callback which will be invoked when downloading is complete.
        """
        target = (lambda: self.download_sync(
            local_path=local_path, remote_path=remote_path, callback=callback))
        threading.Thread(target=target).start()

    @wrap_connection_error
    def upload_to(self, buff, remote_path):
        """Uploads file from buffer to remote path on WebDAV server.
        More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_PUT

        :param buff: the buffer with content for file.
        :param remote_path: the path to save file remotely on WebDAV server.
        """
        urn = Urn(remote_path)
        if urn.is_dir():
            raise OptionNotValid(name="remote_path", value=remote_path)

        if not self.check(urn.parent()):
            raise RemoteParentNotFound(urn.path())

        self.execute_request(action='upload', path=urn.quote(), data=buff)

    def upload(self, remote_path, local_path, progress=None):
        """Uploads resource to remote path on WebDAV server.
        In case resource is directory it will upload all nested files and directories.
        More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_PUT

        :param remote_path: the path for uploading resources on WebDAV server. Can be file and directory.
        :param local_path: the path to local resource for uploading.
        :param progress: Progress function. Not supported now.
        """
        if os.path.isdir(local_path):
            self.upload_directory(local_path=local_path,
                                  remote_path=remote_path,
                                  progress=progress)
        else:
            self.upload_file(local_path=local_path, remote_path=remote_path)

    def upload_directory(self, remote_path, local_path, progress=None):
        """Uploads directory to remote path on WebDAV server.
        In case directory is exist on remote server it will delete it and then upload directory with nested files and
        directories.

        :param remote_path: the path to directory for uploading on WebDAV server.
        :param local_path: the path to local directory for uploading.
        :param progress: Progress function. Not supported now.
        """
        urn = Urn(remote_path, directory=True)
        if not urn.is_dir():
            raise OptionNotValid(name="remote_path", value=remote_path)

        if not os.path.isdir(local_path):
            raise OptionNotValid(name="local_path", value=local_path)

        if not os.path.exists(local_path):
            raise LocalResourceNotFound(local_path)

        if self.check(urn.path()):
            self.clean(urn.path())

        self.mkdir(remote_path)

        for resource_name in listdir(local_path):
            _remote_path = "{parent}{name}".format(parent=urn.path(),
                                                   name=resource_name).replace(
                                                       '\\', '')
            _local_path = os.path.join(local_path, resource_name)
            self.upload(local_path=_local_path,
                        remote_path=_remote_path,
                        progress=progress)

    @wrap_connection_error
    def upload_file(self, remote_path, local_path, progress=None):
        """Uploads file to remote path on WebDAV server. File should be 2Gb or less.
        More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_PUT

        :param remote_path: the path to uploading file on WebDAV server.
        :param local_path: the path to local file for uploading.
        :param progress: Progress function. Not supported now.
        """
        if not os.path.exists(local_path):
            raise LocalResourceNotFound(local_path)

        urn = Urn(remote_path)
        if urn.is_dir():
            raise OptionNotValid(name="remote_path", value=remote_path)

        if os.path.isdir(local_path):
            raise OptionNotValid(name="local_path", value=local_path)

        if not self.check(urn.parent()):
            raise RemoteParentNotFound(urn.path())

        with open(local_path, "rb") as local_file:
            self.execute_request(action='upload',
                                 path=urn.quote(),
                                 data=local_file)

    def upload_sync(self, remote_path, local_path, callback=None):
        """Uploads resource to remote path on WebDAV server synchronously.
        In case resource is directory it will upload all nested files and directories.

        :param remote_path: the path for uploading resources on WebDAV server. Can be file and directory.
        :param local_path: the path to local resource for uploading.
        :param callback: the callback which will be invoked when downloading is complete.
        """
        self.upload(local_path=local_path, remote_path=remote_path)

        if callback:
            callback()

    def upload_async(self, remote_path, local_path, callback=None):
        """Uploads resource to remote path on WebDAV server asynchronously.
        In case resource is directory it will upload all nested files and directories.

        :param remote_path: the path for uploading resources on WebDAV server. Can be file and directory.
        :param local_path: the path to local resource for uploading.
        :param callback: the callback which will be invoked when downloading is complete.
        """
        target = (lambda: self.upload_sync(
            local_path=local_path, remote_path=remote_path, callback=callback))
        threading.Thread(target=target).start()

    @wrap_connection_error
    def copy(self, remote_path_from, remote_path_to, depth=1):
        """Copies resource from one place to another on WebDAV server.
        More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_COPY

        :param remote_path_from: the path to resource which will be copied,
        :param remote_path_to: the path where resource will be copied.
        :param depth: folder depth to copy
        """
        urn_from = Urn(remote_path_from)
        if not self.check(urn_from.path()):
            raise RemoteResourceNotFound(urn_from.path())

        urn_to = Urn(remote_path_to)
        if not self.check(urn_to.parent()):
            raise RemoteParentNotFound(urn_to.path())

        headers = [
            "Destination: {url}".format(url=self.get_url(urn_to.quote()))
        ]
        if self.is_dir(urn_from.path()):
            headers.append("Depth: {depth}".format(depth=depth))
        self.execute_request(action='copy',
                             path=urn_from.quote(),
                             headers_ext=headers)

    @wrap_connection_error
    def move(self, remote_path_from, remote_path_to, overwrite=False):
        """Moves resource from one place to another on WebDAV server.
        More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_MOVE

        :param remote_path_from: the path to resource which will be moved,
        :param remote_path_to: the path where resource will be moved.
        :param overwrite: (optional) the flag, overwrite file if it exists. Defaults is False
        """
        urn_from = Urn(remote_path_from)
        if not self.check(urn_from.path()):
            raise RemoteResourceNotFound(urn_from.path())

        urn_to = Urn(remote_path_to)
        if not self.check(urn_to.parent()):
            raise RemoteParentNotFound(urn_to.path())

        header_destination = "Destination: {path}".format(
            path=self.get_url(urn_to.quote()))
        header_overwrite = "Overwrite: {flag}".format(
            flag="T" if overwrite else "F")
        self.execute_request(
            action='move',
            path=urn_from.quote(),
            headers_ext=[header_destination, header_overwrite])

    @wrap_connection_error
    def clean(self, remote_path):
        """Cleans (Deletes) a remote resource on WebDAV server. The name of method is not changed for back compatibility
        with original library.
        More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_DELETE

        :param remote_path: the remote resource whisch will be deleted.
        """
        urn = Urn(remote_path)
        self.execute_request(action='clean', path=urn.quote())

    @wrap_connection_error
    def info(self, remote_path):
        """Gets information about resource on WebDAV.
        More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_PROPFIND

        :param remote_path: the path to remote resource.
        :return: a dictionary of information attributes and them values with following keys:
                 `created`: date of resource creation,
                 `name`: name of resource,
                 `size`: size of resource,
                 `modified`: date of resource modification,
                 `etag`: etag of resource.
        """
        urn = Urn(remote_path)
        self._check_remote_resource(remote_path, urn)

        response = self.execute_request(action='info', path=urn.quote())
        path = self.get_full_path(urn)
        return WebDavXmlUtils.parse_info_response(
            content=response.content, path=path, hostname=self.webdav.hostname)

    def _check_remote_resource(self, remote_path, urn):
        if not self.check(urn.path()) and not self.check(
                Urn(remote_path, directory=True).path()):
            raise RemoteResourceNotFound(remote_path)

    @wrap_connection_error
    def is_dir(self, remote_path):
        """Checks is the remote resource directory.
        More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_PROPFIND

        :param remote_path: the path to remote resource.
        :return: True in case the remote resource is directory and False otherwise.
        """
        urn = Urn(remote_path)
        parent_urn = Urn(urn.parent())
        self._check_remote_resource(remote_path, urn)

        response = self.execute_request(action='info', path=parent_urn.quote())
        path = self.get_full_path(urn)
        return WebDavXmlUtils.parse_is_dir_response(
            content=response.content, path=path, hostname=self.webdav.hostname)

    @wrap_connection_error
    def get_property(self, remote_path, option):
        """Gets metadata property of remote resource on WebDAV server.
        More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_PROPFIND

        :param remote_path: the path to remote resource.
        :param option: the property attribute as dictionary with following keys:
                       `namespace`: (optional) the namespace for XML property which will be set,
                       `name`: the name of property which will be set.
        :return: the value of property or None if property is not found.
        """
        urn = Urn(remote_path)
        if not self.check(urn.path()):
            raise RemoteResourceNotFound(urn.path())

        data = WebDavXmlUtils.create_get_property_request_content(option)
        response = self.execute_request(action='get_property',
                                        path=urn.quote(),
                                        data=data)
        return WebDavXmlUtils.parse_get_property_response(
            response.content, option['name'])

    @wrap_connection_error
    def set_property(self, remote_path, option):
        """Sets metadata property of remote resource on WebDAV server.
        More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_PROPPATCH

        :param remote_path: the path to remote resource.
        :param option: the property attribute as dictionary with following keys:
                       `namespace`: (optional) the namespace for XML property which will be set,
                       `name`: the name of property which will be set,
                       `value`: (optional) the value of property which will be set. Defaults is empty string.
        """
        self.set_property_batch(remote_path=remote_path, option=[option])

    @wrap_connection_error
    def set_property_batch(self, remote_path, option):
        """Sets batch metadata properties of remote resource on WebDAV server in batch.
        More information you can find by link http://webdav.org/specs/rfc4918.html#METHOD_PROPPATCH

        :param remote_path: the path to remote resource.
        :param option: the property attributes as list of dictionaries with following keys:
                       `namespace`: (optional) the namespace for XML property which will be set,
                       `name`: the name of property which will be set,
                       `value`: (optional) the value of property which will be set. Defaults is empty string.
        """
        urn = Urn(remote_path)
        if not self.check(urn.path()):
            raise RemoteResourceNotFound(urn.path())

        data = WebDavXmlUtils.create_set_property_batch_request_content(option)
        self.execute_request(action='set_property',
                             path=urn.quote(),
                             data=data)

    def resource(self, remote_path):
        urn = Urn(remote_path)
        return Resource(self, urn)

    def push(self, remote_directory, local_directory):
        def prune(src, exp):
            return [sub(exp, "", item) for item in src]

        updated = False
        urn = Urn(remote_directory, directory=True)
        self._validate_remote_directory(urn)
        self._validate_local_directory(local_directory)

        paths = self.list(urn.path())
        expression = "{begin}{end}".format(begin="^", end=urn.path())
        remote_resource_names = prune(paths, expression)

        for local_resource_name in listdir(local_directory):
            local_path = os.path.join(local_directory, local_resource_name)
            remote_path = "{remote_directory}{resource_name}".format(
                remote_directory=urn.path(), resource_name=local_resource_name)

            if os.path.isdir(local_path):
                if not self.check(remote_path=remote_path):
                    self.mkdir(remote_path=remote_path)
                result = self.push(remote_directory=remote_path,
                                   local_directory=local_path)
                updated = updated or result
            else:
                if local_resource_name in remote_resource_names and not self.is_local_more_recent(
                        local_path, remote_path):
                    continue
                self.upload_file(remote_path=remote_path,
                                 local_path=local_path)
                updated = True
            return updated

    def pull(self, remote_directory, local_directory):
        def prune(src, exp):
            return [sub(exp, "", item) for item in src]

        updated = False
        urn = Urn(remote_directory, directory=True)
        self._validate_remote_directory(urn)
        self._validate_local_directory(local_directory)

        local_resource_names = listdir(local_directory)

        paths = self.list(urn.path())
        expression = "{begin}{end}".format(begin="^", end=remote_directory)
        remote_resource_names = prune(paths, expression)

        for remote_resource_name in remote_resource_names:
            if urn.path().endswith(remote_resource_name):
                continue
            local_path = os.path.join(local_directory, remote_resource_name)
            remote_path = "{remote_directory}{resource_name}".format(
                remote_directory=urn.path(),
                resource_name=remote_resource_name)
            remote_urn = Urn(remote_path)

            if remote_urn.path().endswith("/"):
                if not os.path.exists(local_path):
                    updated = True
                    os.mkdir(local_path)
                result = self.pull(remote_directory=remote_path,
                                   local_directory=local_path)
                updated = updated or result
            else:
                if remote_resource_name in local_resource_names and self.is_local_more_recent(
                        local_path, remote_path):
                    continue

                self.download_file(remote_path=remote_path,
                                   local_path=local_path)
                updated = True
        return updated

    def is_local_more_recent(self, local_path, remote_path):
        """Tells if local resource is more recent that the remote on if possible

        :param local_path: the path to local resource.
        :param remote_path: the path to remote resource.

        :return: True if local resource is more recent, False if the remote one is
                 None if comparison is not possible
        """
        try:
            remote_info = self.info(remote_path)
            remote_last_mod_date = remote_info['modified']
            remote_last_mod_date = dateutil_parser.parse(remote_last_mod_date)
            remote_last_mod_date_unix_ts = int(
                remote_last_mod_date.timestamp())
            local_last_mod_date_unix_ts = int(os.stat(local_path).st_mtime)

            return remote_last_mod_date_unix_ts < local_last_mod_date_unix_ts
        except (ValueError, RuntimeWarning, KeyError):
            # If there is problem when parsing dates, or cannot get
            # last modified information, return None
            return None

    def sync(self, remote_directory, local_directory):
        self.pull(remote_directory=remote_directory,
                  local_directory=local_directory)
        self.push(remote_directory=remote_directory,
                  local_directory=local_directory)

    def _validate_remote_directory(self, urn):
        if not self.is_dir(urn.path()):
            raise OptionNotValid(name="remote_path", value=urn.path())

    @staticmethod
    def _validate_local_directory(local_directory):
        if not os.path.isdir(local_directory):
            raise OptionNotValid(name="local_path", value=local_directory)

        if not os.path.exists(local_directory):
            raise LocalResourceNotFound(local_directory)