[Dextrose] [PATCH] 1-to-N Feature.
Anish Mangal
anish at activitycentral.com
Sun Apr 29 09:56:29 EDT 2012
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
Bug:
* Host adds files to share folder
* Client adds the host's share in journal view, journal entries are
visible
* Host disconnects from the network
* Client tries to copy/Client tries to access remote share folder
* Exception in logs
On Sun 29 Apr 2012 07:06:54 PM IST, Anish Mangal wrote:
> I've tested this patch (basic functionality), and it mostly works!
>
> I have already included the httpd OOB patch, and will be committing
> this one soon, and release in rpm so that future dev builds can
> contain it.
>
> Some thoughts about the design/patch:
> * It is a LOT cleaner than the previous 1-N feature, both in
> functionality and usability. People just share their entries (batch
> operations can be used to copy to the 'shares' folder on the host),
> and then batch operations on client device can be used to copy it back
> to the journal. Slick!
>
> * The notification in the bottom-right corner is in a somewhat
> non-standard location, but I can live with it, until the time we push
> this upstream.
>
> * Are the webdav/* files part of some standard API? Is it available as
> an rpm somewhere. If not, the packagers will take care of it, by
> packaging the files in sugar-webdav.rpm
>
> * Note to testers: Need to identify corner cases, and try to make the
> functionality break :-)
>
> Perhaps we need to come up with better icons for the shares folder.
> I'll work on that. The idea is to have the 'shares' folder icon look
> the same for host and client (on either the host or client), only
> differentiated by color-scheme. The icon should also be different that
> the documents folder icon.
>
> Folks requested to review the patch pls. The one quirk is that
> activities are launched from within the shares view, but Ajay has
> already mentioned that in his patch.
>
> Thanks for the patch, this looks awesome!!
>
> On 04/27/2012 03:23 AM, Ajay Garg wrote:
>> The feature specs, and workflow-screenshots, are listed at ::
>> http://wiki.sugarlabs.org/go/Features/Transfer_to_many_options
>>
>> TODO:
>>
>> a) Fix the bug :: For locally-mounted-remote-shares listview, the
>> activity/file should NOT be launched by clicking on the icon.
>>
>> configure.ac | 4 +- src/Makefile.am
>> | 4 +- src/jarabe/journal/journalactivity.py | 3 +
>> src/jarabe/journal/journaltoolbox.py | 7 +
>> src/jarabe/journal/model.py | 218 +++++++-
>> src/jarabe/journal/palettes.py | 49 ++
>> src/jarabe/journal/volumestoolbar.py | 80 +++-
>> src/jarabe/model/buddy.py | 11 +
>> src/jarabe/model/neighborhood.py | 3 +
>> src/jarabe/view/buddymenu.py | 11 +
>> src/jarabe/view/palettes.py | 33 +
>> src/webdav/Condition.py | 475 +++++++++++++++
>> src/webdav/Connection.py | 321 ++++++++++
>> src/webdav/Constants.py | 199 +++++++
>> src/webdav/Makefile.am | 20 +
>> src/webdav/NameCheck.py | 193 ++++++
>> src/webdav/Utils.py | 154 +++++
>> src/webdav/VersionHandler.py | 198 ++++++
>> src/webdav/WebdavClient.py | 1053
>> +++++++++++++++++++++++++++++++++ src/webdav/WebdavRequests.py
>> | 205 +++++++ src/webdav/WebdavResponse.py | 525
>> ++++++++++++++++ src/webdav/__init__.py | 16 +
>> src/webdav/acp/Ace.py | 293 +++++++++
>> src/webdav/acp/AceHandler.py | 182 ++++++
>> src/webdav/acp/Acl.py | 311 ++++++++++
>> src/webdav/acp/GrantDeny.py | 241 ++++++++
>> src/webdav/acp/Makefile.am | 12 +
>> src/webdav/acp/Principal.py | 189 ++++++
>> src/webdav/acp/Privilege.py | 125 ++++
>> src/webdav/acp/__init__.py | 33 + src/webdav/davlib.py
>> | 336 +++++++++++ src/webdav/logger.py | 51 ++
>> src/webdav/qp_xml.py | 240 ++++++++
>> src/webdav/uuid_.py | 476 +++++++++++++++ 34
>> files changed, 6263 insertions(+), 8 deletions(-) create mode
>> 100644 src/webdav/Condition.py create mode 100644
>> src/webdav/Connection.py create mode 100644
>> src/webdav/Constants.py create mode 100644 src/webdav/Makefile.am
>> create mode 100644 src/webdav/NameCheck.py create mode 100644
>> src/webdav/Utils.py create mode 100644
>> src/webdav/VersionHandler.py create mode 100644
>> src/webdav/WebdavClient.py create mode 100644
>> src/webdav/WebdavRequests.py create mode 100644
>> src/webdav/WebdavResponse.py create mode 100644
>> src/webdav/__init__.py create mode 100644 src/webdav/acp/Ace.py
>> create mode 100644 src/webdav/acp/AceHandler.py create mode 100644
>> src/webdav/acp/Acl.py create mode 100644
>> src/webdav/acp/GrantDeny.py create mode 100644
>> src/webdav/acp/Makefile.am create mode 100644
>> src/webdav/acp/Principal.py create mode 100644
>> src/webdav/acp/Privilege.py create mode 100644
>> src/webdav/acp/__init__.py create mode 100644 src/webdav/davlib.py
>> create mode 100644 src/webdav/logger.py create mode 100644
>> src/webdav/qp_xml.py create mode 100644 src/webdav/uuid_.py
>>
>> diff --git a/configure.ac b/configure.ac index 8e6d871..e04bdec
>> 100644 --- a/configure.ac +++ b/configure.ac @@ -61,8 +61,6 @@
>> extensions/cpsection/modemconfiguration/config.py
>> extensions/cpsection/Makefile
>> extensions/cpsection/network/Makefile
>> extensions/cpsection/power/Makefile
>> -extensions/cpsection/updater/backends/Makefile
>> -extensions/cpsection/updater/Makefile
>> extensions/deviceicon/Makefile extensions/globalkey/Makefile
>> extensions/Makefile @@ -79,6 +77,8 @@ src/jarabe/model/Makefile
>> src/jarabe/util/Makefile src/jarabe/util/telepathy/Makefile
>> src/jarabe/view/Makefile +src/webdav/acp/Makefile
>> +src/webdav/Makefile src/Makefile ])
>>
>> diff --git a/src/Makefile.am b/src/Makefile.am index
>> 83571a4..765da8b 100644 --- a/src/Makefile.am +++
>> b/src/Makefile.am @@ -1 +1,3 @@ -SUBDIRS = jarabe +SUBDIRS =
>> \ + jarabe \ + webdav diff --git
>> a/src/jarabe/journal/journalactivity.py
>> b/src/jarabe/journal/journalactivity.py index fa308cd..1b841e9
>> 100644 --- a/src/jarabe/journal/journalactivity.py +++
>> b/src/jarabe/journal/journalactivity.py @@ -485,6 +485,9 @@ class
>> JournalActivity(JournalWindow): def is_editing_mode_present(self):
>> return self._editing_mode
>>
>> + def get_volumes_toolbar(self): + return
>> self._volumes_toolbar +
>>
>> def get_journal(): global _journal diff --git
>> a/src/jarabe/journal/journaltoolbox.py
>> b/src/jarabe/journal/journaltoolbox.py index fd14826..c3614d7
>> 100644 --- a/src/jarabe/journal/journaltoolbox.py +++
>> b/src/jarabe/journal/journaltoolbox.py @@ -624,6 +624,13 @@ class
>> BatchEraseButton(ToolButton, palettes.ActionItem):
>> show_not_completed_ops_info=True) self.props.tooltip = _('Erase')
>>
>> + # De-sensitize Batch-Erase button, for
>> locally-mounted-remote-shares. + from
>> jarabe.journal.journalactivity import get_mount_point +
>> current_mount_point = get_mount_point() + + if
>> model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
>>
>>
> + self.set_sensitive(False)
>> + def _get_actionable_signal(self): return 'clicked'
>>
>> diff --git a/src/jarabe/journal/model.py
>> b/src/jarabe/journal/model.py index 83e216f..fe6dfc6 100644 ---
>> a/src/jarabe/journal/model.py +++ b/src/jarabe/journal/model.py @@
>> -20,6 +20,7 @@
>>
>> import logging import os +import stat import errno import
>> subprocess from datetime import datetime @@ -36,11 +37,15 @@ import
>> gobject import dbus import gio import gconf +import string
>>
>> from sugar import dispatch from sugar import mime from sugar import
>> util
>>
>> +from webdav.WebdavClient import get_remote_webdav_share_metadata
>> +from webdav.WebdavClient import
>> get_resource_by_ip_address_and_resource_key + DS_DBUS_SERVICE =
>> 'org.laptop.sugar.DataStore' DS_DBUS_INTERFACE =
>> 'org.laptop.sugar.DataStore' DS_DBUS_PATH =
>> '/org/laptop/sugar/DataStore' @@ -425,6 +430,123 @@ class
>> InplaceResultSet(BaseResultSet): return
>>
>>
>> +class RemoteShareResultSet(object): + def __init__(self,
>> ip_address, query): + self._ip_address = ip_address +
>> self._file_list = [] + + self.ready = dispatch.Signal() +
>> self.progress = dispatch.Signal() + + # First time, query is
>> none. + if query is None: + return + +
>> query_text = query.get('query', '') + if
>> query_text.startswith('"') and query_text.endswith('"'): +
>> self._regex = re.compile('*%s*' % query_text.strip(['"'])) +
>> elif query_text: + expression = '' + for word
>> in query_text.split(' '): + expression +=
>> '(?=.*%s.*)' % word + self._regex =
>> re.compile(expression, re.IGNORECASE) + else: +
>> self._regex = None + + if query.get('timestamp', ''): +
>> self._date_start = int(query['timestamp']['start']) +
>> self._date_end = int(query['timestamp']['end']) + else: +
>> self._date_start = None + self._date_end = None + +
>> self._mime_types = query.get('mime_type', []) + + self._sort
>> = query.get('order_by', ['+timestamp'])[0] + + def setup(self):
>> + metadata_list_complete =
>> get_remote_webdav_share_metadata(self._ip_address) + for
>> metadata in metadata_list_complete: + + add_to_list =
>> False + if self._regex is not None: + for
>> f in ['fulltext', 'title', +
>> 'description', 'tags']: + if f in metadata and
>> \ + self._regex.match(metadata[f]): +
>> add_to_list = True + break +
>> else: + add_to_list = True + if not
>> add_to_list: + continue + + add_to_list =
>> False + if self._date_start is not None: +
>> if metadata['timestamp'] > self._date_start: +
>> add_to_list = True + else: + add_to_list
>> = True + if not add_to_list: + continue
>> + + add_to_list = False + if self._date_end
>> is not None: + if metadata['timestamp'] <
>> self._date_end: + add_to_list = True +
>> else: + add_to_list = True + if not
>> add_to_list: + continue + + add_to_list =
>> False + if self._mime_types: + mime_type
>> = metadata['mime_type'] + if mime_type in
>> self._mime_types: + add_to_list = True +
>> else: + add_to_list = True + if not
>> add_to_list: + continue + + # If control
>> reaches here, the current metadata has passed + # out
>> all filter-tests. + file_info = (metadata['timestamp'],
>> + metadata['creation_time'], +
>> metadata['filesize'], + metadata) +
>> self._file_list.append(file_info) + + if self._sort[1:] ==
>> 'filesize': + keygetter = itemgetter(2) + elif
>> self._sort[1:] == 'creation_time': + keygetter =
>> itemgetter(1) + else: + # timestamp +
>> keygetter = itemgetter(0) + + self._file_list.sort(lambda a,
>> b: cmp(b, a), + key=keygetter, +
>> reverse=(self._sort[0] == '-')) + + self.ready.send(self) +
>> + def get_length(self): + return len(self._file_list) + +
>> length = property(get_length) + + def seek(self, position): +
>> self._position = position + + def read(self): +
>> modified_timestamp, creation_timestamp, filesize, metadata =
>> self._file_list[self._position] + return metadata + +
>> def stop(self): + self._stopped = True + + def
>> _get_file_metadata(path, stat, fetch_preview=True): """Return the
>> metadata from the corresponding file.
>>
>> @@ -436,10 +558,16 @@ def _get_file_metadata(path, stat,
>> fetch_preview=True): dir_path = os.path.dirname(path) metadata =
>> _get_file_metadata_from_json(dir_path, filename, fetch_preview) if
>> metadata: + # For Documents/Shares/Mounted-Drives. +
>> # Special case: for locally-mounted-remote-files, ensure that +
>> # "metadata['filesize' is already present before-hand. This +
>> # will have to be done at the time of fetching + #
>> webdav-properties per resource. if 'filesize' not in metadata:
>> metadata['filesize'] = stat.st_size return metadata
>>
>> + # For Journal. return {'uid': path, 'title':
>> os.path.basename(path), 'timestamp': stat.st_mtime, @@ -529,11
>> +657,33 @@ def find(query_, page_size): raise ValueError('Exactly
>> one mount point must be specified')
>>
>> if mount_points[0] == '/': + """ + For Journal. +
>> """ return DatastoreResultSet(query, page_size) + elif
>> is_mount_point_for_locally_mounted_remote_share(mount_points[0]): +
>> """ + For Locally-Mounted-Remote-Shares. + Regex
>> Matching is used, to ensure that the mount-point is an +
>> IP-Address. + """ + return
>> RemoteShareResultSet(mount_points[0], query) else: + """ +
>> For Documents/Shares/Mounted-Drives. + """ return
>> InplaceResultSet(query, page_size, mount_points[0])
>>
>>
>> +def is_mount_point_for_locally_mounted_remote_share(mount_point):
>> + import re + + pattern =
>> '[1-9][0-9]{0,2}\.[0-9]{0,3}\.[0-9]{0,3}\.[0-9]{0,3}' + if
>> re.match(pattern, mount_point) is None: + return False +
>> return True + + def _get_mount_point(path): dir_path =
>> os.path.dirname(path) while dir_path: @@ -544,14 +694,45 @@ def
>> _get_mount_point(path): return None
>>
>>
>> +def is_locally_mounted_remote_share(path): + return
>> string.find(path, '/tmp/') == 0 + + +def
>> extract_ip_address_from_locally_mounted_remote_share_path(path): +
>> """ + Path is of type :: + + /tmp/127.0.0.1/webdav/a.txt
>> + """ + return path.split('/')[2] + + def get(object_id):
>> """Returns the metadata for an object """ - if
>> os.path.exists(object_id): - stat = os.stat(object_id) +
>> if (os.path.exists(object_id) or
>> (is_locally_mounted_remote_share(object_id))): + """ +
>> For Documents/Shares/Mounted-Drives/Locally-Mounted-Remote-Shares,
>> + where ".Sugar-Metadata" folder exists. + + The only
>> thing is that, for locally-mounted-remote-shares, the +
>> "file" is not physically present. + """ + if
>> os.path.exists(object_id): + # if the file is physically
>> present, derive file-metadata + # by physical
>> examination of the file. + stat = os.stat(object_id) +
>> else: + # if the file is remote, derive file-metadata by
>> fetching + # properties remotely (webdav properties). +
>> stat = None + metadata = _get_file_metadata(object_id, stat)
>> metadata['mountpoint'] = _get_mount_point(object_id) else: +
>> """ + For journal, where ".Sugar-Metadata" folder does not
>> exists. + """ metadata =
>> _get_datastore().get_properties(object_id, byte_arrays=True)
>> metadata['mountpoint'] = '/' return metadata @@ -561,9 +742,28 @@
>> def get_file(object_id): """Returns the file for an object """ if
>> os.path.exists(object_id): + """ + For
>> Documents/Shares/Mounted-Drives/ +
>> Locally-Mounted-Remote-Shares-in-case-when-it-is-present-already. +
>> """ logging.debug('get_file asked for file with path %r',
>> object_id) return object_id + elif
>> is_locally_mounted_remote_share(object_id): + """ +
>> For Locally-Mounted-Remote-Shares-in-case-when-it-is-not-present. +
>> """ + filename = os.path.basename(object_id) +
>> ip_address =
>> extract_ip_address_from_locally_mounted_remote_share_path(object_id)
>>
>>
> +
>> + resource =
>> get_resource_by_ip_address_and_resource_key(ip_address, '/webdav/'
>> + filename) + download_file_path = '/tmp/' + ip_address +
>> '/' + filename + resource.downloadFile(download_file_path)
>> + + return download_file_path else: + """ +
>> For Journal. + """ logging.debug('get_file asked for entry
>> with id %r', object_id) file_path =
>> _get_datastore().get_filename(object_id) if file_path: @@ -754,6
>> +954,20 @@ def _write_entry_on_external_device(metadata,
>> file_path): _rename_entry_on_external_device(file_path,
>> destination_path, metadata_dir_path)
>>
>> + # For "Shares" folder, we need to set the permissions of the
>> newly + # copied file to 0777, else it will not be accessible by
>> "httpd" + # service. + if metadata['mountpoint'] ==
>> '/var/www/web1/web': + fd = os.open(destination_path,
>> os.O_RDONLY) + os.fchmod(fd, stat.S_IRWXU | stat.S_IRWXG |
>> stat.S_IRWXO) + os.close(fd) + + metadata_file_path =
>> os.path.join(metadata_dir_path, file_name + '.metadata') +
>> fd = os.open(metadata_file_path, os.O_RDONLY) +
>> os.fchmod(fd, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) +
>> os.close(fd) + + object_id = destination_path created.send(None,
>> object_id=object_id)
>>
>> diff --git a/src/jarabe/journal/palettes.py
>> b/src/jarabe/journal/palettes.py index 740b65a..77b0fea 100644 ---
>> a/src/jarabe/journal/palettes.py +++
>> b/src/jarabe/journal/palettes.py @@ -77,6 +77,9 @@ class
>> ObjectPalette(Palette): Palette.__init__(self, primary_text=title,
>> icon=activity_icon)
>>
>> + from jarabe.journal.journalactivity import
>> get_mount_point + current_mount_point = get_mount_point() +
>> if misc.get_activities(metadata) or misc.is_bundle(metadata): if
>> metadata.get('activity_id', ''): resume_label = _('Resume') @@
>> -86,10 +89,15 @@ class ObjectPalette(Palette): resume_with_label =
>> _('Start with') menu_item = MenuItem(resume_label,
>> 'activity-start') menu_item.connect('activate',
>> self.__start_activate_cb) + if
>> model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
>>
>>
> + menu_item.set_sensitive(False)
>> self.menu.append(menu_item) menu_item.show()
>>
>> menu_item = MenuItem(resume_with_label, 'activity-start') +
>> if
>> model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
>>
>>
> + menu_item.set_sensitive(False)
>> + self.menu.append(menu_item) menu_item.show() start_with_menu =
>> StartWithMenu(self._metadata) @@ -101,6 +109,7 @@ class
>> ObjectPalette(Palette): self.menu.append(menu_item)
>> menu_item.show()
>>
>> + menu_item = MenuItem(_('Copy to')) icon =
>> Icon(icon_name='edit-copy', xo_color=color,
>> icon_size=gtk.ICON_SIZE_MENU) @@ -120,16 +129,21 @@ class
>> ObjectPalette(Palette): copy_menu.connect('volume-error',
>> self.__volume_error_cb) menu_item.set_submenu(copy_menu)
>>
>> + if self._metadata['mountpoint'] == '/': menu_item =
>> MenuItem(_('Duplicate')) icon = Icon(icon_name='edit-duplicate',
>> xo_color=color, icon_size=gtk.ICON_SIZE_MENU)
>> menu_item.set_image(icon) menu_item.connect('activate',
>> self.__duplicate_activate_cb) + if
>> model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
>>
>>
> + menu_item.set_sensitive(False)
>> self.menu.append(menu_item) menu_item.show()
>>
>> menu_item = MenuItem(_('Send to'), 'document-send') + if
>> model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
>>
>>
> + menu_item.set_sensitive(False)
>> self.menu.append(menu_item) menu_item.show()
>>
>> @@ -140,14 +154,19 @@ class ObjectPalette(Palette): if detail ==
>> True: menu_item = MenuItem(_('View Details'), 'go-right')
>> menu_item.connect('activate', self.__detail_activate_cb) +
>> if
>> model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
>>
>>
> + menu_item.set_sensitive(False)
>> self.menu.append(menu_item) menu_item.show()
>>
>> menu_item = MenuItem(_('Erase'), 'list-remove')
>> menu_item.connect('activate', self.__erase_activate_cb) + if
>> model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
>>
>>
> + menu_item.set_sensitive(False)
>> self.menu.append(menu_item) menu_item.show()
>>
>> + def __start_activate_cb(self, menu_item):
>> misc.resume(self._metadata)
>>
>> @@ -708,6 +727,25 @@ class DocumentsMenu(BaseCopyMenuItem):
>> self._post_operate_per_metadata_per_action(metadata)
>>
>>
>> +class SharesMenu(BaseCopyMenuItem): + def __init__(self,
>> metadata_list, show_editing_alert, +
>> show_progress_info_alert, batch_mode): +
>> BaseCopyMenuItem.__init__(self, metadata_list, _('Shares'), +
>> show_editing_alert, +
>> show_progress_info_alert, +
>> batch_mode) + + def _operate(self, metadata): + if not
>> self._file_path_valid(metadata): + return False +
>> if not self._metadata_copy_valid(metadata, +
>> '/var/www/web1/web'): + return False + + # This
>> is sync-operation. Call the post-operation now. +
>> self._post_operate_per_metadata_per_action(metadata) + + class
>> FriendsMenu(gtk.Menu): __gtype_name__ = 'JournalFriendsMenu'
>>
>> @@ -835,6 +873,17 @@ class CopyMenuHelper(gtk.Menu):
>> menu.append(documents_menu) documents_menu.show()
>>
>> + if get_mount_point() != '/var/www/web1/web': +
>> documents_menu = SharesMenu(metadata_list, +
>> show_editing_alert, +
>> show_progress_info_alert, +
>> batch_mode) +
>> documents_menu.set_image(Icon(icon_name='user-documents', +
>> icon_size=gtk.ICON_SIZE_MENU)) +
>> documents_menu.connect('volume-error', self.__volume_error_cb) +
>> menu.append(documents_menu) + documents_menu.show() + if
>> get_mount_point() != '/': client = gconf.client_get_default() color
>> = XoColor(client.get_string('/desktop/sugar/user/color')) diff
>> --git a/src/jarabe/journal/volumestoolbar.py
>> b/src/jarabe/journal/volumestoolbar.py index 94914e6..08930f8
>> 100644 --- a/src/jarabe/journal/volumestoolbar.py +++
>> b/src/jarabe/journal/volumestoolbar.py @@ -37,8 +37,10 @@ from
>> sugar.graphics.palette import Palette from sugar.graphics.xocolor
>> import XoColor from sugar import env
>>
>> +from jarabe.frame.notification import NotificationIcon from
>> jarabe.journal import model -from jarabe.view.palettes import
>> JournalVolumePalette, JournalXSPalette +from jarabe.view.palettes
>> import JournalVolumePalette, JournalXSPalette, RemoteSharePalette
>> +import jarabe.frame
>>
>>
>> _JOURNAL_0_METADATA_DIR = '.olpc.store' @@ -209,6 +211,7 @@ class
>> VolumesToolbar(gtk.Toolbar):
>>
>> def _set_up_volumes(self): self._set_up_documents_button() +
>> self._set_up_shares_button()
>>
>> volume_monitor = gio.volume_monitor_get() self._mount_added_hid =
>> volume_monitor.connect('mount-added', @@ -234,6 +237,42 @@ class
>> VolumesToolbar(gtk.Toolbar): self._volume_buttons.append(button)
>> self.show()
>>
>> + def _set_up_shares_button(self): + documents_path =
>> '/var/www/web1/web' + if documents_path is not None: +
>> button = DocumentsButton(documents_path) +
>> button.props.group = self._volume_buttons[0] + label =
>> glib.markup_escape_text(_('Shares')) +
>> button.set_palette(Palette(label)) +
>> button.connect('toggled', self._button_toggled_cb) +
>> button.show() + + position =
>> self.get_item_index(self._volume_buttons[-1]) + 1 +
>> self.insert(button, position) +
>> self._volume_buttons.append(button) + self.show() + +
>> def _add_remote_share_button(self, buddy): + button =
>> RemoteSharesButton(buddy) + button.props.group =
>> self._volume_buttons[0] + label =
>> glib.markup_escape_text(_('%s\'s share') % \ +
>> buddy.props.nick) +
>> button.set_palette(RemoteSharePalette(buddy, button)) +
>> button.connect('toggled', self._button_toggled_cb) +
>> button.show() + + position =
>> self.get_item_index(self._volume_buttons[-1]) + 1 +
>> self.insert(button, position) +
>> self._volume_buttons.append(button) + self.show() + +
>> frame = jarabe.frame.get_view() + notif_icon =
>> NotificationIcon() + notif_icon.props.icon_name =
>> 'computer-xo' + notif_icon.props.xo_color =
>> buddy.props.color + frame.add_notification(notif_icon, +
>> gtk.CORNER_BOTTOM_RIGHT) + def __mount_added_cb(self,
>> volume_monitor, mount): self._add_button(mount)
>>
>> @@ -271,8 +310,8 @@ class VolumesToolbar(gtk.Toolbar): def
>> __volume_error_cb(self, button, strerror, severity):
>> self.emit('volume-error', strerror, severity)
>>
>> - def _button_toggled_cb(self, button): - if
>> button.props.active: + def _button_toggled_cb(self, button,
>> force_toggle=False): + if button.props.active or
>> force_toggle: self.emit('volume-changed', button.mount_point)
>>
>> def _unmount_activated_cb(self, menu_item, mount): @@ -300,6
>> +339,19 @@ class VolumesToolbar(gtk.Toolbar): if
>> len(self.get_children()) < 2: self.hide()
>>
>> + def _remove_remote_share_button(self, mount_point): + #
>> Here, IP_Address is the mount_point. + for button in
>> self.get_children(): + if type(button) ==
>> RemoteSharesButton and \ + button.mount_point ==
>> mount_point: +
>> self._volume_buttons.remove(button) +
>> self.remove(button) +
>> self.get_children()[0].props.active = True + +
>> if len(sel.get_children()) < 2: +
>> self.hide() + break; + def
>> set_active_volume(self, mount): button =
>> self._get_button_for_mount(mount) button.props.active = True @@
>> -313,6 +365,12 @@ class VolumesToolbar(gtk.Toolbar): if
>> button.mount_point != mount_point: button.set_sensitive(sensitive)
>>
>> + def get_journal_button(self): + return
>> self._volume_buttons[0] + + def get_button_toggled_cb(self): +
>> return self._button_toggled_cb +
>>
>> class BaseButton(RadioToolButton): __gsignals__ = { @@ -439,6
>> +497,22 @@ class DocumentsButton(BaseButton): self.props.xo_color =
>> color
>>
>>
>> +class RemoteSharesButton(BaseButton): + + def __init__(self,
>> buddy): + BaseButton.__init__(self,
>> mount_point=buddy.props.ip_address) + + self._buddy = buddy
>> + self.props.named_icon = 'computer-xo' +
>> self.props.xo_color = buddy.props.color +
>> self._buddy_ip_address = buddy.props.ip_address + + def
>> create_palette(self): + palette =
>> RemoteSharePalette(self._buddy) + return palette + + + class
>> XSButton(ToolButton): def __init__(self):
>> ToolButton.__init__(self) diff --git a/src/jarabe/model/buddy.py
>> b/src/jarabe/model/buddy.py index 8f17d7e..c088aa9 100644 ---
>> a/src/jarabe/model/buddy.py +++ b/src/jarabe/model/buddy.py @@
>> -43,6 +43,7 @@ class BaseBuddyModel(gobject.GObject): self._color =
>> None self._tags = None self._current_activity = None +
>> self._ip_address = None
>>
>> gobject.GObject.__init__(self, **kwargs)
>>
>> @@ -87,6 +88,16 @@ class BaseBuddyModel(gobject.GObject):
>> getter=get_current_activity, setter=set_current_activity)
>>
>> + def get_ip_address(self): + return self._ip_address + +
>> def set_ip_address(self, ip_address): + self._ip_address =
>> ip_address + + ip_address = gobject.property(type=object, +
>> getter=get_ip_address, +
>> setter=set_ip_address) + def is_owner(self): raise
>> NotImplementedError
>>
>> diff --git a/src/jarabe/model/neighborhood.py
>> b/src/jarabe/model/neighborhood.py index 4528a15..39f648e 100644
>> --- a/src/jarabe/model/neighborhood.py +++
>> b/src/jarabe/model/neighborhood.py @@ -930,6 +930,9 @@ class
>> Neighborhood(gobject.GObject): if 'key' in properties:
>> buddy.props.key = properties['key']
>>
>> + if 'ip4-address' in properties: +
>> buddy.props.ip_address = properties['ip4-address'] + nick_key =
>> CONNECTION_INTERFACE_ALIASING + '/alias' if nick_key in
>> properties: buddy.props.nick = properties[nick_key] diff --git
>> a/src/jarabe/view/buddymenu.py b/src/jarabe/view/buddymenu.py index
>> de5a772..dfbcfa3 100644 --- a/src/jarabe/view/buddymenu.py +++
>> b/src/jarabe/view/buddymenu.py @@ -73,6 +73,12 @@ class
>> BuddyMenu(Palette): self.menu.append(menu_item) menu_item.show()
>>
>> + access_buddy_remote_share_menu_item = MenuItem(_('Access
>> Share'), 'list-add') +
>> access_buddy_remote_share_menu_item.connect('activate', +
>> self._access_share_cb) +
>> self.menu.append(access_buddy_remote_share_menu_item) +
>> access_buddy_remote_share_menu_item.show() + self._invite_menu =
>> MenuItem('') self._invite_menu.connect('activate',
>> self._invite_friend_cb) self.menu.append(self._invite_menu) @@
>> -83,6 +89,11 @@ class BuddyMenu(Palette): activity =
>> home_model.get_active_activity()
>> self._update_invite_menu(activity)
>>
>> + def _access_share_cb(self, menuitem): + from
>> jarabe.journal.journalactivity import get_journal +
>> volumes_toolbar = get_journal().get_volumes_toolbar() +
>> volumes_toolbar._add_remote_share_button(self._buddy) + def
>> _add_my_items(self): item = MenuItem(_('Shutdown'),
>> 'system-shutdown') item.connect('activate',
>> self.__shutdown_activate_cb) diff --git
>> a/src/jarabe/view/palettes.py b/src/jarabe/view/palettes.py index
>> 7a17f32..3b26faf 100644 --- a/src/jarabe/view/palettes.py +++
>> b/src/jarabe/view/palettes.py @@ -336,3 +336,36 @@ class
>> JournalXSPalette(Palette): def __journal_restore_activate_cb(self,
>> menu_item, xs_hostname): dialog = XSRestoreDialog(xs_hostname)
>> dialog.show() + + +class RemoteSharePalette(Palette): + def
>> __init__(self, buddy, button): + Palette.__init__(self,
>> label=('%s\'s share' % buddy.props.nick)) + self._buddy =
>> buddy + self._button = button + +
>> self.props.secondary_text =
>> glib.markup_escape_text(buddy.props.ip_address) + + vbox =
>> gtk.VBox() + self.set_content(vbox) + vbox.show() + +
>> self.connect('popup', self.__popup_cb) + + menu_item =
>> MenuItem(pgettext('Share', 'Unmount')) + + icon =
>> Icon(icon_name='media-eject', icon_size=gtk.ICON_SIZE_MENU) +
>> menu_item.set_image(icon) + icon.show() + +
>> menu_item.connect('activate', self.__unmount_activate_cb) +
>> self.menu.append(menu_item) + menu_item.show() + + def
>> __unmount_activate_cb(self, menu_item): + from
>> jarabe.journal.journalactivity import get_journal +
>> singleton_volumes_toolbar = get_journal().get_volumes_toolbar() +
>> singleton_volumes_toolbar._remove_remote_share_button(self._buddy.props.ip_address)
>>
>>
> +
>> + def __popup_cb(self, palette): + pass diff --git
>> a/src/webdav/Condition.py b/src/webdav/Condition.py new file mode
>> 100644 index 0000000..76acf94 --- /dev/null +++
>> b/src/webdav/Condition.py @@ -0,0 +1,475 @@ +# pylint:
>> disable-msg=R0921,W0704,R0901,W0511,R0201 +# Copyright 2008 German
>> Aerospace Center (DLR) +# +# Licensed under the Apache License,
>> Version 2.0 (the "License"); +# you may not use this file except in
>> compliance with the License. +# You may obtain a copy of the
>> License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +#
>> +# Unless required by applicable law or agreed to in writing,
>> software +# distributed under the License is distributed on an "AS
>> IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
>> express or implied. +# See the License for the specific language
>> governing permissions and +# limitations under the License. + +
>> +""" +This module contains classes for creating a search condition
>> according to the DASL draft. +The classes will output the WHERE
>> part of a search request to a WebDAV server. + +Instances of the
>> classes defined in this module form a tree data structure which
>> represents +a search condition. This tree is made up of AND-nodes,
>> OR-nodes, Operator- and comparison- +nodes and from property (i.e.
>> variable) and constant leaf nodes. +""" + + +import types +from
>> time import strftime +from calendar import timegm +from rfc822
>> import formatdate + +from webdav.Constants import NS_DAV,
>> PROP_LAST_MODIFIED, DATE_FORMAT_ISO8601 + + +__version__ =
>> "$Revision$"[11:-2] + + +class ConditionTerm(object): + """ +
>> This is the abstact base class for all condition terms. + """ +
>> def __init__(self): + pass + + def toXML(self): +
>> """ + Abstact method which return a XML string which can be
>> passed to a WebDAV server + for a search condition. +
>> """ + raise NotImplementedError + + # start Tamino
>> workaround for missing like-op: + def postFilter(self,
>> resultSet): + """ + Abstact method for temporary
>> workaround for Tamino's absense of the like-operator. + This
>> method shall filter the given result set for those resources which
>> match + all Contains-trems. + """ + return
>> resultSet + # end of workaround + + +class
>> IsCollectionTerm(ConditionTerm): + """ Leaf condition. Checks if
>> the matching resources are collections. """ + + def
>> __init__(self): + """ Constructor. """ + +
>> ConditionTerm.__init__(self) + + def toXML(self): + """ +
>> Returns XML encoding. + """ + + return
>> "<D:is-collection/>" + + +class Literal(ConditionTerm): + """ +
>> A leaf class for condition expressions representing a constant
>> value. + """ + def __init__(self, literal): +
>> ConditionTerm.__init__(self) + self.literal = literal +
>> + def toXML(self): + ''' + Returns XML encoding.
>> + ''' + return "<D:literal>" + self.literal +
>> "</D:literal>" + + +class UnaryTerm(ConditionTerm): + """ +
>> Base class of all nodes with a single child node. + """ + def
>> __init__(self, child): + ConditionTerm.__init__(self) +
>> self.child = child + + def toXML(self): + ''' +
>> Returns XML encoding. + ''' + return
>> self.child.toXML() + + +class BinaryTerm(ConditionTerm): + """ +
>> Base class of all nodes with two child nodes + """ + def
>> __init__(self, left, right): + ConditionTerm.__init__(self)
>> + self.left = left + self.right = right + + def
>> toXML(self): + ''' + Returns XML encoding. +
>> ''' + return self.left.toXML() + self.right.toXML() + +class
>> TupleTerm(ConditionTerm): + """ + Base class of all nodes
>> with multiple single child nodes. + """ + def __init__(self,
>> terms): + ConditionTerm.__init__(self) + self.terms =
>> terms + + def addTerm(self, term): + ''' + Removes
>> a term. + + @param term: term to add + ''' +
>> self.terms.append(term) + + def removeTerm(self, term): +
>> ''' + Adds a term. + + @param term: term to remove +
>> ''' + try: + self.terms.remove(term) +
>> except ValueError: + pass + + def toXML(self): +
>> ''' + Returns XML encoding. + ''' + result =
>> "" + for term in self.terms: + result +=
>> term.toXML() + return result + + +class AndTerm(TupleTerm):
>> + """ + This class represents and logical AND-condition with
>> multiple sub terms. + """ + def toXML(self): + ''' +
>> Returns XML encoding. + ''' + return "<D:and>" +
>> TupleTerm.toXML(self) + "</D:and>" + + # start Tamino workaround
>> for missing like-op: + def postFilter(self, resultSet): +
>> ''' + Filters the given result set. This is a TAMINO WebDav
>> server workaround + for the missing 'like' tag. + +
>> @param resultSet: the result set that needs to be filtered. +
>> ''' + for term in self.terms: + filtered =
>> term.postFilter(resultSet) + resultSet = filtered +
>> return resultSet + # end of workaround + +class
>> OrTerm(TupleTerm): + """ + This class represents and logical
>> OR-condition with multiple sub terms. + """ + def
>> toXML(self): + ''' + Returns XML encoding. +
>> ''' + return "<D:or>" + TupleTerm.toXML(self) + "</D:or>" +
>> + # start Tamino workaround for missing like-op: + def
>> postFilter(self, resultSet): + ''' + Filters the
>> given result set. This is a TAMINO WebDav server workaround +
>> for the missing 'like' tag. + + @param resultSet: the result
>> set that needs to be filtered. + ''' + raise
>> NotImplementedError + + +class NotTerm(UnaryTerm): + """ +
>> This class represents a negation term for the contained sub term. +
>> """ + def toXML(self): + ''' + Returns XML
>> encoding. + ''' + # start Tamino workaround for
>> missing like-op: + if isinstance(self.child,
>> ContainsTerm): + return "" + # end of workaround
>> + return "<D:not>" + UnaryTerm.toXML(self) + "</D:not>" + +
>> # start Tamino workaround for missing like-op: + def
>> postFilter(self, resultSet): + ''' + Filters the
>> given result set. This is a TAMINO WebDav server workaround +
>> for the missing 'like' tag. + + @param resultSet: the result
>> set that needs to be filtered. + ''' + if
>> isinstance(self.child, ContainsTerm): +
>> self.child.negate = 1 + # TODO: pass on filter +
>> return self.child.postFilter(resultSet) + + +class
>> ExistsTerm(UnaryTerm): + """ + Nodes of this class must have
>> a single child with tuple type (of len 2) representing a +
>> WebDAV property. + This leaf term evaluates to true if the
>> (child) property exists. + """ + def toXML(self): +
>> ''' + Returns XML encoding. + ''' + return
>> '<D:is-defined><D:prop xmlns="%s"><%s' % self.child + '
>> /></D:prop></D:is-defined>' + +class
>> ContentContainsTerm(UnaryTerm): + """ + This class can be
>> used to search for a given phrase in resources' contents. + """
>> + def toXML(self): + ''' + Returns XML encoding. +
>> ''' + return "<D:contains>" + self.child + "</D:contains>"
>> + + + +class BinaryRelationTerm(BinaryTerm): + """ + This is
>> the abstact base class for the following relation operands. +
>> """ + def __init__(self, left, right): +
>> BinaryTerm.__init__(self, left, right) + if
>> isinstance(self.left, types.StringType): # Must be namespace +
>> name pair + self.left = ('DAV:', self.left) + if
>> not isinstance(self.right, Literal): + self.right =
>> Literal(self.right) # Must be Literal instance + +
>> def toXML(self): + ''' + Returns XML encoding. +
>> ''' + ## TODO: extract name space and create shortcut for
>> left element + return '<D:prop xmlns="%s"><%s /></D:prop>' %
>> self.left + self.right.toXML() + + +class
>> StringRelationTerm(BinaryRelationTerm): + """ + This is the
>> abstact base class for the following string relation classes. +
>> """ + def __init__(self, left, right, caseless=None): +
>> """ + @param left: webdav property (namespace, name) +
>> @param right: string/unicode literal + qparam caseless: 1
>> for case sensitive comparison + """ +
>> BinaryRelationTerm.__init__(self, left, Literal(right)) +
>> self.caseless = caseless + if self.caseless: +
>> self.attrCaseless = "yes" + else: +
>> self.attrCaseless = "no" + +class
>> NumberRelationTerm(BinaryRelationTerm): + """ + This is the
>> abstact base class for the following number comparison classes. +
>> """ + def __init__(self, left, right): + """ +
>> @param left: webdav property (namespace, name) + @param
>> right: constant number + """ + ## TODO: implemet
>> typed literal + BinaryRelationTerm.__init__(self, left,
>> Literal(str(right))) + +class
>> DateRelationTerm(BinaryRelationTerm): + """ + This is the
>> abstact base class for the following date comparison classes. +
>> """ + def __init__(self, left, right): + """ +
>> @param left: webdav property (namespace, name) + @param
>> right: string literal containing a date in ISO8601 format +
>> """ + ## TODO: implemet typed literal + assert
>> len(right) == 9, "No time is specified for literal: " + str(right)
>> + BinaryRelationTerm.__init__(self, left, right) + if
>> self.left == (NS_DAV, PROP_LAST_MODIFIED): + rfc822Time
>> = formatdate(timegm(right)) # must not use locale setting
>> + self.right = Literal(rfc822Time) + else:
>> + self.right = Literal(strftime(DATE_FORMAT_ISO8601,
>> right)) + + +class MatchesTerm(StringRelationTerm): + """ +
>> Nodes of this class evaluate to true if the (child) property's
>> value matches the (child) string. + """ + def toXML(self): +
>> ''' + Returns XML encoding. + ''' + return
>> '<D:eq caseless="%s">' % self.attrCaseless +
>> StringRelationTerm.toXML(self) + "</D:eq>" + +class
>> ContainsTerm(StringRelationTerm): + """ + Nodes of this class
>> evaluate to true if the (left child) property's value contains the
>> + (right child) string. + """ + def __init__(self, left,
>> right, isTaminoWorkaround=False): + right = unicode(right) +
>> StringRelationTerm.__init__(self, left, "%" + right + "%") +
>> # Tamino workaround: operator like is not yet implemented: +
>> self.negate = 0 + self.isTaminoWorkaround =
>> isTaminoWorkaround + + def toXML(self): + ''' +
>> Returns XML encoding. + ''' + # Tamino workaround:
>> operator like is not yet implemented: + # Produce a
>> is-defined-condition instead + if self.isTaminoWorkaround: +
>> return "<D:isdefined><D:prop xmlns='%s'><%s" % self.left + "
>> /></D:prop></D:isdefined>" + else: + return
>> '<D:like caseless="%s">' % self.attrCaseless +
>> StringRelationTerm.toXML(self) + "</D:like>" + + # start Tamino
>> workaround for missing like-op: + def postFilter(self,
>> resultSet): + ''' + Filters the given result set.
>> This is a TAMINO WebDav server workaround + for the missing
>> 'like' tag. + + @param resultSet: the result set that needs
>> to be filtered. + ''' + newResult = {} + word
>> = self.right.literal[1:-1] # remove leading and trailing '%'
>> characters (see __init__()) + for url, properties in
>> resultSet.items(): + value = properties.get(self.left) +
>> if self.negate: + if not value or
>> value.textof().find(word) < 0: + newResult[url]
>> = properties + else: + if value and
>> value.textof().find(word) >= 0: + newResult[url]
>> = properties + return newResult + # end of workaround +
>> +class IsEqualTerm(NumberRelationTerm): + """ + Nodes of this
>> class evaluate to true if the (left child) numerical property's
>> value is equal + to the (right child) number. + """ + def
>> toXML(self): + ''' + Returns XML encoding. +
>> ''' + return "<D:eq>" + NumberRelationTerm.toXML(self) +
>> "</D:eq>" + +class IsGreaterTerm(NumberRelationTerm): + """ +
>> Nodes of this class evaluate to true if the (left child) numerical
>> property's value is greater + than the (right child) number. +
>> """ + def toXML(self): + ''' + Returns XML
>> encoding. + ''' + return "<D:gt>" +
>> NumberRelationTerm.toXML(self) + "</D:gt>" + +class
>> IsGreaterOrEqualTerm(NumberRelationTerm): + """ + Nodes of
>> this class evaluate to true if the (left child) numerical
>> property's value is greater + than or equal to the (right child)
>> number. + """ + def toXML(self): + ''' +
>> Returns XML encoding. + ''' + return "<D:gte>" +
>> NumberRelationTerm.toXML(self) + "</D:gte>" + +class
>> IsSmallerTerm(NumberRelationTerm): + """ + Nodes of this
>> class evaluate to true if the (left child) numerical property's
>> value is less + than the (right child) number. + """ + def
>> toXML(self): + ''' + Returns XML encoding. +
>> ''' + return "<D:lt>" + NumberRelationTerm.toXML(self) +
>> "</D:lt>" + +class IsSmallerOrEqualTerm(NumberRelationTerm): +
>> """ + Nodes of this class evaluate to true if the (left child)
>> numerical property's value is less + than or equal to the (right
>> child) number. + """ + def toXML(self): + ''' +
>> Returns XML encoding. + ''' + return "<D:lte>" +
>> NumberRelationTerm.toXML(self) + "</D:lte>" + + +class
>> OnTerm(DateRelationTerm): + """ + Nodes of this class
>> evaluate to true if the (left child) property's value is a date +
>> equal to the (right child) date. + """ + def toXML(self): +
>> ''' + Returns XML encoding. + ''' + return
>> "<D:eq>" + DateRelationTerm.toXML(self) + "</D:eq>" + +class
>> AfterTerm(DateRelationTerm): + """ + Nodes of this class
>> evaluate to true if the (left child) property's value is a date +
>> succeeding the (right child) date. + """ + def toXML(self): +
>> ''' + Returns XML encoding. + ''' + return
>> "<D:gt>" + DateRelationTerm.toXML(self) + "</D:gt>" + +class
>> BeforeTerm(DateRelationTerm): + """ + Nodes of this class
>> evaluate to true if the (left child) property's value is a date +
>> preceeding the (right child) date. + """ + def toXML(self): +
>> ''' + Returns XML encoding. + ''' + return
>> "<D:lt>" + DateRelationTerm.toXML(self) + "</D:lt>" + + + +# Simple
>> module test +if __name__ == '__main__': + # use the example
>> from the webdav specification + condition = AndTerm(
>> (MatchesTerm('getcontenttype', 'image/gif'), \ +
>> IsGreaterTerm('getcontentlength', 4096)) ) + print "Where: " +
>> condition.toXML() diff --git a/src/webdav/Connection.py
>> b/src/webdav/Connection.py new file mode 100644 index
>> 0000000..66f7833 --- /dev/null +++ b/src/webdav/Connection.py @@
>> -0,0 +1,321 @@ +# pylint:
>> disable-msg=W0142,W0102,R0901,R0904,E0203,E1101,C0103 +# +#
>> Copyright 2008 German Aerospace Center (DLR) +# +# Licensed under
>> the Apache License, Version 2.0 (the "License"); +# you may not use
>> this file except in compliance with the License. +# You may obtain
>> a copy of the License at +# +#
>> http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by
>> applicable law or agreed to in writing, software +# distributed
>> under the License is distributed on an "AS IS" BASIS, +# WITHOUT
>> WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#
>> See the License for the specific language governing permissions
>> and +# limitations under the License. + + +""" +The contained class
>> extends the HTTPConnection class for WebDAV support. +""" + + +from
>> httplib import HTTPConnection, CannotSendRequest, BadStatusLine,
>> ResponseNotReady +from copy import copy +import base64 # for
>> basic authentication +try: + import hashlib +except ImportError:
>> # for Python 2.4 compatibility + import md5 + hashlib = md5
>> +import mimetypes +import os # file handling +import urllib
>> +import types +import socket # to "catch" socket.error +from
>> threading import RLock +try: + from uuid import uuid4 +except
>> ImportError: # for Python 2.4 compatibility + from uuid_ import
>> uuid4 +from xml.parsers.expat import ExpatError + +from davlib
>> import DAV +from qp_xml import Parser + +from webdav.WebdavResponse
>> import MultiStatusResponse, ResponseFormatError +from webdav import
>> Constants +from webdav.logger import getDefaultLogger + +
>> +__version__ = "$LastChangedRevision$" + + +class Connection(DAV):
>> + """ + This class handles a connection to a WebDAV server. +
>> This class is used internally. Client code should prefer classes +
>> L{WebdavClient.ResourceStorer} and
>> L{WebdavClient.CollectionStorer}. + + @author: Roland Betz +
>> """ + + # Constants + # The following switch activates a
>> workaround for the Tamino webdav server: + # Tamino expects
>> URLs which are passed in a HTTP header to be Latin-1 encoded + #
>> instead of Utf-8 encoded. + # Set this switch to zero in order
>> to communicate with conformant servers. + blockSize = 30000 +
>> MaxRetries = 10 + + def __init__(self, *args, **kwArgs): +
>> DAV.__init__(self, *args, **kwArgs) +
>> self.__authorizationInfo = None + self.logger =
>> getDefaultLogger() + self.isConnectedToCatacomb = True +
>> self.serverTypeChecked = False + self._lock = RLock() +
>> + def _request(self, method, url, body=None, extra_hdrs={}): +
>> + self._lock.acquire() + try: + # add the
>> authorization header + extraHeaders = copy(extra_hdrs) +
>> if self.__authorizationInfo: + + # update (digest)
>> authorization data + if
>> hasattr(self.__authorizationInfo, "update"): +
>> self.__authorizationInfo.update(method=method, uri=url) +
>> + extraHeaders["AUTHORIZATION"] =
>> self.__authorizationInfo.authorization + + # encode
>> message parts + body = _toUtf8(body) + url =
>> _urlEncode(url) + for key, value in
>> extraHeaders.items(): + extraHeaders[key] =
>> _toUtf8(value) + if key == "Destination": #
>> copy/move header + if
>> self.isConnectedToCatacomb: +
>> extraHeaders[key] = _toUtf8(value.replace(Constants.SHARP,
>> Constants.QUOTED_SHARP)) + + else: # in case of
>> TAMINO 4.4 + extraHeaders[key] =
>> _urlEncode(value) + # pass message to httplib class +
>> for retry in range(0, Connection.MaxRetries): # retry loop +
>> try: + self.logger.debug("REQUEST Send %s for
>> %s" % (method, url)) +
>> self.logger.debug("REQUEST Body: " + repr(body)) +
>> for hdr in extraHeaders.items(): +
>> self.logger.debug("REQUEST Header: " + repr(hdr)) +
>> self.request(method, url, body, extraHeaders) +
>> response = self.getresponse() + break # no
>> retry needed + except (CannotSendRequest,
>> socket.error, BadStatusLine, ResponseNotReady), exc: +
>> # Workaround, start: reconnect and retry... +
>> self.logger.debug("Exception: " + str(exc) + " Retry ... ") +
>> self.close() + try: +
>> self.connect() + except (CannotSendRequest,
>> socket.error, BadStatusLine, ResponseNotReady), exc: +
>> raise WebdavError("Cannot perform request. Connection failed.") +
>> if retry == Connection.MaxRetries - 1: +
>> raise WebdavError("Cannot perform request.") + return
>> self.__evaluateResponse(method, response) + finally: +
>> self._lock.release() + + def __evaluateResponse(self, method,
>> response): + """ Evaluates the response of the WebDAV
>> server. """ + + status, reason = response.status,
>> response.reason + self.logger.debug("Method: " + method + "
>> Status %d: " % status + reason) + + if status >=
>> Constants.CODE_LOWEST_ERROR: # error has occured ? +
>> self.logger.debug("ERROR Response: " + response.read()) +
>> + # identify authentication CODE_UNAUTHORIZED, throw
>> appropriate exception + if status ==
>> Constants.CODE_UNAUTHORIZED: + raise
>> AuthorizationError(reason, status,
>> response.msg["www-authenticate"]) + + response.close() +
>> raise WebdavError(reason, status) + + if status ==
>> Constants.CODE_MULTISTATUS: + content = response.read()
>> + ## check for UTF-8 encoding + try: +
>> response.root = Parser().parse(content) + except
>> ExpatError, error: + errorMessage = "Invalid XML
>> document has been returned.\nReason: '%s'" % str(error.args) +
>> raise WebdavError(errorMessage) + try: +
>> response.msr = MultiStatusResponse(response.root) +
>> except ResponseFormatError: + raise
>> WebdavError("Invalid WebDAV response.") +
>> response.close() + self.logger.debug("RESPONSE
>> (Multi-Status): " + unicode(response.msr)) + elif method ==
>> 'LOCK' and status == Constants.CODE_SUCCEEDED: +
>> response.parse_lock_response() + response.close() +
>> elif method != 'GET' and method != 'PUT': +
>> self.logger.debug("RESPONSE Body: " + response.read()) +
>> response.close() + return response + + def
>> addBasicAuthorization(self, user, password, realm=None): +
>> if user and len(user) > 0: + self.__authorizationInfo =
>> _BasicAuthenticationInfo(realm=realm, user=user,
>> password=password) + + def addDigestAuthorization(self, user,
>> password, realm, qop, nonce, uri = None, method = None): +
>> if user and len(user) > 0: + # username, realm,
>> password, uri, method, qop are required +
>> self.__authorizationInfo = _DigestAuthenticationInfo(realm=realm,
>> user=user, password=password, uri=uri, method=method, qop=qop,
>> nonce=nonce) + + def putFile(self, path, srcfile, header={}): +
>> self._lock.acquire() + try: + # Assemble header +
>> try: + size = os.path.getsize(srcfile.name) +
>> except os.error, error: + raise WebdavError("Cannot
>> determine file size.\nReason: ''" % str(error.args)) +
>> header["Content-length"] = str(size) + + contentType,
>> contentEnc = mimetypes.guess_type(path) + if
>> contentType: + header['Content-Type'] = contentType
>> + if contentEnc: +
>> header['Content-Encoding'] = contentEnc + if
>> self.__authorizationInfo: + # update (digest)
>> authorization data + if
>> hasattr(self.__authorizationInfo, "update"): +
>> self.__authorizationInfo.update(method="PUT", uri=path) +
>> header["AUTHORIZATION"] = self.__authorizationInfo.authorization +
>> + # send first request + path =
>> _urlEncode(path) + try: +
>> HTTPConnection.request(self, 'PUT', path, "", header) +
>> self._blockCopySocket(srcfile, self, Connection.blockSize) +
>> srcfile.close() + response = self.getresponse() +
>> except (CannotSendRequest, socket.error, BadStatusLine,
>> ResponseNotReady), exc: +
>> self.logger.debug("Exception: " + str(exc) + " Retry ... ") +
>> raise WebdavError("Cannot perform request.") + status,
>> reason = (response.status, response.reason) +
>> self.logger.debug("Status %d: %s" % (status, reason)) +
>> try: + if status >= Constants.CODE_LOWEST_ERROR:
>> # error has occured ? + raise
>> WebdavError(reason, status) + finally: +
>> self.logger.debug("RESPONSE Body: " + response.read()) +
>> response.close() + return response + finally: +
>> self._lock.release() + + def _blockCopySocket(self, source,
>> toSocket, blockSize): + transferedBytes = 0 + block =
>> source.read(blockSize) + #while source.readinto(block,
>> blockSize): + while len(block): +
>> toSocket.send(block) + self.logger.debug("Wrote %d
>> bytes." % len(block)) + transferedBytes += len(block) +
>> block = source.read(blockSize) +
>> self.logger.info("Transfered %d bytes." % transferedBytes) + +
>> def __str__(self): + return self.protocol + "://" +
>> self.host + ':' + str(self.port) + + +class
>> _BasicAuthenticationInfo(object): + def __init__(self,
>> **kwArgs): + self.__dict__.update(kwArgs) +
>> self.cookie = base64.encodestring("%s:%s" % (self.user,
>> self.password) ).strip() + self.authorization = "Basic " +
>> self.cookie + self.password = None # protect password
>> security + +class _DigestAuthenticationInfo(object): + + __nc =
>> "0000000" # in hexadecimal without leading 0x + + def
>> __init__(self, **kwArgs): + + self.__dict__.update(kwArgs) +
>> + if self.qop is None: + raise
>> WebdavError("Digest without qop is not implemented.") + if
>> self.qop == "auth-int": + raise WebdavError("Digest with
>> qop-int is not implemented.") + + def update(self, **kwArgs): +
>> """ Update input data between requests""" + +
>> self.__dict__.update(kwArgs) + + def _makeDigest(self): +
>> """ Creates the digest information. """ + + # increment
>> nonce count + self._incrementNc() + + # username,
>> realm, password, uri, method, qop are required + + a1 =
>> "%s:%s:%s" % (self.user, self.realm, self.password) + ha1 =
>> hashlib.md5(a1).hexdigest() + + #qop == auth + a2 =
>> "%s:%s" % (self.method, self.uri) + ha2 =
>> hashlib.md5(a2).hexdigest() + + cnonce = str(uuid4()) +
>> + responseData = "%s:%s:%s:%s:%s:%s" % (ha1, self.nonce,
>> _DigestAuthenticationInfo.__nc, cnonce, self.qop, ha2) +
>> digestResponse = hashlib.md5(responseData).hexdigest() + +
>> authorization = "Digest username=\"%s\", realm=\"%s\",
>> nonce=\"%s\", uri=\"%s\", algorithm=MD5, response=\"%s\", qop=auth,
>> nc=%s, cnonce=\"%s\"" \ + % (self.user,
>> self.realm, self.nonce, self.uri, digestResponse,
>> _DigestAuthenticationInfo.__nc, cnonce) + return
>> authorization + + authorization = property(_makeDigest) + +
>> def _incrementNc(self): + _DigestAuthenticationInfo.__nc =
>> self._dec2nc(self._nc2dec() + 1) + + def _nc2dec(self): +
>> return int(_DigestAuthenticationInfo.__nc, 16) + + def
>> _dec2nc(self, decimal): + return hex(decimal)[2:].zfill(8) +
>> + +class WebdavError(IOError): + def __init__(self, reason,
>> code=0): + IOError.__init__(self, code) + self.code =
>> code + self.reason = reason + def __str__(self): +
>> return self.reason + + +class AuthorizationError(WebdavError): +
>> def __init__(self, reason, code, authHeader): +
>> WebdavError.__init__(self, reason, code) + + self.authType =
>> authHeader.split(" ")[0] + self.authInfo = authHeader + +
>> +def _toUtf8(body): + if not body is None: + if
>> type(body) == types.UnicodeType: + body =
>> body.encode('utf-8') + return body + + +def _urlEncode(url): +
>> if type(url) == types.UnicodeType: + url =
>> url.encode('utf-8') + return urllib.quote(url) diff --git
>> a/src/webdav/Constants.py b/src/webdav/Constants.py new file mode
>> 100644 index 0000000..56dfd77 --- /dev/null +++
>> b/src/webdav/Constants.py @@ -0,0 +1,199 @@ +# pylint:
>> disable-msg=C0103 +# +# Copyright 2008 German Aerospace Center
>> (DLR) +# +# Licensed under the Apache License, Version 2.0 (the
>> "License"); +# you may not use this file except in compliance with
>> the License. +# You may obtain a copy of the License at +# +#
>> http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by
>> applicable law or agreed to in writing, software +# distributed
>> under the License is distributed on an "AS IS" BASIS, +# WITHOUT
>> WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#
>> See the License for the specific language governing permissions
>> and +# limitations under the License. + + +""" +Contains XML tag
>> names for the WebDAV protocol (RFC 2815) +and further WebDAV
>> related constants. +""" + + +__version__ = "$Revision$"[11:-2] + +
>> +QUOTED_SHARP = "%23" +SHARP = "#" + +# Date formats
>> +DATE_FORMAT_ISO8601 = r"%Y-%m-%dT%H:%M:%SZ" +DATE_FORMAT_HTTP =
>> r"%a, %d %b %Y %H:%M:%S GMT" # not used, substituted by rfc822
>> function + +NS_DAV = 'DAV:' +NS_TAMINO =
>> 'http://namespaces.softwareag.com/tamino/response2' +
>> +TAG_PROPERTY_FIND = 'propfind' +TAG_PROPERTY_NAME =
>> 'propname' +TAG_PROPERTY_UPDATE = 'propertyupdate'
>> +TAG_PROPERTY_SET = 'set' +TAG_PROPERTY_REMOVE = 'remove'
>> +TAG_ALL_PROPERTY = 'allprop' +TAG_PROP = 'prop' +
>> +TAG_MULTISTATUS = 'multistatus' +TAG_RESPONSE =
>> 'response' +TAG_HREF = 'href' +TAG_PROPERTY_STATUS
>> = 'propstat' +TAG_STATUS = 'status'
>> +TAG_RESPONSEDESCRIPTION = 'responsdescription' +
>> +PROP_CREATION_DATE = 'creationdate' +PROP_DISPLAY_NAME =
>> 'displayname' +PROP_CONTENT_LANGUAGE = 'getcontentlanguage'
>> +PROP_CONTENT_LENGTH = 'getcontentlength' +PROP_CONTENT_TYPE
>> = 'getcontenttype' +PROP_ETAG = 'getetag'
>> +PROP_MODIFICATION_DATE = 'modificationdate' # this property is
>> supported by +# Tamino 4.4 but not by Catacomb; the date format is
>> ISO8601 +PROP_LAST_MODIFIED = 'getlastmodified'
>> +PROP_LOCK_DISCOVERY = 'lockdiscovery' +PROP_RESOURCE_TYPE =
>> 'resourcetype' +PROP_SOURCE = 'source'
>> +PROP_SUPPORTED_LOCK = 'supportedlock' +PROP_OWNER =
>> 'owner' + +PROP_RESOURCE_TYPE_RESOURCE = 'resource'
>> +PROP_RESOURCE_TYPE_COLLECTION = 'collection' + +TAG_LINK
>> = 'link' +TAG_LINK_SOURCE = 'src' +TAG_LINK_DESTINATION =
>> 'dst' + +TAG_LOCK_ENTRY = 'lockentry' +TAG_LOCK_SCOPE =
>> 'lockscope' +TAG_LOCK_TYPE = 'locktype' +TAG_LOCK_INFO =
>> 'lockinfo' +TAG_ACTIVE_LOCK = 'activelock' +TAG_LOCK_DEPTH =
>> 'depth' +TAG_LOCK_TOKEN = 'locktoken' +TAG_LOCK_TIMEOUT =
>> 'timeout' +TAG_LOCK_EXCLUSIVE = 'exclusive' +TAG_LOCK_SHARED =
>> 'shared' +TAG_LOCK_OWNER = 'owner' + +# HTTP error code
>> constants +CODE_MULTISTATUS = 207 +CODE_SUCCEEDED = 200
>> +CODE_CREATED = 201 +CODE_NOCONTENT = 204 + +CODE_LOWEST_ERROR =
>> 300 + +CODE_UNAUTHORIZED = 401 +CODE_FORBIDDEN = 403
>> +CODE_NOT_FOUND = 404 +CODE_CONFLICT = 409
>> +CODE_PRECONDITION_FAILED = 412 +CODE_LOCKED = 423 # no
>> permission +CODE_FAILED_DEPENDENCY = 424 + +CODE_OUTOFMEM = 507 +
>> +# ? +CONFIG_UNICODE_URL = 1 + +# constants for WebDAV DASL
>> according to draft + +TAG_SEARCH_REQUEST = 'searchrequest'
>> +TAG_SEARCH_BASIC = 'basicsearch' +TAG_SEARCH_SELECT = 'select'
>> +TAG_SEARCH_FROM = 'from' +TAG_SEARCH_SCOPE = 'scope'
>> +TAG_SEARCH_WHERE = 'where' + +# constants for WebDAV ACP
>> (according to draft-ietf-webdav-acl-09) below ... + +TAG_ACL
>> = 'acl' +TAG_ACE = 'ace' +TAG_GRANT =
>> 'grant' +TAG_DENY = 'deny' +TAG_PRIVILEGE
>> = 'privilege' +TAG_PRINCIPAL = 'principal' +TAG_ALL
>> = 'all' +TAG_AUTHENTICATED = 'authenticated'
>> +TAG_UNAUTHENTICATED = 'unauthenticated' +TAG_OWNER
>> = 'owner' +TAG_PROPERTY = 'property' +TAG_SELF
>> = 'self' +TAG_INHERITED = 'inherited' +TAG_PROTECTED
>> = 'protected' +TAG_SUPPORTED_PRIVILEGE = 'supported-privilege'
>> +TAG_DESCRIPTION = 'description' + +# privileges for WebDAV
>> ACP: +TAG_READ = 'read' +TAG_WRITE = 'write' +TAG_WRITE_PROPERTIES
>> = 'write-properties' +TAG_WRITE_CONTENT = 'write-content'
>> +TAG_UNLOCK = 'unlock' +TAG_READ_ACL = 'read-acl'
>> +TAG_READ_CURRENT_USER_PRIVILEGE_SET =
>> 'read-current-user-privilege-set' +TAG_WRITE_ACL = 'write-acl'
>> +TAG_ALL = 'all' +TAG_BIND = 'bind' +TAG_UNBIND = 'unbind' +#
>> Tamino-specific privileges +TAG_TAMINO_SECURITY = 'security' +#
>> Limestone-specific privileges +TAG_BIND_COLLECTION =
>> 'bind-collection' +TAG_UNBIND_COLLECTION = 'unbind-collection'
>> +TAG_READ_PRIVATE_PROPERTIES = 'read-private-properties'
>> +TAG_WRITE_PRIVATE_PROPERTIES = 'write-private-properties' + +#
>> properties for WebDAV ACP: +PROP_CURRENT_USER_PRIVILEGE_SET =
>> 'current-user-privilege-set' +PROP_SUPPORTED_PRIVILEGE_SET =
>> 'supported-privilege-set' +PROP_PRINCIPAL_COLLECTION_SET =
>> 'principal-collection-set' + +# reports for WebDAV ACP
>> +REPORT_ACL_PRINCIPAL_PROP_SET = 'acl-principal-prop-set' + + +
>> +# constants for WebDAV Delta-V + +# WebDAV Delta-V method names
>> +METHOD_REPORT = 'REPORT' +METHOD_VERSION_CONTROL =
>> 'VERSION-CONTROL' +METHOD_UNCHECKOUT = 'UNCHECKOUT'
>> +METHOD_CHECKOUT = 'CHECKOUT' +METHOD_CHECKIN = 'CHECKIN'
>> +METHOD_UPDATE = 'UPDATE' + +# Special properties
>> +PROP_SUCCESSOR_SET = (NS_DAV, 'successor-set')
>> +PROP_PREDECESSOR_SET = (NS_DAV, 'predecessor-set')
>> +PROP_VERSION_HISTORY = (NS_DAV, 'version-history') +PROP_CREATOR =
>> (NS_DAV, 'creator-displayname') +PROP_VERSION_NAME = (NS_DAV,
>> 'version-name') +PROP_CHECKEDIN = (NS_DAV, 'checked-in')
>> +PROP_CHECKEDOUT = (NS_DAV, 'checked-out') +PROP_COMMENT = (NS_DAV,
>> 'comment') + +# XML tags for request body +TAG_VERSION_TREE =
>> 'version-tree' +TAG_LOCATE_BY_HISTORY = 'locate-by-history'
>> +TAG_UPDATE = 'update' +TAG_VERSION = 'version' + +# HTTP header
>> constants +HTTP_HEADER_DEPTH_INFINITY = 'infinity' +HTTP_HEADER_IF
>> = 'if' +HTTP_HEADER_DAV = 'dav' +HTTP_HEADER_DASL = 'dasl'
>> +HTTP_HEADER_OPTION_ACL = 'access-control'
>> +HTTP_HEADER_OPTION_DAV_BASIC_SEARCH = 'DAV:basicsearch'
>> +HTTP_HEADER_SERVER = 'server' +HTTP_HEADER_SERVER_TAMINO =
>> 'Apache/2.0.54 (Win32)' diff --git a/src/webdav/Makefile.am
>> b/src/webdav/Makefile.am new file mode 100644 index
>> 0000000..3356daf --- /dev/null +++ b/src/webdav/Makefile.am @@ -0,0
>> +1,20 @@ +SUBDIRS = acp + +sugardir = $(pythondir)/webdav
>> +sugar_PYTHON = \ + Connection.py
>> \ + davlib.py \ + logger.py
>> \ + NameCheck.py \ + Utils.py
>> \ + VersionHandler.py \ +
>> WebdavRequests.py \ + Condition.py \ +
>> Constants.py \ + __init__.py \ +
>> qp_xml.py \ + uuid_.py \ +
>> WebdavClient.py \ + WebdavResponse.py + + diff --git
>> a/src/webdav/NameCheck.py b/src/webdav/NameCheck.py new file mode
>> 100644 index 0000000..7976973 --- /dev/null +++
>> b/src/webdav/NameCheck.py @@ -0,0 +1,193 @@ +# pylint:
>> disable-msg=R0904,W0142,W0511,W0104,C0321,E1103,W0212 +# +#
>> Copyright 2008 German Aerospace Center (DLR) +# +# Licensed under
>> the Apache License, Version 2.0 (the "License"); +# you may not use
>> this file except in compliance with the License. +# You may obtain
>> a copy of the License at +# +#
>> http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by
>> applicable law or agreed to in writing, software +# distributed
>> under the License is distributed on an "AS IS" BASIS, +# WITHOUT
>> WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#
>> See the License for the specific language governing permissions
>> and +# limitations under the License. + + +""" +Check name of new
>> collections/resources for "illegal" characters. +""" + + +import
>> re +import unicodedata + + +__version__ = "$LastChangedRevision$"
>> + + +_unicodeUmlaut = [unicodedata.lookup("LATIN CAPITAL LETTER A
>> WITH DIAERESIS"), + unicodedata.lookup("LATIN
>> SMALL LETTER A WITH DIAERESIS"), +
>> unicodedata.lookup("LATIN CAPITAL LETTER O WITH DIAERESIS"), +
>> unicodedata.lookup("LATIN SMALL LETTER O WITH DIAERESIS"), +
>> unicodedata.lookup("LATIN CAPITAL LETTER U WITH DIAERESIS"), +
>> unicodedata.lookup("LATIN SMALL LETTER U WITH DIAERESIS"), +
>> unicodedata.lookup("LATIN SMALL LETTER SHARP S")] + +# Define
>> characters and character base sets +_german =
>> u"".join(_unicodeUmlaut) +_alpha = "A-Za-z" +_num = "0-9"
>> +_alphaNum = _alpha + _num +_space = " " +_under = "_" +_dash =
>> "\-" +_dot = "\." +_exclam = "\!" +_tilde = "\~" +_dollar =
>> "\$" +_plus = "+" +_equal = "=" +_sharp = "#" + +# Define character
>> groups +_letterNum = _alphaNum + _german +_letter = _alpha +
>> _german + +# Define character sets for names +firstPropertyChar =
>> _letter + _under +propertyChar = firstPropertyChar + _num + _dash +
>> _dot +firstResourceChar = firstPropertyChar + _num + _tilde +
>> _exclam + _dollar + \ + _dot + _dash + _plus +
>> _equal + _sharp +resourceChar = firstResourceChar + _space + +#
>> Define regular expressions for name validation +_propertyFirstRe =
>> re.compile(u"^["+ firstPropertyChar +"]") + +_propertyRe =
>> re.compile(u"[^"+ propertyChar +"]") +_resourceFirstRe =
>> re.compile(u"^["+ firstResourceChar +"]") +_resourceRe =
>> re.compile(u"[^"+ resourceChar +"]") + + +def
>> isValidPropertyName(name): + """ + Check if the given
>> property name is valid. + + @param name: Property name. +
>> @type name: C{unicode} + + @return: Boolean indicating whether
>> the given property name is valid or not. + @rtype: C{bool} +
>> """ + + illegalChar = _propertyRe.search(name) + return
>> illegalChar == None and _propertyFirstRe.match(name) != None +
>> + +def isValidResourceName(name): + """ + Check if the given
>> resource name is valid. + + @param name: Resource name. +
>> @type name: C{unicode} + + @return: Boolean indicating whether
>> the given resource name is valid or not. + @rtype: C{bool} +
>> """ + + illegalChar = _resourceRe.search(name) + return
>> illegalChar == None and _resourceFirstRe.match(name) != None + +
>> +def validatePropertyName(name): + """ + Check if the given
>> property name is valid. + + @param name: Property name. +
>> @type name: C{unicode} + @raise WrongNameError: if validation
>> fails (see L{datafinder.common.NameCheck.WrongNameError}) + """
>> + + illegalChar = _propertyRe.search(name) + if illegalChar:
>> + raise WrongNameError(illegalChar.start(),
>> name[illegalChar.start()]) + if not
>> _propertyFirstRe.match(name): + if len(name) > 0: +
>> raise WrongNameError(0, name[0]) + else: + raise
>> WrongNameError(0, 0) + + +def validateResourceName(name): + """
>> + Check if the given resource name is valid. + + @param name:
>> name of resource/collection + @type name: C{unicode} + @raise
>> WrongNameError: if validation fails (@see
>> L{datafinder.common.NameCheck.WrongNameError}) + """ + +
>> illegalChar = _resourceRe.search(name) + if illegalChar: +
>> raise WrongNameError(illegalChar.start(),
>> name[illegalChar.start()]) + if not
>> _resourceFirstRe.match(name): + if len(name) > 0: +
>> raise WrongNameError(0, name[0]) + else: + raise
>> WrongNameError(0, 0) + + +def getResourceNameErrorPosition(name): +
>> """ + Get position of illegal character (and the
>> error-message). + This method can be used to get this
>> information if L{isValidPropertyName} + or
>> L{isValidResourceName} failed. + + @param name: Resource name. +
>> @type name: C{unicode} + + @return: Tuple of error position and
>> message. + @rtype: C{tuple} of C{int} and C{unicode} + """ +
>> + result = (-1, None) + illegalChar =
>> _resourceRe.search(name) + if illegalChar: + result =
>> (illegalChar.start(), \ + u"Illegal character '%s'
>> at index %d." % \ +
>> (name[illegalChar.start()], illegalChar.start())) + elif not
>> _resourceFirstRe.match(name): + result = (0, u"Illegal
>> character '%s' at index %d." % (name[0], 0)) + return result + +
>> +class WrongNameError(ValueError): + """ + Exception raised
>> if an "illegal" character was found. + + @ivar character:
>> character that caused the exception + @type character:
>> C{unicode} + @ivar position: position of C{character} + @type
>> position: C{int} + """ + + def __init__(self, position,
>> character): + """ + Constructor. + + @param
>> character: Character that caused the exception. + @type
>> character: C{unicode} + @param position: Position of
>> C{character} + @type position: C{int} + """ +
>> + ValueError.__init__(self) + self.character =
>> character + self.position = position + + def
>> __str__(self): + """ Returns string representation. """ +
>> + return ValueError.__str__(self) + \ +
>> "Character '%s' at index %d." % (self.character, self.position)
>> diff --git a/src/webdav/Utils.py b/src/webdav/Utils.py new file
>> mode 100644 index 0000000..ec05755 --- /dev/null +++
>> b/src/webdav/Utils.py @@ -0,0 +1,154 @@ +# pylint:
>> disable-msg=W0141,R0912 +# +# Copyright 2008 German Aerospace
>> Center (DLR) +# +# Licensed under the Apache License, Version 2.0
>> (the "License"); +# you may not use this file except in compliance
>> with the License. +# You may obtain a copy of the License at +# +#
>> http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by
>> applicable law or agreed to in writing, software +# distributed
>> under the License is distributed on an "AS IS" BASIS, +# WITHOUT
>> WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#
>> See the License for the specific language governing permissions
>> and +# limitations under the License. + + +""" +The module contains
>> functions to support use of the WebDav functionalities. +""" + +
>> +import os +import sys + +from webdav.WebdavClient import
>> CollectionStorer, ResourceStorer +from webdav.Constants import
>> NS_DAV, PROP_RESOURCE_TYPE, CODE_NOT_FOUND,
>> PROP_RESOURCE_TYPE_RESOURCE +from webdav.Connection import
>> WebdavError + + +__version__ = "$Revision$"[11:-2] + + +def
>> resourceExists(node, name = None, resourceType =
>> PROP_RESOURCE_TYPE_RESOURCE): + """ + Check if resource
>> exists. + + Usage: + -
>> resourceExists(ResourceStorer-object): + check if resource
>> exists + - resourceExists(CollectionStorer-object, name): +
>> check if resource name exists in collection + + @param node:
>> node that has to be checked or node of collection + @type node:
>> L{ResourceStorer<webdav.WebdavClient.ResourceStorer>} + @param
>> name: name of resource (in collection node) that has to be checked
>> + @type name: string + + @return: boolean + + @raise
>> WebdavError: all WebDAV errors except WebDAV error 404 (not found)
>> + """ + + exists = False + if not node: + return
>> exists + try: + myResourceType = "" + if name: +
>> # make sure it's unicode: + if not isinstance(name,
>> unicode): + name =
>> name.decode(sys.getfilesystemencoding()) + url =
>> node.url + if url.endswith("/"): + url =
>> url + name + else: + url = url + "/" +
>> name + newNode = ResourceStorer(url, node.connection) +
>> element = newNode.readProperty(NS_DAV, PROP_RESOURCE_TYPE) +
>> else: # name is "None": + element =
>> node.readProperty(NS_DAV, PROP_RESOURCE_TYPE) + + if
>> len(element.children) > 0: + myResourceType =
>> element.children[0].name + if resourceType == myResourceType
>> or resourceType == PROP_RESOURCE_TYPE_RESOURCE: + exists
>> = True + else: + exists = False + except
>> WebdavError, wderr: + if wderr.code == CODE_NOT_FOUND: +
>> # node doesn't exist -> exists = False: + exists =
>> False + else: + # another exception occured ->
>> "re-raise" it: + raise + return exists + + +def
>> downloadCollectionContent(destinationPath, collectionToDownload): +
>> """ + Downloads the resources contained to the given directory.
>> + + @param destinationPath: Path to download the files to, will
>> be created if it not exists. + @type destinationPath: C{String}
>> + @param collectionToDownload: Collection to download the
>> content from. + @type collectionToDownload: instance of
>> L{CollectionStorer<webdav.WebdavClient.CollectionStorer>} + +
>> @raise WebdavError: If something goes wrong. + """ + + from
>> time import mktime, gmtime + + downloadCount = 0 + +
>> listOfItems = collectionToDownload.getCollectionContents() + +
>> if not os.path.exists(destinationPath): + try: +
>> os.makedirs(destinationPath) + except OSError: +
>> errorMessage = "Cannot create download destination directory '%s'."
>> % destinationPath + raise WebdavError(errorMessage) +
>> + try: + itemsInPath = os.listdir(destinationPath) +
>> except OSError: + errorMessage = "Cannot read the content of
>> download destination directory '%s'." % destinationPath +
>> raise WebdavError(errorMessage) + + for item in listOfItems: +
>> # skip collections + if not isinstance(item[0],
>> CollectionStorer): + itemSavePath =
>> os.path.join(destinationPath, item[0].name) +
>> existsItemSavePath = os.path.exists(itemSavePath) + + #
>> update? + if existsItemSavePath: + try: +
>> isUpdateNecessary = mktime(item[1].getLastModified()) >
>> mktime(gmtime(os.path.getmtime(itemSavePath))) +
>> except (ValueError, OverflowError): +
>> isUpdateNecessary = True + # windows is not case
>> sensitive + for realItem in itemsInPath: +
>> if realItem.lower() == item[0].name.lower(): +
>> itemsInPath.remove(realItem) + else: +
>> isUpdateNecessary = True + + # download + if
>> not existsItemSavePath or (existsItemSavePath and
>> isUpdateNecessary): +
>> item[0].downloadFile(itemSavePath) + downloadCount =
>> downloadCount + 1 + + # delete old items + try: + for
>> item in itemsInPath: +
>> os.remove(os.path.join(destinationPath, item)) + except OSError,
>> e: + if e.errno == 13: # permission error +
>> sys.stderr.write("permission problem on '%s' in %s\n" %
>> (e.filename, e.strerror)) + else: + raise +
>> + return downloadCount diff --git
>> a/src/webdav/VersionHandler.py b/src/webdav/VersionHandler.py new
>> file mode 100644 index 0000000..a1962c6 --- /dev/null +++
>> b/src/webdav/VersionHandler.py @@ -0,0 +1,198 @@ +# pylint:
>> disable-msg=W0612,W0142 +# +# Copyright 2008 German Aerospace
>> Center (DLR) +# +# Licensed under the Apache License, Version 2.0
>> (the "License"); +# you may not use this file except in compliance
>> with the License. +# You may obtain a copy of the License at +# +#
>> http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by
>> applicable law or agreed to in writing, software +# distributed
>> under the License is distributed on an "AS IS" BASIS, +# WITHOUT
>> WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#
>> See the License for the specific language governing permissions
>> and +# limitations under the License. + + +""" +The WebDAV client
>> module forwards Delta-V related method invocations to +the
>> following VersionHandler class. +""" + +__version__ =
>> '$Revision$'[11:-2] + + +import types + +from webdav import
>> Constants +from davlib import XML_CONTENT_TYPE, XML_DOC_HEADER + +
>> +class VersionHandler(object): + """ + Implements a client
>> interface for WebDAV Delta-V methods + For the Delta-V see RFC
>> 3253 at http://www.ietf.org/rfc/rfc3253.txt + """ + + #
>> restrict instance variables + __slots__ = ('path',
>> 'connection') + + + def __init__(self, connection, path): +
>> """ + Construct a VersionHandler with a URL path and a
>> WebDAV connection. + This constructor must not be called
>> outside class ResourceStorer. + + @param connection:
>> L{webdav.Connection} instance + @param path: resource's path
>> part of URL + """ + #assert isinstance(connection,
>> Connection), \ + # "Class of connection is %s." %
>> connection.__class__.__name__ + self.connection =
>> connection + self.path = path + + + def
>> activateVersionControl(self): + """ + Turns version
>> control on for this resource. + The resource becomes a
>> version controlled resource (VCR) + """ + response =
>> self.connection._request(Constants.METHOD_VERSION_CONTROL,
>> self.path, None, {}) + # set auto-versioning to
>> DAV:locked-checkout + ## parse response body in case of an
>> error + + def uncheckout(self, lockToken=None): + """ +
>> Undos a previous check-out operation on this VCR. + The VCR
>> is reverted to the state before the checkout/lock operation. +
>> Beware: Property or content changes will be lost ! + A
>> (optional) lock has to be removed seperatedly. + + @param
>> lockToken: returned by a preceeding lock() method invocation or
>> None + """ + headers = {} + if lockToken: +
>> headers = lockToken.toHeader() + response =
>> self.connection._request(Constants.METHOD_UNCHECKOUT, self.path,
>> None, headers) + ## parse response body in case of an error
>> + + def listAllVersions(self): + """ + List
>> version history. + + @return: List of versions for this VCR.
>> Each version entry is a tuple adhering + to the format
>> (URL-path, name, creator, tuple of successor URL-paths). +
>> If there are no branches then there is at most one successor within
>> the tuple. + """ + # implementation is similar to the
>> propfind method + headers = {} +
>> headers['Content-Type'] = XML_CONTENT_TYPE + body =
>> _createReportVersionTreeBody() + response =
>> self.connection._request(Constants.METHOD_REPORT, self.path, body,
>> headers) + # response is multi-status + result = [] +
>> for path, properties in response.msr.items(): + # parse
>> the successor-set value from XML into alist +
>> result.append( (path, str(properties[Constants.PROP_VERSION_NAME]),
>> \ +
>> str(properties[Constants.PROP_CREATOR]), \ +
>> _extractSuccessorList(properties[Constants.PROP_SUCCESSOR_SET])) )
>> + ## TODO: sort for path and produce list +
>> result.sort() + return result + + # warning: not tested
>> yet + def readVersionProperties(self): + """ +
>> Provide version related information on this VCR. + This
>> include a reference to the latest version resource, +
>> check-out state information and a comment. + + @return: map
>> of version properties with values. + """ +
>> versionProperties = (Constants.PROP_CHECKEDIN,
>> Constants.PROP_CHECKEDOUT, Constants.PROP_COMMENT) + return
>> self.connection.readProperties(*versionProperties) + + + def
>> revertToVersion(self, oldVersion): + """ + Revert
>> this VCR to the given version. + Beware: All versions
>> succeeding the given version are made unavailable. + +
>> @param oldVersion: URL-path of a previous version of this VCR. +
>> """ + ## send an update request + assert
>> isinstance(oldVersion, types.StringType) or isinstance(oldVersion,
>> types.UnicodeType) + response =
>> self.connection._request(Constants.METHOD_UPDATE, self.path, +
>> _createUpdateBody(oldVersion), {}) + return response + + +
>> # the following is not needed when using auto-versioning + + #
>> warning: not tested yet + def checkout(self): + """ +
>> Marks resource as checked-out + This is usually followed by
>> a GET (download) operation. + """ + response =
>> self.connection._request(Constants.METHOD_CHECKOUT, self.path,
>> None, {}) + ## parse response body in case of an error + +
>> # warning: not tested yet + def checkin(self): + """ +
>> Creates a new version from the VCR's content. + This
>> opeartion is usually preceeded by one or more write operations. +
>> """ + response =
>> self.connection._request(Constants.METHOD_CHECKIN, self.path, None,
>> {}) + versionUrl = response.getheader('Location') +
>> return versionUrl + ## parse response body in case of an
>> error + + + + +# Helper functions +def
>> _createReportVersionTreeBody(): + """ + TBD + + @return:
>> ... + @rtype: string + """ + versions = 'D:' +
>> Constants.TAG_VERSION_TREE + prop = 'D:' + Constants.TAG_PROP +
>> nameList = [Constants.PROP_SUCCESSOR_SET,
>> Constants.PROP_VERSION_NAME, Constants.PROP_CREATOR] + return
>> XML_DOC_HEADER + \ + '<%s xmlns:D="DAV:"><%s>' % (versions,
>> prop) + \ + reduce(lambda xml, name: xml + "<D:%s/>" %
>> name[1], [''] + nameList) + \ + '</%s></%s>' % (prop,
>> versions) + +def _createUpdateBody(path): + """ + TBD + +
>> @return: ... + @rtype: string + """ + update = 'D:' +
>> Constants.TAG_UPDATE + version = 'D:' + Constants.TAG_VERSION +
>> href = 'D:' + Constants.TAG_HREF + #PROP = 'D:' + TAG_PROP +
>> return XML_DOC_HEADER + \ + '<%s xmlns:D="DAV:"><%s><%s>' %
>> (update, version, href) + \ + path + \ +
>> '</%s></%s></%s>' % (href, version, update) + +def
>> _extractSuccessorList(element): + """ + TBD + + @return:
>> ... + @rtype: tuple of strings + """ + result = [] +
>> for href in element.children: +
>> result.append(href.textof()) + return tuple(result) diff --git
>> a/src/webdav/WebdavClient.py b/src/webdav/WebdavClient.py new file
>> mode 100644 index 0000000..4d6278d --- /dev/null +++
>> b/src/webdav/WebdavClient.py @@ -0,0 +1,1053 @@ +# pylint:
>> disable-msg=R0904,W0142,W0511,W0104,C0321,E1103,W0212 +# +#
>> Copyright 2008 German Aerospace Center (DLR) +# +# Licensed under
>> the Apache License, Version 2.0 (the "License"); +# you may not use
>> this file except in compliance with the License. +# You may obtain
>> a copy of the License at +# +#
>> http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by
>> applicable law or agreed to in writing, software +# distributed
>> under the License is distributed on an "AS IS" BASIS, +# WITHOUT
>> WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#
>> See the License for the specific language governing permissions
>> and +# limitations under the License. + + +""" +This module
>> contains the classes ResourceStorer and CollectionStorer for
>> accessing WebDAV resources. +""" + + +from davlib import
>> XML_CONTENT_TYPE + +from urlparse import urlsplit +import re
>> +import types +import sys +import os +import shutil + +import
>> simplejson + +from webdav import Constants +from
>> webdav.WebdavResponse import LiveProperties +from
>> webdav.WebdavRequests import createFindBody, createUpdateBody,
>> createDeleteBody, createSearchBody +from webdav.Condition import
>> ConditionTerm +from webdav.Connection import Connection,
>> WebdavError, AuthorizationError +from webdav.VersionHandler import
>> VersionHandler + +from webdav.acp.Privilege import Privilege +from
>> webdav.acp.Acl import ACL +from webdav.NameCheck import
>> validateResourceName, WrongNameError + + +__version__ =
>> '$Revision$'[11:-2] + + +def switchUnicodeUrlOn(switch): + """ +
>> Configure whether to use unicode (UTF-8) encoded URLs (default) or
>> + Latin-1 encoded URLs. + + @param switch: 1 if unicode URLs
>> shall be used + """ + + assert switch == 0 or switch == 1,
>> "Pass boolean argument, please." + Constants.CONFIG_UNICODE_URL
>> = switch + + +def parseDigestAuthInfo(authInfo): + """ +
>> Parses the authentication information returned from a server and
>> returns + a dictionary containing realm, qop, and nonce. + +
>> @see: L{AuthorizationError<webdav.Connection.AuthorizationError>} +
>> or the main function of this module. + """ + + info = dict()
>> + info["realm"] = re.search('realm="([^"]+)"',
>> authInfo).group(1) + info["qop"] = re.search('qop="([^"]+)"',
>> authInfo).group(1) + info["nonce"] =
>> re.search('nonce="([^"]+)"', authInfo).group(1) + return info +
>> + +class ResourceStorer(object): + """ + This class provides
>> client access to a WebDAV resource + identified by an URI. It
>> provides all WebDAV class 2 features which include + uploading
>> data, getting and setting properties qualified by a XML name
>> space, + locking and unlocking the resource. + This class
>> does not cache resource data. This has to be performed by its
>> clients. + + @author: Roland Betz + """ + + # Instance
>> properties + url = property(lambda self: str(self.connection) +
>> self.path, None, None, "Resource's URL") + + def __init__(self,
>> url, connection=None, validateResourceNames=True): + """ +
>> Creates an instance for the given URL + User must invoke
>> validate() after construction to check the resource on the server.
>> + + @param url: Unique resource location for this storer. +
>> @type url: C{string} + @param connection: this optional
>> parameter contains a Connection object + for the host
>> part of the given URL. Passing a connection saves +
>> memory by sharing this connection. (defaults to None) +
>> @type connection: L{webdav.Connection} + @raise
>> WebdavError: If validation of resource name path parts fails. +
>> """ + + assert connection == None or isinstance(connection,
>> Connection) + parts = urlsplit(url, allow_fragments=False) +
>> self.path = parts[2] + self.validateResourceNames =
>> validateResourceNames + + # validate URL path + for
>> part in self.path.split('/'): + if part != '' and not
>> "ino:" in part: # explicitly allowing this character sequence as a
>> part of a path (Tamino 4.4) + if
>> self.validateResourceNames: + try: +
>> validateResourceName(part) + except
>> WrongNameError: + raise WebdavError("Found
>> invalid resource name part.") + self.name = part +
>> # was: filter(lambda part: part and validateResourceName(part),
>> self.path.split('/')) + # but filter is deprecated +
>> + self.defaultNamespace = None # default XML name space
>> of properties + if connection: + self.connection
>> = connection + else: + conn =
>> parts[1].split(":") + if len(conn) == 1: +
>> self.connection = Connection(conn[0], protocol = parts[0]) # host
>> and protocol + else: + self.connection =
>> Connection(conn[0], int(conn[1]), protocol = parts[0]) # host and
>> port and protocol + self.versionHandler =
>> VersionHandler(self.connection, self.path) + + + def
>> validate(self): + """ + Check whether URL contains a
>> WebDAV resource + Uses the WebDAV OPTIONS method. + +
>> @raise WebdavError: L{WebdavError} if URL does not contain a WebDAV
>> resource + """ + #davHeader =
>> response.getheader(HTTP_HEADER_DAV) + davHeader =
>> self.getSpecificOption(Constants.HTTP_HEADER_DAV) +
>> self.connection.logger.debug("HEADER DAV: %s" % davHeader) +
>> if not(davHeader) or davHeader.find("2") < 0: # DAV class 2
>> supported ? + raise WebdavError("URL does not support
>> WebDAV", 0) + + def options(self): + """ + Send an
>> OPTIONS request to server and return all HTTP headers. + +
>> @return: map of all HTTP headers returned by the OPTIONS method. +
>> """ + response = self.connection.options(self.path) +
>> result = {} + result.update(response.msg) +
>> self.connection.logger.debug("OPTION returns: " +
>> str(result.keys())) + return result + + def
>> _getAclSupportAvailable(self): + """ + Returns True
>> if the current connection has got ACL support. + + @return:
>> ACL support (True / False) + @rtype: C{bool} + """ +
>> options = self.getSpecificOption(Constants.HTTP_HEADER_DAV) +
>> if options.find(Constants.HTTP_HEADER_OPTION_ACL) >= 0: +
>> return True + else: + return False + +
>> aclSupportAvailable = property(_getAclSupportAvailable) + + def
>> _getDaslBasicsearchSupportAvailable(self): + """ +
>> Returns True if the current connection supports DASL basic search.
>> + + @return: DASL basic search support (True / False) +
>> @rtype: C{bool} + """ + options =
>> self.getSpecificOption(Constants.HTTP_HEADER_DASL) + if not
>> options or \ + not
>> options.find(Constants.HTTP_HEADER_OPTION_DAV_BASIC_SEARCH) >= 0: +
>> return False + else: + return True + +
>> daslBasicsearchSupportAvailable =
>> property(_getDaslBasicsearchSupportAvailable) + + def
>> isConnectedToCatacombServer(self): + """ + Returns
>> True if connected to a Catacomb WebDav server. + + @return:
>> if connected to Catacomb Webdav server (True / False) +
>> @rtype: C{bool} + """ + if not
>> self.connection.serverTypeChecked: + options =
>> self.getSpecificOption(Constants.HTTP_HEADER_SERVER) +
>> if options.find(Constants.HTTP_HEADER_SERVER_TAMINO) >= 0: +
>> self.connection.isConnectedToCatacomb = False + else: +
>> self.connection.isConnectedToCatacomb = True +
>> self.connection.serverTypeChecked = True + return
>> self.connection.isConnectedToCatacomb + + def
>> getSpecificOption(self, option): + """ + Returns
>> specified WebDav options. + @param option: name of the
>> option + + @return: String containing the value of the
>> option. + @rtype: C{string} + """ + options =
>> '' + try: + options = self.options().get(option)
>> + except KeyError: + return options +
>> return options + + ### delegate some method invocations + def
>> __getattr__(self, name): + """ + Build-in method: +
>> Forwards unknow lookups (methods) to delegate object
>> 'versionHandler'. + + @param name: name of unknown
>> attribute + """ + # delegate Delta-V methods +
>> return getattr(self.versionHandler, name) + + def copy(self,
>> toUrl, infinity=True): + """ + Copies this resource.
>> + + @param toUrl: target URI path + @param infinity:
>> Flag that indicates that the complete content of collection is
>> copied. (default) + @type depth: C{boolean} + """ +
>> self.connection.logger.debug("Copy to " + repr(toUrl)); +
>> _checkUrl(toUrl) + if infinity: + response =
>> self.connection.copy(self.path, toUrl) + else: +
>> response = self.connection.copy(self.path, toUrl, 0) + if
>> response.status == Constants.CODE_MULTISTATUS and
>> response.msr.errorCount > 0: + raise
>> WebdavError("Request failed: " + response.msr.reason,
>> response.msr.code) + + def delete(self, lockToken=None): +
>> """ + Deletes this resource. + + @param lockToken:
>> String returned by last lock operation or null. + @type
>> lockToken: L{LockToken} + """ + assert lockToken ==
>> None or isinstance(lockToken, LockToken), \ +
>> "Invalid lockToken argument %s" % type(lockToken) + header =
>> {} + if lockToken: + header =
>> lockToken.toHeader() + response =
>> self.connection.delete(self.path, header) + if
>> response.status == Constants.CODE_MULTISTATUS and
>> response.msr.errorCount > 0: + raise
>> WebdavError("Request failed: " + response.msr.reason,
>> response.msr.code) + + def move(self, toUrl): + """ +
>> Moves this resource to the given path or renames it. + +
>> @param toUrl: new (URI) path + """ +
>> self.connection.logger.debug("Move to " + repr(toUrl)); +
>> _checkUrl(toUrl) + response =
>> self.connection.move(self.path, toUrl) + if response.status
>> == Constants.CODE_MULTISTATUS and response.msr.errorCount > 0: +
>> raise WebdavError("Request failed: " + response.msr.reason,
>> response.msr.code) + + + def lock(self, owner): + """ +
>> Locks this resource for exclusive write access. This means that for
>> succeeding + write operations the returned lock token has to
>> be passed. + If the methode does not throw an exception the
>> lock has been granted. + + @param owner: describes the lock
>> holder + @return: lock token string (automatically
>> generated) + @rtype: L{LockToken} + """ +
>> response = self.connection.lock(self.path, owner) + if
>> response.status == Constants.CODE_MULTISTATUS and
>> response.msr.errorCount > 0: + raise
>> WebdavError("Request failed: " + response.msr.reason,
>> response.msr.code) + return LockToken(self.url,
>> response.locktoken) + + def unlock(self, lockToken): +
>> """ + Removes the lock from this resource. + + @param
>> lockToken: which has been return by the lock() methode +
>> @type lockToken: L{LockToken} + """ +
>> self.connection.unlock(self.path, lockToken.token) + + + def
>> deleteContent(self, lockToken=None): + """ + Delete
>> binary data at permanent storage. + + @param lockToken: None
>> or lock token from last lock request + @type lockToken:
>> L{LockToken} + """ + assert lockToken == None or
>> isinstance(lockToken, LockToken), \ + "Invalid
>> lockToken argument %s" % type(lockToken) + header = {} +
>> if lockToken: + header = lockToken.toHeader() +
>> self.connection.put(self.path, "", extra_hdrs=header) + + def
>> uploadContent(self, content, lockToken=None): + """ +
>> Write binary data to permanent storage. + + @param content:
>> containing binary data + @param lockToken: None or lock
>> token from last lock request + @type lockToken:
>> L{LockToken} + """ + assert not content or
>> isinstance(content, types.UnicodeType) or\ +
>> isinstance(content, types.StringType), "Content is not a string: "
>> + content.__class__.__name__ + assert lockToken == None or
>> isinstance(lockToken, LockToken), \ + "Invalid
>> lockToken argument %s" % type(lockToken) + header = {} +
>> if lockToken: + header = lockToken.toHeader() +
>> response = None + if not content is None: +
>> header["Content-length"] = len(content) + else: +
>> header["Content-length"] = 0 + + try: + response
>> = self.connection.put(self.path, content, extra_hdrs=header) +
>> finally: + if response: +
>> self.connection.logger.debug(response.read()) +
>> response.close() + + def uploadFile(self, newFile,
>> lockToken=None): + """ + Write binary data to
>> permanent storage. + + @param newFile: File containing
>> binary data. + @param lockToken: None or lock token from
>> last lock request + @type lockToken: L{LockToken} +
>> """ + assert isinstance(newFile, types.FileType), "Argument
>> is no file: " + file.__class__.__name__ + assert lockToken
>> == None or isinstance(lockToken, LockToken), \ +
>> "Invalid lockToken argument %s" % type(lockToken) + header =
>> {} + if lockToken: + header =
>> lockToken.toHeader() + self.connection.putFile(self.path,
>> newFile, header=header) + + def downloadContent(self): +
>> """ + Read binary data from permanent storage. + """
>> + response = self.connection.get(self.path) + # TODO:
>> Other interface ? return self.connection.getfile() + return
>> response + + def downloadFile(self, localFileName): +
>> """ + Copy binary data from permanent storage to a local
>> file. + + @param localFileName: file to write binary data
>> to + """ + localFile = open(localFileName, 'wb') +
>> remoteFile = self.downloadContent() +
>> _blockCopyFile(remoteFile, localFile, Connection.blockSize) +
>> remoteFile.close() + localFile.close() + + def
>> readProperties(self, *names): + """ + Reads the given
>> properties. + + @param names: a list of property names. +
>> A property name is a (XmlNameSpace, propertyName) tuple. +
>> @return: a map from property names to DOM Element or String
>> values. + """ + assert names, "Property names are
>> missing." + body = createFindBody(names,
>> self.defaultNamespace) + response =
>> self.connection.propfind(self.path, body, depth=0) +
>> properties = response.msr.values()[0] + if
>> properties.errorCount > 0: + raise WebdavError("Property
>> is missing on '%s': %s" % (self.path, properties.reason),
>> properties.code) + return properties + + def
>> readProperty(self, nameSpace, name): + """ + Reads
>> the given property. + + @param nameSpace: XML-namespace +
>> @type nameSpace: string + @param name: A property name. +
>> @type name: string + + @return: a map from property names to
>> DOM Element or String values. + """ + results =
>> self.readProperties((nameSpace, name)) + if len(results) ==
>> 0: + raise WebdavError("Property is missing: " +
>> results.reason) + return results.values()[0] + + def
>> readAllProperties(self): + """ + Reads all properties
>> of this resource. + + @return: a map from property names to
>> DOM Element or String values. + """ + response =
>> self.connection.allprops(self.path, depth=0) + return
>> response.msr.values()[0] + + def readAllPropertyNames(self): +
>> """ + Returns the names of all properties attached to this
>> resource. + + @return: List of property names + """ +
>> response = self.connection.propnames(self.path, depth=0) +
>> return response.msr.values()[0] + + def
>> readStandardProperties(self): + """ + Read all WebDAV
>> live properties. + + @return: A L{LiveProperties} instance
>> which contains a getter method for each live property. +
>> """ + body = createFindBody(LiveProperties.NAMES,
>> Constants.NS_DAV) + response =
>> self.connection.propfind(self.path, body, depth=0) +
>> properties = response.msr.values()[0] + return
>> LiveProperties(properties) + + def writeProperties(self,
>> properties, lockToken=None): + """ + Sets or updates
>> the given properties. + + @param lockToken: if the resource
>> has been locked this is the lock token. + @type lockToken:
>> L{LockToken} + @param properties: a map from property names
>> to a String or + DOM element value for
>> each property to add or update. + """ + assert
>> isinstance(properties, types.DictType) + assert lockToken ==
>> None or isinstance(lockToken, LockToken), \ +
>> "Invalid lockToken argument %s" % type(lockToken) + header =
>> {} + if lockToken: + header =
>> lockToken.toHeader() + body = createUpdateBody(properties,
>> self.defaultNamespace) + response =
>> self.connection.proppatch(self.path, body, header) + if
>> response.msr.errorCount > 0: + raise
>> WebdavError("Request failed: " + response.msr.reason,
>> response.msr.code) + + def deleteProperties(self,
>> lockToken=None, *names): + """ + Removes the given
>> properties from this resource. + + @param lockToken: if the
>> resource has been locked this is the lock token. + @type
>> lockToken: L{LockToken} + @param names: a collection of
>> property names. + A property name is a (XmlNameSpace,
>> propertyName) tuple. + """ + assert lockToken == None
>> or isinstance(lockToken, LockToken), \ + "Invalid
>> lockToken argument %s" % type(lockToken) + header = {} +
>> if lockToken: + header = lockToken.toHeader() +
>> body = createDeleteBody(names, self.defaultNamespace) +
>> response = self.connection.proppatch(self.path, body, header) +
>> if response.msr.errorCount > 0: + raise
>> WebdavError("Request failed: " + response.msr.reason,
>> response.msr.code) + + # ACP extension + def setAcl(self,
>> acl, lockToken=None): + """ + Sets ACEs in the
>> non-inherited and non-protected ACL or the resource. + This
>> is the implementation of the ACL method of the WebDAV ACP. +
>> + @param acl: ACL to be set on resource as ACL object. +
>> @param lockToken: If the resource has been locked this is the lock
>> token (defaults to None). + @type lockToken: L{LockToken} +
>> """ + assert lockToken == None or isinstance(lockToken,
>> LockToken), \ + "Invalid lockToken argument %s" %
>> type(lockToken) + headers = {} + if lockToken: +
>> headers = lockToken.toHeader() + headers['Content-Type'] =
>> XML_CONTENT_TYPE + body = acl.toXML() +
>> response = self.connection._request('ACL', self.path, body,
>> headers) + return response + ## TODO: parse DAV:error
>> response + + def getAcl(self): + """ + Returns
>> this resource's ACL in an ACL instance. + + @return: Access
>> Control List. + @rtype: L{ACL<webdav.acp.Acl.ACL>} +
>> """ + xmlAcl = self.readProperty(Constants.NS_DAV,
>> Constants.TAG_ACL) + return ACL(xmlAcl) + + def
>> getCurrentUserPrivileges(self): + """ + Returns a
>> tuple of the current user privileges. + + @return: list of
>> Privilege instances + @rtype: list of
>> L{Privilege<webdav.acp.Privilege.Privilege>} + """ +
>> privileges = self.readProperty(Constants.NS_DAV,
>> Constants.PROP_CURRENT_USER_PRIVILEGE_SET) + result = [] +
>> for child in privileges.children: +
>> result.append(Privilege(domroot=child)) + return result +
>> + def getPrincipalCollections(self): + """ +
>> Returns a list principal collection URLs. + + @return: list
>> of principal collection URLs + @rtype: C{list} of C{unicode}
>> elements + """ + webdavQueryResult =
>> self.readProperty(Constants.NS_DAV,
>> Constants.PROP_PRINCIPAL_COLLECTION_SET) +
>> principalCollectionList = [] + for child in
>> webdavQueryResult.children: +
>> principalCollectionList.append(child.first_cdata) + return
>> principalCollectionList + + def getOwnerUrl(self): + """
>> Explicitly retireve the Url of the owner. """ + + result =
>> self.readProperty(Constants.NS_DAV, Constants.PROP_OWNER) +
>> if result and len(result.children): + return
>> result.children[0].textof() + return None + +class
>> CollectionStorer(ResourceStorer): + """ + This class provides
>> client access to a WebDAV collection resource identified by an
>> URI. + This class does not cache resource data. This has to be
>> performed by its clients. + + @author: Roland Betz + """ +
>> + def __init__(self, url, connection=None,
>> validateResourceNames=True): + """ + Creates a
>> CollectionStorer instance for a URL and an optional Connection
>> object. + User must invoke validate() after constuction to
>> check the resource on the server. + + @see:
>> L{webdav.WebdavClient.ResourceStorer.__init__} + @param url:
>> unique resource location for this storer + @param
>> connection: this optional parameter contains a Connection object
>> for the host part + of the given URL. Passing a
>> connection saves memory by sharing this connection. + """ +
>> if url[-1] != '/': # Collection URL must end with slash +
>> url += '/' + ResourceStorer.__init__(self, url, connection,
>> validateResourceNames) + + def getResourceStorer(self, name): +
>> """ + Return a ResourceStorer instance for a child resource
>> (member) of this Collection. + + @param name: leaf name of
>> child resource + @return: L{ResourceStorer} instance +
>> """ + assert isinstance(name, types.StringType) or
>> isinstance(name, types.UnicodeType) + return
>> ResourceStorer(self.url + name, self.connection,
>> self.validateResourceNames) + + def validate(self): +
>> """ + Check whether this URL contains a WebDAV collection. +
>> Uses the WebDAV OPTION method. + + @raise WebdavError:
>> L{WebdavError} if URL does not contain a WebDAV collection
>> resource. + """ + super(CollectionStorer,
>> self).validate() + isCollection =
>> self.readProperty(Constants.NS_DAV, Constants.PROP_RESOURCE_TYPE) +
>> if not (isCollection and isCollection.children): + raise
>> WebdavError("Not a collection URL.", 0) + + def
>> addCollection(self, name, lockToken=None): + """ +
>> Make a new WebDAV collection resource within this collection. +
>> + @param name: of the new collection + @param
>> lockToken: None or token returned by last lock operation +
>> @type lockToken: L{LockToken} + """ + assert
>> isinstance(name, types.StringType) or isinstance(name,
>> types.UnicodeType) + assert lockToken == None or
>> isinstance(lockToken, LockToken), \ + "Invalid
>> lockToken argument %s" % type(lockToken) + header = {} +
>> if lockToken: + header = lockToken.toHeader() +
>> if self.validateResourceNames: +
>> validateResourceName(name) + if name[-1] != '/': #
>> Collection URL must end with slash + name += '/' +
>> self.connection.mkcol(self.path + name, header) + return
>> CollectionStorer(self.url + name, self.connection,
>> self.validateResourceNames) + + def addResource(self, name,
>> content=None, properties=None, lockToken=None): + """ +
>> Create a new empty WebDAV resource contained in this collection
>> with the given + properties. + + @param name: leaf
>> name of the new resource + @param content: None or initial
>> binary content of resource + @param properties:
>> name/value-map containing properties + @param lockToken:
>> None or token returned by last lock operation + @type
>> lockToken: L{LockToken} + """ + assert
>> isinstance(name, types.StringType) or isinstance(name,
>> types.UnicodeType) + assert lockToken == None or
>> isinstance(lockToken, LockToken), \ + "Invalid
>> lockToken argument %s" % type(lockToken) + if
>> self.validateResourceNames: + validateResourceName(name)
>> # check for invalid characters + resource_ =
>> ResourceStorer(self.url + name, self.connection,
>> self.validateResourceNames) +
>> resource_.uploadContent(content, lockToken) + if
>> properties: + resource_.writeProperties(properties,
>> lockToken) + return resource_ + + def
>> deleteResource(self, name, lockToken=None): + """ +
>> Delete a collection which is contained within this collection +
>> + @param name: leaf name of a contained collection
>> resource + @param lockToken: None or token returned by last
>> lock operation + @type lockToken: L{LockToken} +
>> """ + assert isinstance(name, types.StringType) or
>> isinstance(name, types.UnicodeType) + assert lockToken ==
>> None or isinstance(lockToken, LockToken), \ +
>> "Invalid lockToken argument %s" % type(lockToken) + header =
>> {} + if lockToken: + header =
>> lockToken.toHeader() + if self.validateResourceNames: +
>> validateResourceName(name) + response =
>> self.connection.delete(self.path + name, header) + if
>> response.status == Constants.CODE_MULTISTATUS and
>> response.msr.errorCount > 0: + raise
>> WebdavError("Request failed: %s" % response.msr.reason,
>> response.msr.code) + + def lockAll(self, owner): + """ +
>> Locks this collection resource for exclusive write access. This
>> means that for + succeeding write operations the returned
>> lock token has to be passed. + The operation is applied
>> recursively to all contained resources. + If the methode
>> does not throw an exception then the lock has been granted. +
>> + @param owner: describes the lock holder + @return:
>> Lock token string (automatically generated). + @rtype:
>> L{LockToken} + """ + assert isinstance(owner,
>> types.StringType) or isinstance(owner, types.UnicodeType) +
>> response = self.connection.lock(self.path, owner,
>> depth=Constants.HTTP_HEADER_DEPTH_INFINITY) + return
>> LockToken(self.url, response.locktoken) + + def
>> listResources(self): + """ + Describe all members
>> within this collection. + + @return: map from URI to a
>> L{LiveProperties} instance containing the WebDAV +
>> live attributes of the contained resource + """ + #
>> *LiveProperties.NAMES denotes the list of all live properties as
>> an + # argument to the method call. + response =
>> self.connection.getprops(self.path, +
>> depth=1, +
>> ns=Constants.NS_DAV, +
>> *LiveProperties.NAMES) + result = {} + for path,
>> properties in response.msr.items(): + if path ==
>> self.path: # omit this collection resource +
>> continue + ## some servers do not append a trailing
>> slash to collection paths + if self.path.endswith('/')
>> and self.path[0:-1] == path: + continue +
>> result[path] = LiveProperties(properties=properties) +
>> return result + + def getCollectionContents(self): + """
>> + Return a list of the tuple (resources or collection) /
>> properties) + + @return: a list of the tuple (resources or
>> collection) / properties) + @rtype: C{list} + """ +
>> self.validate() + collectionContents = [] + result =
>> self.listResources() + for url, properties_ in
>> result.items(): + if not self.path == url: +
>> if properties_.getResourceType() == 'resource': +
>> myWebDavStorer = ResourceStorer(url, self.connection,
>> self.validateResourceNames) + else: +
>> myWebDavStorer = CollectionStorer(url, self.connection,
>> self.validateResourceNames) +
>> collectionContents.append((myWebDavStorer, properties_)) +
>> return collectionContents + + def findProperties(self, *names):
>> + """ + Retrieve given properties for this collection
>> and all directly contained resources. + + @param names: a
>> list of property names + @return: a map from resource URI to
>> a map from property name to value. + """ + assert
>> isinstance(names, types.ListType) or isinstance(names,
>> types.TupleType), \ + "Argument name has type %s" %
>> str(type(names)) + body = createFindBody(names,
>> self.defaultNamespace) + response =
>> self.connection.propfind(self.path, body, depth=1) + return
>> response.msr + + def deepFindProperties(self, *names): +
>> """ + Retrieve given properties for this collection and all
>> contained (nested) resources. + + Note: + ===== +
>> This operation can take a long time if used with recursive=true and
>> is therefore + disabled on some WebDAV servers. + +
>> @param names: a list of property names + @return: a map from
>> resource URI to a map from property name to value. + """ +
>> assert isinstance(names, types.ListType.__class__) or
>> isinstance(names, types.TupleType), \ + "Argument
>> name has type %s" % str(type(names)) + body =
>> createFindBody(names, self.defaultNamespace) + response =
>> self.connection.propfind(self.path, body,
>> depth=Constants.HTTP_HEADER_DEPTH_INFINITY) + return
>> response.msr + + def findAllProperties(self): + """ +
>> Retrieve all properties for this collection and all directly
>> contained resources. + + @return: a map from resource URI to
>> a map from property name to value. + """ + response =
>> self.connection.allprops(self.path, depth=1) + return
>> response.msr + + + # DASL extension + def search(self,
>> conditions, selects): + """ + Search for contained
>> resources which match the given search condition. + + @param
>> conditions: tree of ConditionTerm instances representing a logical
>> search term + @param selects: list of property names to
>> retrieve for the found resources + """ + assert
>> isinstance(conditions, ConditionTerm) + headers = {
>> 'Content-Type' : XML_CONTENT_TYPE, "depth":
>> Constants.HTTP_HEADER_DEPTH_INFINITY} + body =
>> createSearchBody(selects, self.path, conditions) + response
>> = self.connection._request('SEARCH', self.path, body, headers) +
>> return response.msr + + +class LockToken(object): + """ +
>> This class provides help on handling WebDAV lock tokens. + +
>> @author: Roland Betz + """ + # restrict instance variables +
>> __slots__ = ('url', 'token') + + def __init__(self, url,
>> token): + assert isinstance(url, types.StringType) or
>> isinstance(url, types.UnicodeType), \ + "Invalid url
>> argument %s" % type(url) + assert isinstance(token,
>> types.StringType) or isinstance(token, types.UnicodeType), \ +
>> "Invalid lockToken argument %s" % type(token) + self.url =
>> url + self.token = token + + def value(self): +
>> """ + Descriptive string containing the lock token's URL and
>> the token itself. + + @return: Descriptive lock token with
>> URL. + @rtype: C{string} + """ + return "<" +
>> self.url + "> (<" + self.token + ">)" + + def toHeader(self): +
>> """ + Header fragment for WebDAV request. + +
>> @return: Dictionary containing an entry for the lock token query. +
>> @rtype: C{dictionary} + """ + return
>> {Constants.HTTP_HEADER_IF: self.value()} + + def __str__(self):
>> + return self.value() + + +def _blockCopyFile(source, dest,
>> blockSize): + """ + Copies a file in chunks of C{blockSize}.
>> + + @param source: Source file. + @type source: FileIO
>> buffer. + @param dest: Destination file. + @type dest:
>> FileIO buffer. + @param blockSize: Size of block in bytes. +
>> @type blockSize: C{int} + """ + transferedBytes = 0 +
>> block = source.read(blockSize) + while len(block): +
>> dest.write(block) + transferedBytes += len(block); +
>> block = source.read(blockSize) + +def _checkUrl(url): + """ +
>> Checks the given URL for validity. + + @param url: URL to
>> check. + @type url: C{string} + + @raise ValueError: If the
>> URL does not contain valid/usable content. + """ + + parts =
>> urlsplit(url, allow_fragments=False) + if len(parts[0]) == 0 or
>> len(parts[1]) == 0 or len(parts[2]) == 0: + raise
>> ValueError("Invalid URL: " + repr(url)) + + +def
>> get_key_from_resource(resource): + return resource.path + +def
>> ensure_correct_remote_webdav_hierarchy(remote_webdav_share_resources,
>>
>>
> +
> remote_webdav_share_collections):
>> + pass + #assert len(remote_webdav_share_collections.keys())
>> == 1 + +class WebDavUrlManager(): + """ + This class holds
>> all data, relevant to a WebDavUrl. + + One thing must be noted,
>> that a valid WebDavUrl is the one which + may contain zero or
>> more resources (files), or zero or more + collections
>> (directories). + + Thus, following are valid WebDavUrls :: + +
>> dav://1.2.3.4/webdav + dav://1.2.3.4/webdav/dir_1 +
>> dav://1.2.3.4/webdav/dir_1/dir_2 + + but following are not :: +
>> + dav://1.2.3.4/webdav/a.txt +
>> dav://1.2.3.4/webdav/dir_1/b.jpg +
>> dav://1.2.3.4/webdab/dir_1/dir_2/c.avi + """ + + def
>> __init__(self, WebDavUrl, username, password): +
>> self._WebDavUrl = WebDavUrl + self._username = username +
>> self._password = password + + # Since we are only
>> instantiating primitive types in + # "__init__", we may
>> safely call the class-function here. +
>> self._fetch_resources_and_collections() + + def
>> _get_key_from_resource(self, resource): + return
>> resource.path.encode(sys.getfilesystemencoding()) + + def
>> _get_number_of_collections(self): + return
>> len(self._remote_webdav_share_collections) + + def
>> _get_resources_dict(self): + return
>> self._remote_webdav_share_resources + + def
>> _get_collections_dict(self): + return
>> self._remote_webdav_share_collections + + def
>> _get_resource_by_key(self, key): + return
>> self._remote_webdav_share_resources[key]['resource'] + + def
>> _get_metadata_list(self): + metadata_list = [] + for
>> key in self._remote_webdav_share_resources.keys(): +
>> metadata_list.append(self._remote_webdav_share_resources[key]['metadata'])
>>
>>
> + return metadata_list
>> + + def _get_live_properties(self, resource_key): +
>> resource_container =
>> self._remote_webdav_share_resources[resource_key] + return
>> resource_container['webdav-properties'] + + def
>> _set_metadata_for_resource(self, key, metadata): +
>> self._remote_webdav_share_resources[key]['metadata'] = metadata + +
>> def _fetch_resources_and_collections(self): +
>> webdavConnection = CollectionStorer(self._WebDavUrl,
>> validateResourceNames=False) + + authFailures = 0 +
>> while authFailures < 2: + try: +
>> self._remote_webdav_share_resources = {} +
>> self._remote_webdav_share_collections = {} + + for
>> resource, properties in webdavConnection.getCollectionContents(): +
>> try: + key =
>> self._get_key_from_resource(resource) +
>> selected_dict = None + + if
>> properties.getResourceType() == 'resource': +
>> selected_dict = self._remote_webdav_share_resources +
>> else: + selected_dict =
>> self._remote_webdav_share_collections + +
>> selected_dict[key] = {} +
>> selected_dict[key]['resource'] = resource +
>> selected_dict[key]['webdav-properties'] = properties +
>> except UnicodeEncodeError: + print("Cannot
>> encode resource path or properties.") + break #
>> break out of the authorization failure counter + except
>> AuthorizationError, e: + if self._username is None
>> or self._password is None: + raise
>> Exception("WebDav username or password is None. Please specify
>> appropriate values.") + + if e.authType == "Basic":
>> +
>> webdavConnection.connection.addBasicAuthorization(self._username,
>> self._password) + elif e.authType == "Digest": +
>> info = parseDigestAuthInfo(e.authInfo) +
>> webdavConnection.connection.addDigestAuthorization(self._username,
>> self._password, realm=info["realm"], qop=info["qop"],
>> nonce=info["nonce"]) + else: +
>> raise + authFailures += 1 + +webdav_manager = {} +
>> + +def get_resource_by_ip_address_and_resource_key(ip_address,
>> key): + global webdav_manager + + if ip_address in
>> webdav_manager.keys(): + root_webdav =
>> webdav_manager[ip_address] + resources_dict =
>> root_webdav._get_resources_dict() + resource_dict =
>> resources_dict[key] + resource = resource_dict['resource']
>> + + return resource + + +def
>> get_remote_webdav_share_metadata(ip_address): + protocol =
>> 'dav://' + + root_webdav_url = '/webdav' + +
>> complete_root_url = protocol + ip_address + root_webdav_url + +
>> root_webdav = WebDavUrlManager(complete_root_url, 'test', 'olpc')
>> + + # Keep reference to the "WebDavUrlManager", keyed by
>> IP-Address. + global webdav_manager +
>> webdav_manager[ip_address] = root_webdav + + + # Assert that the
>> number of collections is only one at this url + # (i.e. only
>> ".Sugar-Metadata" is present). + assert
>> root_webdav._get_number_of_collections() == 1 + +
>> root_sugar_metadata_url = root_webdav_url + '/.Sugar-Metadata' + +
>> complete_root_sugar_metadata_url = protocol + ip_address +
>> root_sugar_metadata_url + root_webdav_sugar_metadata =
>> WebDavUrlManager(complete_root_sugar_metadata_url, 'test', 'olpc')
>> + + # assert that the number of collections is zero at this
>> url. + assert
>> root_webdav_sugar_metadata._get_number_of_collections() == 0 + +
>> # Now. associate sugar-metadata with each of the "root-webdav" +
>> # resource. + root_webdav_resources =
>> root_webdav._get_resources_dict() +
>> root_webdav_sugar_metadata_resources =
>> root_webdav_sugar_metadata._get_resources_dict() + + # Prepare
>> the metadata-download folder. + downloaded_data_root_dir =
>> '/tmp/' + ip_address + downloaded_metadata_file_dir =
>> downloaded_data_root_dir + '/.Sugar-Metadata' + if
>> os.path.isdir(downloaded_data_root_dir): +
>> shutil.rmtree(downloaded_data_root_dir) +
>> os.makedirs(downloaded_metadata_file_dir) + + for
>> root_webdav_resource_name in root_webdav_resources.keys(): +
>> """ + root_webdav_resource_name is of the type :: + +
>> /webdav/a.txt + """ + split_tokens_array =
>> root_webdav_resource_name.split('/') + + # This will provide
>> us with "a.txt" + basename =
>> split_tokens_array[len(split_tokens_array) - 1] + + # This
>> will provide us with "a.txt.metadata" +
>> sugar_metadata_basename = basename + '.metadata' + + # Thus
>> will provide us with "/webdav/.Sugar-Metadata/a.txt.metadata" +
>> sugar_metadata_url = root_sugar_metadata_url + '/' +
>> sugar_metadata_basename + + # Ensure that
>> "sugar_metadata_url" is present as one of the + # keys in
>> "root_webdav_sugar_metadata_resources" + assert
>> sugar_metadata_url in root_webdav_sugar_metadata_resources.keys()
>> + + # Now download the metadata file, read its contents, and
>> store + # the metadata in memory. + # It is assumed
>> that the metadata-file is small enough to be + # read in one
>> call to "read". + + downloaded_metadata_file_path =
>> downloaded_metadata_file_dir + '/' + sugar_metadata_basename +
>> metadata_resource =
>> root_webdav_sugar_metadata._get_resource_by_key(sugar_metadata_url)
>>
>>
> + metadata_resource.downloadFile(downloaded_metadata_file_path)
>> + + file_pointer = open(downloaded_metadata_file_path) +
>> metadata = eval(file_pointer.read()) + file_pointer.close()
>> + + # Very critical. + # 1. CRITICAL ONE: + #
>> Fill in the uid. + # Note that the file is not physically
>> present. + metadata['uid'] = downloaded_data_root_dir + '/'
>> + basename + + # 2. CRITICAL TWO: + # Fill in the
>> properties, that can only be done by reading + # in the
>> webdav-properties. + live_properties =
>> root_webdav._get_live_properties(root_webdav_resource_name) +
>> metadata['filesize'] = live_properties.getContentLength() +
>> metadata['timestamp'] = live_properties.getLastModified() +
>> metadata['creation_time'] = live_properties.getCreationDate() + +
>> # Now, write this to the metadata-file, so that + #
>> webdav-properties get gelled into sugar-metadata. + +
>> file_pointer = open(downloaded_metadata_file_path, 'w') +
>> file_pointer.write(simplejson.dumps(metadata)) +
>> file_pointer.close() + +
>> root_webdav._set_metadata_for_resource(root_webdav_resource_name, +
>> metadata) + + return root_webdav._get_metadata_list() diff --git
>> a/src/webdav/WebdavRequests.py b/src/webdav/WebdavRequests.py new
>> file mode 100644 index 0000000..79e586a --- /dev/null +++
>> b/src/webdav/WebdavRequests.py @@ -0,0 +1,205 @@ +# pylint:
>> disable-msg=W0511,W0212,E1111 +# +# Copyright 2008 German Aerospace
>> Center (DLR) +# +# Licensed under the Apache License, Version 2.0
>> (the "License"); +# you may not use this file except in compliance
>> with the License. +# You may obtain a copy of the License at +# +#
>> http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by
>> applicable law or agreed to in writing, software +# distributed
>> under the License is distributed on an "AS IS" BASIS, +# WITHOUT
>> WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#
>> See the License for the specific language governing permissions
>> and +# limitations under the License. + + +""" +This module handles
>> WebDav server requests. +""" + + +import types +from webdav import
>> Constants +import qp_xml +from tempfile import TemporaryFile +
>> +from davlib import XML_DOC_HEADER + +from webdav.NameCheck import
>> validatePropertyName + + +__version__ = "$LastChangedRevision$" +
>> + +## TODO: create a property list class + +class
>> XmlNameSpaceMangler(object): + ''' + Handles WebDav
>> requests. + ''' + + # restrict instance variables +
>> __slots__ = ('shortcuts', 'defaultNameSpace') + + def
>> __init__(self, nameList, defaultNameSpace = None): + ''' +
>> + @param nameList: + @param defaultNameSpace: +
>> ''' + + assert isinstance(nameList, types.ListType) or
>> isinstance(nameList, types.TupleType), \ + "1. argument
>> has wrong type %s" % type(nameList) + self.shortcuts = {} +
>> self.defaultNameSpace = defaultNameSpace + for name in
>> nameList: + if not isinstance(name, types.TupleType): +
>> name = (defaultNameSpace, name) + assert
>> isinstance(name, types.TupleType) and len(name) == 2, \ +
>> "Name is not a namespace, name tuple: %s" % type(name) +
>> validatePropertyName(name[1]) + if name[0] and not
>> self.shortcuts.has_key(name[0]): +
>> self.shortcuts[name[0]] = 'ns%d' % len(self.shortcuts) + + def
>> getNameSpaces(self): + ''' + Returns the namespace. +
>> ''' + + result = "" + for namespace, short in
>> self.shortcuts.items(): + result += ' xmlns:%s="%s"' %
>> (short, namespace) + return result + + def
>> getUpdateElements(self, valueMap): + ''' + + @param
>> valueMap: + ''' + + elements = "" + for name
>> in valueMap.keys(): + fullname = name + if
>> isinstance(name, types.StringType): + fullname =
>> (self.defaultNameSpace, name) + if not fullname[0]: +
>> tag = fullname[1] + else: + tag =
>> self.shortcuts[fullname[0]] + ':' + fullname[1] + value
>> = valueMap[name] + if value: + if
>> isinstance(value, qp_xml._element): + tmpFile =
>> TemporaryFile('w+') + value =
>> qp_xml.dump(tmpFile, value) + tmpFile.flush() +
>> tmpFile.seek(0) + tmpFile.readline() +
>> value = tmpFile.read() + else: +
>> value = "<![CDATA[%s]]>" % value + else: +
>> value = "" + elements += "<%s>%s</%s>" % (tag, value,
>> tag) + return elements + + def getNameElements(self,
>> nameList): + ''' + + @param nameList: + ''' +
>> + elements = "" + for name in nameList: +
>> if isinstance(name, types.StringType): + name =
>> (self.defaultNameSpace, name) + if not name[0]: +
>> tag = name[1] + else: + tag =
>> self.shortcuts[name[0]] + ':' + name[1] + elements +=
>> "<%s />" % tag + return elements + + + +def
>> createUpdateBody(propertyDict, defaultNameSpace = None): + ''' +
>> + @param propertyDict: + @param defaultNameSpace: + ''' +
>> + updateTag = 'D:' + Constants.TAG_PROPERTY_UPDATE + setTag
>> = 'D:' + Constants.TAG_PROPERTY_SET + propTag = 'D:' +
>> Constants.TAG_PROP + mangler =
>> XmlNameSpaceMangler(propertyDict.keys(), defaultNameSpace) +
>> return XML_DOC_HEADER + \ + '<%s xmlns:D="DAV:"><%s><%s %s>'
>> % (updateTag, setTag, propTag, mangler.getNameSpaces()) + \ +
>> mangler.getUpdateElements(propertyDict) + \ +
>> '</%s></%s></%s>' % (propTag, setTag, updateTag) + + +def
>> createDeleteBody(nameList, defaultNameSpace = None): + ''' +
>> + @param nameList: + @param defaultNameSpace: + ''' +
>> + updateTag = 'D:' + Constants.TAG_PROPERTY_UPDATE +
>> removeTag = 'D:' + Constants.TAG_PROPERTY_REMOVE + propTag =
>> 'D:' + Constants.TAG_PROP + mangler =
>> XmlNameSpaceMangler(nameList, defaultNameSpace) + return
>> XML_DOC_HEADER + \ + '<%s xmlns:D="DAV:"><%s><%s %s>' %
>> (updateTag, removeTag, propTag, mangler.getNameSpaces()) + \ +
>> mangler.getNameElements(nameList) + \ + '</%s></%s></%s>' %
>> (propTag, removeTag, updateTag) + + +def createFindBody(nameList,
>> defaultNameSpace = None): + ''' + + @param nameList: +
>> @param defaultNameSpace: + ''' + + findTag = 'D:' +
>> Constants.TAG_PROPERTY_FIND + propTag = 'D:' +
>> Constants.TAG_PROP + mangler = XmlNameSpaceMangler(nameList,
>> defaultNameSpace) + return XML_DOC_HEADER + \ + '<%s
>> xmlns:D="DAV:"><%s %s>' % (findTag, propTag,
>> mangler.getNameSpaces()) + \ +
>> mangler.getNameElements(nameList) + \ + '</%s></%s>' %
>> (propTag, findTag) + + +def createSearchBody(selects, path,
>> conditions, defaultNameSpace = None): + ''' + Creates DASL
>> XML body. + + @param selects: list of property names to retrieve
>> for the found resources + @param path: list of conditions +
>> @param conditions: tree of ConditionTerm instances representing a
>> logical search term + @param defaultNameSpace: default
>> namespace + ''' + + searchTag = 'D:' +
>> Constants.TAG_SEARCH_REQUEST + basicTag = 'D:' +
>> Constants.TAG_SEARCH_BASIC + selectTag = 'D:' +
>> Constants.TAG_SEARCH_SELECT + fromTag = 'D:' +
>> Constants.TAG_SEARCH_FROM + scopeTag = 'D:' +
>> Constants.TAG_SEARCH_SCOPE + whereTag = 'D:' +
>> Constants.TAG_SEARCH_WHERE + propTag = 'D:' +
>> Constants.TAG_PROP + hrefTag = 'D:' + Constants.TAG_HREF +
>> depthTag = 'D:' + Constants.TAG_LOCK_DEPTH + depthValue =
>> Constants.HTTP_HEADER_DEPTH_INFINITY + mangler =
>> XmlNameSpaceMangler(selects, defaultNameSpace) + return
>> XML_DOC_HEADER + \ + '<%s xmlns:D="DAV:"><%s>' % (searchTag,
>> basicTag) + \ + '<%s><%s %s>%s</%s></%s>' % (selectTag,
>> propTag, mangler.getNameSpaces(), +
>> mangler.getNameElements(selects), propTag, selectTag) + \ +
>> '<%s><%s><%s>%s</%s><%s>%s</%s></%s></%s>' % (fromTag, scopeTag,
>> hrefTag, path, hrefTag, +
>> depthTag, depthValue, depthTag, scopeTag, fromTag) + \ +
>> '<%s>%s</%s>' % (whereTag, conditions.toXML(),whereTag) + \ +
>> '</%s></%s>' % (basicTag, searchTag) + \ No newline at end of file
>> diff --git a/src/webdav/WebdavResponse.py
>> b/src/webdav/WebdavResponse.py new file mode 100644 index
>> 0000000..c84943d --- /dev/null +++ b/src/webdav/WebdavResponse.py
>> @@ -0,0 +1,525 @@ +# pylint:
>> disable-msg=R0903,W0142,W0221,W0212,W0104,W0511,C0103,R0901 +# +#
>> Copyright 2008 German Aerospace Center (DLR) +# +# Licensed under
>> the Apache License, Version 2.0 (the "License"); +# you may not use
>> this file except in compliance with the License. +# You may obtain
>> a copy of the License at +# +#
>> http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by
>> applicable law or agreed to in writing, software +# distributed
>> under the License is distributed on an "AS IS" BASIS, +# WITHOUT
>> WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#
>> See the License for the specific language governing permissions
>> and +# limitations under the License. + + +""" +Handles WebDAV
>> responses. +""" + + +from davlib import _parse_status +import
>> qp_xml +from webdav import Constants +import time +import rfc822
>> +import urllib +# Handling Jython 2.5 bug concerning the date
>> pattern +# conversion in time.strptime +try: + from java.lang
>> import IllegalArgumentException +except ImportError: + class
>> IllegalArgumentException(object): + pass + + +__version__ =
>> "$LastChangedRevision$" + + +class HttpStatus(object): + """ +
>> TBD + + @ivar code: + @type code: + @ivar reason: +
>> @type reason: + @ivar errorCount: + @type errorCount: int +
>> """ + + def __init__(self, elem): + """ + TBD +
>> + @param elem: ... + @type elem: instance of
>> L{Element} + """ + self.code, self.reason =
>> _parse_status(elem) + self.errorCount = (self.code >=
>> Constants.CODE_LOWEST_ERROR) + def __str__(self): +
>> return "HTTP status %d: %s" % (self.code, self.reason) + + +class
>> MultiStatusResponse(dict): + """ + TBD + + @ivar status: +
>> @type status: + @ivar reason: + @type reason: + @ivar
>> errorCount: + @type errorCount: + """ + + # restrict
>> instance variables + __slots__ = ('errorCount', 'reason',
>> 'status') + + def __init__(self, domroot): +
>> dict.__init__(self) + self.errorCount = 0 +
>> self.reason = None + self.status =
>> Constants.CODE_MULTISTATUS + if (domroot.ns !=
>> Constants.NS_DAV) or (domroot.name != Constants.TAG_MULTISTATUS): +
>> raise ResponseFormatError(domroot, 'Invalid response:
>> <DAV:multistatus> expected.') + self._scan(domroot) + +
>> def getCode(self): + if self.errorCount == 0: +
>> return Constants.CODE_SUCCEEDED + if len(self) >
>> self.errorCount: + return Constants.CODE_MULTISTATUS +
>> return self.values()[0].code + + def getReason(self): +
>> result = "" + for response in self.values(): + if
>> response.code > Constants.CODE_LOWEST_ERROR: +
>> result += response.reason + return result
>> + + def __str__(self): + result = "" + for key,
>> value in self.items(): + if isinstance(value,
>> PropertyResponse): + result += "Resource at %s has
>> %d properties and %d errors.\n" % (key, len(value),
>> value.errorCount) + else: + result +=
>> "Resource at %s returned " % key + str(value) + return
>> result + + def _scan(self, root): + for child in
>> root.children: + if child.ns != Constants.NS_DAV: +
>> continue + if child.name ==
>> Constants.TAG_RESPONSEDESCRIPTION: + self.reason =
>> child.textof() + elif child.name ==
>> Constants.TAG_RESPONSE: + self._scanResponse(child)
>> + ### unknown child element + + def
>> _scanResponse(self, elem): + hrefs = [] + response =
>> None + for child in elem.children: + if child.ns
>> != Constants.NS_DAV: + continue + if
>> child.name == Constants.TAG_HREF: + try: +
>> href = _unquoteHref(child.textof()) + except
>> UnicodeDecodeError: + raise
>> ResponseFormatError(child, "Invalid 'href' data encoding.") +
>> hrefs.append(href) + elif child.name ==
>> Constants.TAG_STATUS: + self._scanStatus(child,
>> *hrefs) + elif child.name ==
>> Constants.TAG_PROPERTY_STATUS: + if not response: +
>> if len(hrefs) != 1: + raise
>> ResponseFormatError(child, 'Invalid response: One <DAV:href>
>> expected.') + response = PropertyResponse()
>> + self[hrefs[0]] = response
>> + response._scan(child) + elif
>> child.name == Constants.TAG_RESPONSEDESCRIPTION: +
>> for href in hrefs: +
>> self[href].reasons.append(child.textOf()) + ### unknown
>> child element + if response and response.errorCount > 0: +
>> self.errorCount += 1 + + def _scanStatus(self, elem, *hrefs): +
>> if len(hrefs) == 0: + raise ResponseFormatError(elem,
>> 'Invalid response: <DAV:href> expected.') + status =
>> HttpStatus(elem) + for href in hrefs: +
>> self[href] = status + if status.errorCount: +
>> self.errorCount += 1 + + # Instance properties + code =
>> property(getCode, None, None, "HTTP response code") + + + +class
>> PropertyResponse(dict): + """ + TBD + + @ivar errors: +
>> @type errors: list of ... + @ivar reasons: + @type reasons:
>> list of ... + @ivar failedProperties: + @type
>> failedProperties: dict of ... + """ + + # restrict instance
>> variables + __slots__ = ('errors', 'reasons',
>> 'failedProperties') + + def __init__(self): +
>> dict.__init__(self) + self.errors = [] + self.reasons
>> = [] + self.failedProperties = {} + + def __str__(self):
>> + result = "" + for value in self.values(): +
>> result += value.name + '= ' + value.textof() + '\n' + result
>> += self.getReason() + return result + + def
>> getCode(self): + if len(self.errors) == 0: +
>> return Constants.CODE_SUCCEEDED + if len(self) > 0: +
>> return Constants.CODE_MULTISTATUS + return
>> self.errors[-1].code + + def getReason(self): + result =
>> "" + if len(self.errors) > 0: + result = "Failed
>> for: " + repr(self.failedProperties.keys()) + "\n" + for
>> error in self.errors: + result += "%s (%d). " %
>> (error.reason, error.code) + for reason in self.reasons: +
>> result += "%s. " % reason + return result + + def
>> _scan(self, element): + status = None + statusElement
>> = element.find(Constants.TAG_STATUS, Constants.NS_DAV) + if
>> statusElement: + status = HttpStatus(statusElement) +
>> if status.errorCount: + self.errors.append(status) +
>> + propElement = element.find(Constants.TAG_PROP,
>> Constants.NS_DAV) + if propElement: + for prop in
>> propElement.children: + if status.errorCount: +
>> self.failedProperties[(prop.ns, prop.name)]= status +
>> else: + prop.__class__ = Element # bad, bad
>> trick + self[prop.fullname] = prop +
>> reasonElement = element.find(Constants.TAG_RESPONSEDESCRIPTION,
>> Constants.NS_DAV) + if reasonElement: +
>> self.reasons.append(reasonElement.textOf()) + + # Instance
>> properties + code = property(getCode, None, None, "HTTP response
>> code") + errorCount = property(lambda self: len(self.errors),
>> None, None, "HTTP response code") + reason = property(getReason,
>> None, None, "HTTP response code") + + + + +class
>> LiveProperties(object): + """ + This class provides
>> convenient access to the WebDAV 'live' properties of a resource. +
>> WebDav 'live' properties are defined in RFC 2518, Section 13. +
>> Each property is converted from string to its natural data type. +
>> + @version: $Revision$ + @author: Roland Betz + """ +
>> + # restrict instance variables + __slots__ =
>> ('properties') + + NAMES = (Constants.PROP_CREATION_DATE,
>> Constants.PROP_DISPLAY_NAME, +
>> Constants.PROP_CONTENT_LENGTH, Constants.PROP_CONTENT_TYPE,
>> Constants.PROP_ETAG, + Constants.PROP_LAST_MODIFIED,
>> Constants.PROP_OWNER, + Constants.PROP_LOCK_DISCOVERY,
>> Constants.PROP_RESOURCE_TYPE, Constants.PROP_SUPPORTED_LOCK ) +
>> + def __init__(self, properties=None, propElement=None): +
>> """ + Construct <code>StandardProperties</code> from a map
>> of properties containing + live properties or from a XML
>> 'prop' element containing live properties + + @param
>> properties: map as implemented by class L{PropertyResponse} +
>> @param propElement: an C{Element} value + """ +
>> assert isinstance(properties, PropertyResponse) or \ +
>> isinstance(propElement, qp_xml._element), \ +
>> "Argument properties has type %s" % str(type(properties)) +
>> self.properties = {} + for name, value in
>> properties.items(): + if name[0] == Constants.NS_DAV
>> and name[1] in self.NAMES: +
>> self.properties[name[1]] = value + + def
>> getContentLanguage(self): + """ + Return the language
>> of a resource's textual content or null + + @return: string
>> + """ + + result = "" + if not
>> self.properties.get(Constants.PROP_CONTENT_LANGUAGE, None) is
>> None: + result =
>> self.properties.get(Constants.PROP_CONTENT_LANGUAGE).textof() +
>> return result + + def getContentLength(self): + """ +
>> Returns the length of the resource's content in bytes. + +
>> @return: number of bytes + """ + + result = 0 +
>> if not self.properties.get(Constants.PROP_CONTENT_LENGTH, None) is
>> None: + result =
>> int(self.properties.get(Constants.PROP_CONTENT_LENGTH).textof()) +
>> return result + + def getContentType(self): + """ +
>> Return the resource's content MIME type. + + @return: MIME
>> type string + """ + + result = "" + if not
>> self.properties.get(Constants.PROP_CONTENT_TYPE, None) is None: +
>> result = self.properties.get(Constants.PROP_CONTENT_TYPE).textof()
>> + return result + + def getCreationDate(self): +
>> """ + Return date of creation as time tuple. +
>> + @return: time tuple + @rtype: C{time.struct_time}
>> + + @raise ValueError: If string is not in the expected
>> format (ISO 8601). + """ + + datetimeString = "" +
>> if not self.properties.get(Constants.PROP_CREATION_DATE, None) is
>> None: + datetimeString =
>> self.properties.get(Constants.PROP_CREATION_DATE).textof() + +
>> result = rfc822.parsedate(datetimeString) + if result is
>> None: + result = _parseIso8601String(datetimeString) + +
>> return time.mktime(result) + + def getEntityTag(self): +
>> """ + Return a entity tag which is unique for a particular
>> version of a resource. + Different resources or one resource
>> before and after modification have different etags. + +
>> @return: entity tag string + """ + + result = "" +
>> if not self.properties.get(Constants.PROP_ETAG, None) is None: +
>> result = self.properties.get(Constants.PROP_ETAG).textof() +
>> return result + + def getDisplayName(self): + """ +
>> Returns a resource's display name. + + @return: string +
>> """ + + result = "" + if not
>> self.properties.get(Constants.PROP_DISPLAY_NAME, None) is None: +
>> result = self.properties.get(Constants.PROP_DISPLAY_NAME).textof()
>> + return result + + def getLastModified(self): +
>> """ + Return last modification of resource as time tuple. +
>> + @return: Modification date time. + @rtype:
>> C{time.struct_time} + + @raise ValueError: If the date time
>> string is not in the expected format (RFC 822 / ISO 8601). +
>> """ + + datetimeString = None + if not
>> self.properties.get(Constants.PROP_LAST_MODIFIED, None) is None: +
>> datetimeString =
>> self.properties.get(Constants.PROP_LAST_MODIFIED).textof() +
>> result = rfc822.parsedate(datetimeString) + if result is
>> None: + result = _parseIso8601String(datetimeString) +
>> return time.mktime(result) + + def getLockDiscovery(self): +
>> """ + Return all current lock's applied to a resource or
>> null if it is not locked. + + @return: a lockdiscovery DOM
>> element according to RFC 2815 + """ + + xml =
>> self.properties.get(Constants.PROP_LOCK_DISCOVERY) + return
>> _scanLockDiscovery(xml) + + def getResourceType(self): +
>> """ + Return a resource's WebDAV type. + + @return:
>> 'collection' or 'resource' + """ + + xml =
>> self.properties.get(Constants.PROP_RESOURCE_TYPE) + if xml
>> and xml.children: + return xml.children[0].name +
>> return "resource" + + def getSupportedLock(self): + """ +
>> Return a DOM element describing all supported lock options for a
>> resource. + Usually this is shared and exclusive write
>> lock. + + @return: supportedlock DOM element according to
>> RFC 2815 + """ + + xml =
>> self.properties.get(Constants.PROP_SUPPORTED_LOCK) + return
>> xml + + def getOwnerAsUrl(self): + """ + Return a
>> resource's owner in form of a URL. + + @return: string +
>> """ + + xml = self.properties.get(Constants.PROP_OWNER) +
>> if xml and len(xml.children): + return
>> xml.children[0].textof() + return None + + def
>> __str__(self): + result = "" + result += " Name=" +
>> self.getDisplayName() + result += "\n Type=" +
>> self.getResourceType() + result += "\n Length=" +
>> str(self.getContentLength()) + result += "\n Content Type="+
>> self.getContentType() + result += "\n ETag=" +
>> self.getEntityTag() + result += "\n Created=" +
>> time.strftime("%c GMT", self.getCreationDate()) + result +=
>> "\n Modified=" + time.strftime("%c GMT", self.getLastModified()) +
>> return result + + +def _parseIso8601String(date): + """ +
>> Parses the given ISO 8601 string and returns a time tuple. + The
>> strings should be formatted according to RFC 3339 (see section
>> 5.6). + But currently there are two exceptions: + 1. Time
>> offset is limited to "Z". + 2. Fragments of seconds are
>> ignored. + """ + + if "." in date and "Z" in date: # Contains
>> fragments of second? + secondFragmentPos = date.rfind(".") +
>> timeOffsetPos = date.rfind("Z") + date =
>> date[:secondFragmentPos] + date[timeOffsetPos:] + try: +
>> timeTuple = time.strptime(date, Constants.DATE_FORMAT_ISO8601) +
>> except IllegalArgumentException: # Handling Jython 2.5 bug
>> concerning the date pattern accordingly + import _strptime #
>> Using the Jython fall back solution directly + timeTuple =
>> _strptime.strptime(date, Constants.DATE_FORMAT_ISO8601) + return
>> timeTuple + + +class ResponseFormatError(IOError): + """ + An
>> instance of this class is raised when the web server returned a
>> webdav + reply which does not adhere to the standard and cannot
>> be recognized. + """ + def __init__(self, element, message=
>> None): + IOError.__init__(self, "ResponseFormatError at
>> element %s: %s" % (element.name, message)) + self.element =
>> element + self.message = message + + +class
>> Element(qp_xml._element): + """ + This class improves the DOM
>> interface (i.e. element interface) provided by the qp_xml module +
>> TODO: substitute qp_xml by 'real' implementation. e.g. domlette +
>> """ + def __init__(self, namespace, name, cdata=''): +
>> qp_xml._element.__init__(self, ns=namespace, name=name, lang=None,
>> parent=None, + children=[], ns_scope={},
>> attrs={}, + first_cdata=cdata,
>> following_cdata='') + + def __str__(self): + return
>> self.textof() + + def __getattr__(self, name): + if
>> (name == 'fullname'): + return (self.__dict__['ns'],
>> self.__dict__['name']) + raise AttributeError, name + +
>> def add(self, child): + self.children.append(child) +
>> return child + +def _scanLockDiscovery(root): + assert root.name
>> == Constants.PROP_LOCK_DISCOVERY, "Invalid lock discovery XML
>> element" + active = root.find(Constants.TAG_ACTIVE_LOCK,
>> Constants.NS_DAV) + if active: + return
>> _scanActivelock(active) + return None + +def
>> _scanActivelock(root): + assert root.name ==
>> Constants.TAG_ACTIVE_LOCK, "Invalid active lock XML element" +
>> token = _scanOrError(root, Constants.TAG_LOCK_TOKEN) + value =
>> _scanOrError(token, Constants.TAG_HREF) + owner =
>> _scanOwner(root) + depth = _scanOrError(root,
>> Constants.TAG_LOCK_DEPTH) + return (value.textof(), owner,
>> depth.textof()) + +def _scanOwner(root): + owner =
>> root.find(Constants.TAG_LOCK_OWNER, Constants.NS_DAV) + if
>> owner: + href = owner.find(Constants.TAG_HREF,
>> Constants.NS_DAV) + if href: + return
>> href.textof() + return owner.textof() + return None +
>> +def _scanOrError(elem, childName): + child =
>> elem.find(childName, Constants.NS_DAV) + if not child: +
>> raise ResponseFormatError(elem, "Invalid response: <"+childName+">
>> expected") + return child + + +def _unquoteHref(href): +
>> #print "*** Response HREF=", repr(href) + if type(href) ==
>> type(u""): + try: + href = href.encode('ascii') +
>> except UnicodeError: # URL contains unescaped non-ascii
>> character + # handle bug in Tamino webdav server +
>> return urllib.unquote(href) + href = urllib.unquote(href) +
>> if Constants.CONFIG_UNICODE_URL: + return unicode(href,
>> 'utf-8') + else: + return unicode(href, 'latin-1') diff
>> --git a/src/webdav/__init__.py b/src/webdav/__init__.py new file
>> mode 100644 index 0000000..3e46609 --- /dev/null +++
>> b/src/webdav/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2008 German
>> Aerospace Center (DLR) +# +# Licensed under the Apache License,
>> Version 2.0 (the "License"); +# you may not use this file except in
>> compliance with the License. +# You may obtain a copy of the
>> License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +#
>> +# Unless required by applicable law or agreed to in writing,
>> software +# distributed under the License is distributed on an "AS
>> IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
>> express or implied. +# See the License for the specific language
>> governing permissions and +# limitations under the License. + +
>> +__version__ = "$LastChangedRevision$" diff --git
>> a/src/webdav/acp/Ace.py b/src/webdav/acp/Ace.py new file mode
>> 100644 index 0000000..8321d41 --- /dev/null +++
>> b/src/webdav/acp/Ace.py @@ -0,0 +1,293 @@ +# Copyright 2008 German
>> Aerospace Center (DLR) +# +# Licensed under the Apache License,
>> Version 2.0 (the "License"); +# you may not use this file except in
>> compliance with the License. +# You may obtain a copy of the
>> License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +#
>> +# Unless required by applicable law or agreed to in writing,
>> software +# distributed under the License is distributed on an "AS
>> IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
>> express or implied. +# See the License for the specific language
>> governing permissions and +# limitations under the License. + +
>> +""" +ACE object handling according to WebDAV ACP specification.
>> +""" + + +from webdav.acp.Principal import Principal +from
>> webdav.acp.GrantDeny import GrantDeny +from webdav import
>> Constants +from webdav.Connection import WebdavError + +
>> +__version__ = "$LastChangedRevision$" + + +class ACE(object): +
>> """ + This class provides functionality for handling ACEs + +
>> @ivar principal: A principal (user or group) + @type
>> principal: L{Principal} object + @ivar invert: Flag
>> indicating whether ACE should invert the principal. + @type
>> invert: C{bool} + @ivar grantDenies: Grant or deny clauses
>> for privileges + @type grantDenies: C{list} of L{GrantDeny}
>> objects + @ivar protected: Flag indicating whether ACE is
>> protected. + @type protected: C{bool} + @ivar inherited:
>> URL indicating the source from where the ACE is inherited. +
>> @type inherited: C{string} + """ + + # restrict instance
>> variables + __slots__ = ('principal', 'invert', 'grantDenies',
>> 'protected', 'inherited') + + def __init__(self, domroot=None,
>> principal=None, grantDenies=None): + """ +
>> Constructor should be called with either no parameters (create
>> blank ACE), + one parameter (a DOM tree or principal), or
>> two parameters (principal and + sequence of GrantDenies). +
>> + @param domroot: A DOM tree (default: None). +
>> @type domroot: L{webdav.WebdavResponse.Element} object +
>> @param principal: A principal (user or group), (default: None). +
>> @type principal: L{Principal} object + @param
>> grantDenies: Grant and deny clauses for privileges (default:
>> None). + @type grantDenies: sequence of L{GrantDeny}
>> objects + + @raise WebdavError: When non-valid parameters
>> are passed a L{WebdavError} is raised. + """ +
>> self.principal = Principal() + self.protected = None +
>> self.inherited = None + self.invert = None +
>> self.grantDenies = [] + + if domroot: +
>> self.principal =
>> Principal(domroot=domroot.find(Constants.TAG_PRINCIPAL,
>> Constants.NS_DAV)) + self.inherited =
>> domroot.find(Constants.TAG_INHERITED, Constants.NS_DAV) +
>> if self.inherited: + self.inherited =
>> self.inherited.children[0].textof() + if
>> domroot.find(Constants.TAG_PROTECTED, Constants.NS_DAV): +
>> self.protected = 1 + for child in domroot.children: +
>> if child.ns == Constants.NS_DAV \ + and
>> (child.name == Constants.TAG_GRANT or child.name ==
>> Constants.TAG_DENY): +
>> self.grantDenies.append(GrantDeny(domroot=child)) + elif
>> isinstance(principal, Principal): + newPrincipal =
>> Principal() + newPrincipal.copy(principal) +
>> self.principal = newPrincipal + if
>> (isinstance(grantDenies, list) or isinstance(grantDenies, tuple)):
>> + self.addGrantDenies(grantDenies) + elif
>> domroot == None and grantDenies == None: + # no param
>> ==> blank ACE + pass + else: + # This
>> shouldn't happen, someone screwed up with the params ... +
>> raise WebdavError('non-valid parameters handed to ACE
>> constructor') + + def __cmp__(self, other): + if not
>> isinstance(other, ACE): + return 1 + if
>> self.principal == other.principal \ + and
>> self.invert == other.invert \ + and self.protected
>> == other.protected \ + and self.inherited ==
>> other.inherited: + equal = 1 + for grantDeny
>> in self.grantDenies: + inList = 0 +
>> for otherGrantDeny in other.grantDenies: + if
>> grantDeny == otherGrantDeny: + inList = 1 +
>> if inList == 0: + equal = 0 + return
>> not equal + else: + return 1 + + def
>> __repr__(self): + repr = '<class ACE: ' + if
>> self.invert: + repr += 'inverted principal, ' %
>> (self.invert) + if self.principal: + repr +=
>> 'principal: %s, ' % (self.principal) + if self.protected: +
>> repr += 'protected, ' + if self.inherited: + repr
>> += 'inherited href: %s, ' % (self.inherited) + first = 1 +
>> repr += 'grantDenies: [' + for grantDeny in
>> self.grantDenies: + if first: + repr +=
>> '%s' % grantDeny + first = 0 + else: +
>> repr += ', %s' % grantDeny + return '%s]>' % (repr) + +
>> def copy(self, other): + '''Copy an ACE object. + +
>> @param other: Another ACE to copy. + @type other: L{ACE}
>> object + + @raise WebdavError: When an object that is not an
>> L{ACE} is passed + a L{WebdavError} is raised. +
>> ''' + if not isinstance(other, ACE): + raise
>> WebdavError('Non-ACE object passed to copy method: %s.' %
>> other.__class__) + self.invert = other.invert +
>> self.protected = other.protected + self.inherited =
>> other.inherited + self.principal = Principal() + if
>> other.principal: + self.principal.copy(other.principal)
>> + if other.grantDenies: +
>> self.addGrantDenies(other.grantDenies) + + def isValid(self): +
>> """ + Returns true/false (1/0) whether necessarry props +
>> principal and grantDenies are set and whether the ACE contains one
>> + grant or deny clauses. + + @return: Validity of
>> ACE. + @rtype: C{bool} + """ + return
>> self.principal and len(self.grantDenies) == 1 + + def
>> isGrant(self): + ''' + Returns true/false (1/0) if
>> ACE contains only grant clauses. + + @return: Value whether
>> the ACE is of grant type. + @rtype: C{bool} + ''' +
>> if self.isMixed() or len(self.grantDenies) < 1: + return
>> 0 + else: + return self.grantDenies[0].isGrant()
>> + + def isDeny(self): + ''' + Returns true/false
>> (1/0) if ACE contains only deny clauses. + + @return: Value
>> whether the ACE is of deny type. + @rtype: C{bool} +
>> ''' + if self.isMixed() or len(self.grantDenies) < 1: +
>> return 0 + else: + return
>> self.grantDenies[0].isDeny() + + def isMixed(self): +
>> ''' + Returns true/false (1/0) if ACE contains both types
>> (grant and deny) of clauses. + + @return: Value whether the
>> ACE is of mixed (grant and deny) type. + @rtype: C{bool} +
>> ''' + mixed = 0 + if len(self.grantDenies): +
>> first = self.grantDenies[0].grantDeny + for grantDeny in
>> self.grantDenies: + if grantDeny.grantDeny !=
>> first: + mixed = 1 + return mixed + +
>> def toXML(self, defaultNameSpace=None): + """ +
>> Returns ACE content as a string of valid XML as described in WebDAV
>> ACP. + + @param defaultNameSpace: Name space (default:
>> None). + @type defaultNameSpace: C(string) + """ +
>> assert self.isValid(), "ACE is not initialized or does not contain
>> valid content!" + + ACE = 'D:' + Constants.TAG_ACE +
>> res = self.principal.toXML(self.invert) + for grantDeny in
>> self.grantDenies: + res += grantDeny.toXML() + if
>> self.protected: + res += '<D:protected/>' + if
>> self.inherited: + res +=
>> '<D:inherited><D:href>%s</D:href></D:inherited>' %
>> (self.inherited) + return '<%s>%s</%s>' % (ACE, res, ACE) +
>> + def setPrincipal(self, principal): + ''' + Sets
>> the passed principal on the ACE. + + @param principal: A
>> principal. + @type principal: L{Principal} object +
>> ''' + self.principal = Principal() +
>> self.principal.copy(principal) + + def setInherited(self,
>> href): + ''' + Sets the passed URL on the ACE to
>> denote from where it is inherited. + + @param href: A URL. +
>> @type href: C{string} + ''' + self.inherited = href
>> + + def addGrantDeny(self, grantDeny): + ''' +
>> Adds the passed GrantDeny object to list if it's not in it, yet. +
>> + @param grantDeny: A grant or deny clause. + @type
>> grantDeny: L{GrantDeny} object + ''' + # only add it
>> if it's not in the list, yet ... + inList = 0 + for
>> element in self.grantDenies: + if element == grantDeny:
>> + inList = 1 + if not inList: +
>> newGrantDeny = GrantDeny() +
>> newGrantDeny.copy(grantDeny) +
>> self.grantDenies.append(newGrantDeny) + + def
>> addGrantDenies(self, grantDenies): + '''Adds the list of
>> passed grant/deny objects to list. + + @param grantDenies:
>> Grant or deny clauses. + @type grantDenies: sequence of
>> L{GrantDeny} objects + ''' + map(lambda grantDeny:
>> self.addGrantDeny(grantDeny), grantDenies) + + def
>> delGrantDeny(self, grantDeny): + '''Deletes the passed
>> GrantDeny object from list. + + @param grantDeny: A grant or
>> deny clause. + @type grantDeny: L{GrantDeny} object +
>> + @raise WebdavError: A L{WebdavError} is raised if the
>> clause to be + deleted is not present. + ''' +
>> # only add it if it's not in the list, yet ... + count = 0 +
>> index = 0 + for element in self.grantDenies: +
>> count += 1 + if element == grantDeny: +
>> index = count + if index: +
>> self.grantDenies.pop(index - 1) + else: + raise
>> WebdavError('GrantDeny to be deleted not in list: %s.' %
>> grantDeny) + + def delGrantDenies(self, grantDenies): +
>> '''Deletes the list of passed grant/deny objects from list. +
>> + @param grantDenies: Grant or deny clauses. + @type
>> grantDenies: sequence of L{GrantDeny} objects + ''' +
>> map(lambda grantDeny: self.delGrantDeny(grantDeny), grantDenies)
>> diff --git a/src/webdav/acp/AceHandler.py
>> b/src/webdav/acp/AceHandler.py new file mode 100644 index
>> 0000000..e07b74d --- /dev/null +++ b/src/webdav/acp/AceHandler.py
>> @@ -0,0 +1,182 @@ +# Copyright 2008 German Aerospace Center (DLR)
>> +# +# Licensed under the Apache License, Version 2.0 (the
>> "License"); +# you may not use this file except in compliance with
>> the License. +# You may obtain a copy of the License at +# +#
>> http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by
>> applicable law or agreed to in writing, software +# distributed
>> under the License is distributed on an "AS IS" BASIS, +# WITHOUT
>> WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#
>> See the License for the specific language governing permissions
>> and +# limitations under the License. + + +""" +Handling of WebDAV
>> Access Protocol Extensions and ACL preparation for UI. +""" + +
>> +from webdav import Constants +from webdav.WebdavClient import
>> ResourceStorer +from webdav.Connection import WebdavError + +
>> +__version__ = "$LastChangedRevision$" + + +def
>> extractSupportedPrivilegeSet(userPrivileges): + """ + Returns
>> a dictionary of supported privileges. + + @param userPrivileges:
>> A DOM tree. + @type userPrivileges:
>> L{webdav.WebdavResponse.Element} object + + @raise WebdavError:
>> When unknown elements appear in the +
>> C{DAV:supported-privilege} appear a L{WebdavError} is raised. +
>> + @return: A dictionary with privilege names as keys and
>> privilege descriptions as values. + @rtype: C{dictionary} +
>> """ + result = {} + for element in userPrivileges.children: +
>> if element.name == Constants.TAG_SUPPORTED_PRIVILEGE: +
>> privName = '' + privDescription = '' +
>> for privilege in element.children: + if
>> privilege.name == Constants.TAG_PRIVILEGE: +
>> privName = privilege.children[0].name + elif
>> privilege.name == Constants.TAG_DESCRIPTION: +
>> privDescription = privilege.textof() + else: +
>> raise WebdavError('Unknown element in DAV:supported-privilege: ' +
>> privilege.name) + + if privName and
>> privDescription: + result[privName] =
>> privDescription + privName = '' +
>> privDescription = '' + else: + raise
>> WebdavError('Invalid element tag in DAV:supported-privilege-set: '
>> + element.name) + return result + + +def
>> _insertAclDisplaynames(acl): + """ + Modifies the ACL by
>> adding the human readable names + (DAV:displayname property) of
>> each principal found in an ACL. + + This should be done with the
>> REPORT method, but it is not supported by + Jacarta Slide, yet.
>> (As of Aug. 1, 2003 in CVS repository) + + So we are going to do
>> it differently by foot the harder way ... + + @param acl: An ACL
>> object for which the displaynames should be retrieved. + @type
>> acl: L{ACL} object + """ + ## This is redundant code to be
>> still kept for the REPORT method way of doing it ... + ##
>> property = '''<D:prop><D:displayname/></D:prop>''' + ## return
>> self.getReport(REPORT_ACL_PRINCIPAL_PROP_SET, property) + for
>> ace in acl.aces: + if not ace.principal.property: +
>> principalConnection = \ +
>> ResourceStorer(ace.principal.principalURL) +
>> ace.principal.displayname = \ +
>> principalConnection.readProperty(Constants.NS_DAV,
>> Constants.PROP_DISPLAY_NAME) + + +def prepareAcls(acls): + """ +
>> Returns all ACLs describing the behaviour of the resource. The
>> information + in the ACL is modified to contain all information
>> needed to display in the UI. + + @param acls: ACL objects. +
>> @type acls: C{list} of L{ACL} objects + + @return: (non-valid)
>> ACLs that contain both grant and deny clauses in an ACE. +
>> Displaynames are added to the Principals where needed. + @rtype:
>> C{list} of L{ACL} objects + """ + for acl in acls.keys(): +
>> acls[acl] = acls[acl].joinGrantDeny() +
>> _insertAclDisplaynames(acls[acl]) + return acls + + +def
>> prepareAcl(acl): + """ + Returns an ACL describing the
>> behaviour of the resource. The information + in the ACL is
>> modified to contain all information needed to display in the UI. +
>> + @param acl: An ACL object. + @type acl: L{ACL} object +
>> + @return: A (non-valid) ACL that contains both grant and deny
>> clauses in an ACE. + Displaynames are added to the
>> Principals where needed. + @rtype: L{ACL} object + """ +
>> acl = acl.joinGrantDeny() + _insertAclDisplaynames(acl) +
>> return acl + + +def refineAclForSet(acl): + """ + Sets the
>> ACL composed from the UI on the WebDAV server. For that purpose the
>> + ACL object gets refined first to form a well accepted ACL to
>> be set by the + ACL WebDAV method. + + @param acl: An ACL
>> object to be refined. + @type acl: L{ACL} object + +
>> @return: A valid ACL that contains only grant or deny clauses in an
>> ACE. + Inherited and protected ACEs are stripped out. +
>> @rtype: L{ACL} object + """ + acl = acl.splitGrantDeny() +
>> acl = acl.stripAces() + return acl + + +##~ unsupported or
>> unfinished methods: +##~ +##~ def report(self, report,
>> request=None, lockToken=None): +##~ """ +##~ This method
>> implements the WebDAV ACP method: REPORT for given report +##~
>> types. +##~ +##~ Parameters: +##~ +##~ 'report' -- Report
>> type as a string. +##~ +##~ 'request' -- XML content of the
>> request for the report (defaults to None). +##~ +##~
>> 'lockToken' -- Lock token to be set (defaults to None). +##~
>> """ +##~ raise WebdavError('Reports are not supported by our
>> Jacarta Slide, yet (as of Aug. 1, 2003 in CVS).') +##~ +##~
>> headers = createCondition(lockToken) +##~
>> headers['Content-Type'] = XML_CONTENT_TYPE +##~ body
>> = '<D:%s xmlns:D="DAV:">%s</D:%s>' % (report, request, report) +##~
>> #print "Body: ", body +##~ response =
>> self.connection._request('REPORT', self.path, body, headers) +##~
>> return response +##~ ## TODO: parse DAV:error response +##~
>> +##~ +##~ def getAllAcls(self): +##~ """ +##~ Returns a
>> dictionary of ACL resources with respective ACL objects +##~
>> that apply to the given resource. +##~ +##~ ### This method
>> needs to be extended for inherited ACLs when Tamino +##~
>> support tells me (Guy) how to get to them. +##~ """ +##~
>> acls = {self.path: self.getAcl()} +##~ for ace in
>> acls[self.path].aces: +##~ if ace.inherited: +##~
>> if not ace.inherited in acls: +##~
>> acls[ace.inherited] = self.getAcl() +##~ +##~ # append some
>> more stuff here to acls for possible inherited ACLs +##~ return
>> acls diff --git a/src/webdav/acp/Acl.py b/src/webdav/acp/Acl.py new
>> file mode 100644 index 0000000..8f2b36f --- /dev/null +++
>> b/src/webdav/acp/Acl.py @@ -0,0 +1,311 @@ +# pylint:
>> disable-msg=W0622 +# +# Copyright 2008 German Aerospace Center
>> (DLR) +# +# Licensed under the Apache License, Version 2.0 (the
>> "License"); +# you may not use this file except in compliance with
>> the License. +# You may obtain a copy of the License at +# +#
>> http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by
>> applicable law or agreed to in writing, software +# distributed
>> under the License is distributed on an "AS IS" BASIS, +# WITHOUT
>> WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#
>> See the License for the specific language governing permissions
>> and +# limitations under the License. + + + +""" +ACL object
>> handling according to WebDAV ACP specification. +""" + + +from
>> webdav.acp.Ace import ACE +from webdav import Constants +from
>> webdav.Connection import WebdavError +from webdav.davlib import
>> XML_DOC_HEADER + + +__version__ = "$LastChangedRevision$" + +
>> +class ACL(object): + """ + This class provides access to
>> Access Control List funcionality + as specified in the WebDAV
>> ACP. + + @ivar aces: ACEs in ACL + @type aces:
>> C{list} of L{ACE} objects + @ivar withInherited: Flag indicating
>> whether ACL contains inherited ACEs. + @type withInherited:
>> C{bool} + """ + + # restrict instance variables +
>> __slots__ = ('aces', 'withInherited') + + def __init__(self,
>> domroot=None, aces=None): + """ + Constructor should
>> be called with either no parameters (create blank ACE), + or
>> one parameter (a DOM tree or ACE list). + + @param domroot:
>> A DOM tree (default: None). + @type domroot:
>> L{webdav.WebdavResponse.Element} object + @param aces:
>> ACE objects (default: None) + @type aces: C{list} of
>> L{ACE} objects + + @raise WebdavError: When non-valid
>> parameters are passed a L{WebdavError} is raised. + """ +
>> self.withInherited = None + self.aces = [] +
>> + if domroot: + for child in domroot.children: +
>> if child.name == Constants.TAG_ACE and child.ns ==
>> Constants.NS_DAV: + self.addAce(ACE(child)) +
>> else: + # This shouldn't happen, someone screwed
>> up with the params ... + raise
>> WebdavError('Non-ACE tag handed to ACL constructor: ' + child.ns +
>> child.name) + elif isinstance(aces, list) or
>> isinstance(aces, tuple): + self.addAces(aces) +
>> elif domroot == None and aces == None: + # no param ==>
>> blank object + pass + else: + # This
>> shouldn't happen, someone screwed up with the params ... +
>> raise WebdavError('non-valid parameters handed to ACL
>> constructor') + + def __cmp__(self, other): + if not
>> isinstance(other, ACL): + return 1 + if
>> self.withInherited == other.withInherited: + equal = 1 +
>> for ace in self.aces: + inList = 0 +
>> for otherAce in other.aces: + if ace ==
>> otherAce: + inList = 1 + if
>> inList == 0: + equal = 0 + return not
>> equal + else: + return 1 + + def
>> __repr__(self): + repr = '<class ACL: ' + if
>> self.withInherited: + repr += 'with inherited, ' +
>> first = 1 + repr += 'aces: [' + for ace in
>> self.aces: + if first: + repr += '%s' %
>> ace + first = 0 + else: +
>> repr += ', %s' % ace + return '%s]>' % (repr) + + def
>> copy(self, other): + '''Copy an ACL object. + +
>> @param other: Another ACL to copy. + @type other: L{ACL}
>> object + + @raise WebdavError: When an object that is not an
>> L{ACL} is passed + a L{WebdavError} is raised. +
>> ''' + if not isinstance(other, ACL): + raise
>> WebdavError('Non-ACL object passed to copy method: %s' %
>> other.__class__) + self.withInherited = other.withInherited
>> + if other.aces: + self.addAces(other.aces) + +
>> def toXML(self): + """ + Returns ACL content as a
>> string of valid XML as described in WebDAV ACP. + """ +
>> aclTag = 'D:' + Constants.TAG_ACL + return XML_DOC_HEADER
>> +\ + '<' + aclTag + ' xmlns:D="DAV:">' + reduce(lambda
>> xml, ace: xml + ace.toXML() + '\n', [''] + self.aces) +\ +
>> '</' + aclTag + '>' + + def addAce(self, ace): + ''' +
>> Adds the passed ACE object to list if it's not in it, yet. +
>> + @param ace: An ACE. + @type ace: L{ACE} object +
>> ''' + newAce = ACE() + newAce.copy(ace) + #
>> only add it if it's not in the list, yet ... + inList = 0 +
>> for element in self.aces: + if element == ace: +
>> inList = 1 + if not inList: +
>> self.aces.append(newAce) + + def addAces(self, aces): +
>> '''Adds the list of passed ACE objects to list. + + @param
>> aces: ACEs + @type aces: sequence of L{ACE} objects +
>> ''' + for ace in aces: + self.addAce(ace) + +
>> def delAce(self, ace): + '''Deletes the passed ACE object
>> from list. + + @param ace: An ACE. + @type ace:
>> L{ACE} object + + @raise WebdavError: When the ACE to be
>> deleted is not within the ACL + a L{WebdavError} is
>> raised. + ''' + # find where it is and delete it ...
>> + count = 0 + index = 0 + for element in
>> self.aces: + count += 1 + if element == ace:
>> + index = count + if index: +
>> self.aces.pop(index - 1) + else: + raise
>> WebdavError('ACE to be deleted not in list: %s.' % ace) + + def
>> delAces(self, aces): + '''Deletes the list of passed ACE
>> objects from list. + + @param aces: ACEs + @type
>> aces: sequence of L{ACE} objects + ''' + for ace in
>> aces: + self.delAce(ace) + + def
>> delPrincipalsAces(self, principal): + """ + Deletes
>> all ACEs in ACL by given principal. + + @param principal: A
>> principal. + @type principal: L{Principal} object +
>> """ + # find where it is and delete it ... + index =
>> 0 + while index < len(self.aces): + if
>> self.aces[index].principal.principalURL == principal.principalURL:
>> + self.aces.pop(index) + else: +
>> index += 1 + + def joinGrantDeny(self): + """ +
>> Returns a "refined" ACL of the ACL for ease of use in the UI. +
>> The purpose is to post the user an ACE that can contain both,
>> granted + and denied, privileges. So possible pairs of grant
>> and deny ACEs are joined + to return them in one ACE. This
>> resulting ACE then of course IS NOT valid + for setting ACLs
>> anymore. They will have to be reconverted to yield valid +
>> ACLs for the ACL method. + + @return: A (non-valid) ACL that
>> contains both grant and deny clauses in an ACE. + @rtype:
>> L{ACL} object + """ + joinedAces = {} + for
>> ace in self.aces: + if not ace.principal.principalURL is
>> None: + principalKey = ace.principal.principalURL +
>> elif not ace.principal.property is None: +
>> principalKey = ace.principal.property + else: +
>> principalKey = None + if ace.inherited: +
>> principalKey = ace.inherited + ":" + principalKey + if
>> principalKey in joinedAces: +
>> joinedAces[principalKey].addGrantDenies(ace.grantDenies) +
>> else: + joinedAces[principalKey] = ACE() +
>> joinedAces[principalKey].copy(ace) + newAcl = ACL() +
>> newAcl.addAces(joinedAces.values()) + return newAcl + +
>> def splitGrantDeny(self): + """ + Returns a "refined"
>> ACL of the ACL for ease of use in the UI. + The purpose is
>> to post the user an ACE that can contain both, granted + and
>> denied, privileges. So possible joined grant and deny clauses in
>> ACEs + splitted to return them in separate ACEs. This
>> resulting ACE then is valid + for setting ACLs again. This
>> method is to be seen in conjunction with the + method
>> joinGrantDeny as it reverts its effect. + + @return: A valid
>> ACL that contains only ACEs with either grant or deny clauses. +
>> @rtype: L{ACL} object + """ + acesGrant = {} +
>> acesDeny = {} + for ace in self.aces: + for
>> grantDeny in ace.grantDenies: + if
>> grantDeny.isGrant(): + if
>> ace.principal.principalURL in acesGrant: +
>> ace.addGrantDeny(grantDeny) + else: +
>> acesGrant[ace.principal.principalURL] = ACE() +
>> acesGrant[ace.principal.principalURL].copy(ace) +
>> acesGrant[ace.principal.principalURL].grantDenies = [] +
>> acesGrant[ace.principal.principalURL].addGrantDeny(grantDeny) +
>> else: + if ace.principal.principalURL in
>> acesDeny: + ace.addGrantDeny(grantDeny) +
>> else: + acesDeny[ace.principal.principalURL]
>> = ACE() +
>> acesDeny[ace.principal.principalURL].copy(ace) +
>> acesDeny[ace.principal.principalURL].grantDenies = [] +
>> acesDeny[ace.principal.principalURL].addGrantDeny(grantDeny) +
>> newAcl = ACL() + newAcl.addAces(acesGrant.values()) +
>> newAcl.addAces(acesDeny.values()) + return newAcl + + def
>> isValid(self): + """ + Returns true (1) if all
>> contained ACE objects are valid, + otherwise false (0) is
>> returned. + + @return: Validity of ACL. + @rtype:
>> C{bool} + """ + valid = 1 + if
>> len(self.aces): + for ace in self.aces: +
>> if not ace.isValid(): + valid = 0 +
>> return valid + + def stripAces(self, inherited=True,
>> protected=True): + """ + Returns an ACL object with
>> all ACEs stripped that are inherited + and/or protected. +
>> + @param inherited: Flag to indicate whether inherited ACEs
>> should + be stripped (default: True). + @type
>> inherited: C{bool} + @param protected: Flag to indicate
>> whether protected ACEs should + be stripped (default:
>> True). + @type protected: C{bool} + + @return: An
>> ACL without the stripped ACEs. + @rtype: L{ACL} object +
>> """ + newAcl = ACL() + if len(self.aces): +
>> for ace in self.aces: + keep = 1 + if
>> inherited and ace.inherited: + keep = 0 +
>> elif protected and ace.protected: + keep = 0 +
>> if keep: + newAcl.addAce(ace) + return
>> newAcl diff --git a/src/webdav/acp/GrantDeny.py
>> b/src/webdav/acp/GrantDeny.py new file mode 100644 index
>> 0000000..52c9b93 --- /dev/null +++ b/src/webdav/acp/GrantDeny.py @@
>> -0,0 +1,241 @@ +# Copyright 2008 German Aerospace Center (DLR) +#
>> +# Licensed under the Apache License, Version 2.0 (the "License");
>> +# you may not use this file except in compliance with the
>> License. +# You may obtain a copy of the License at +# +#
>> http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by
>> applicable law or agreed to in writing, software +# distributed
>> under the License is distributed on an "AS IS" BASIS, +# WITHOUT
>> WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#
>> See the License for the specific language governing permissions
>> and +# limitations under the License. + + +""" +Handling of grant
>> and deny clauses in ACEs according to WebDAV ACP specification.
>> +""" + + +from webdav.acp.Privilege import Privilege +from webdav
>> import Constants +from webdav.Connection import WebdavError + +
>> +__version__ = "$LastChangedRevision$" + + +class
>> GrantDeny(object): + """ + This class provides functionality
>> for handling + grant and deny clauses in ACEs. + + @ivar
>> grantDeny: Flag indicating whether clause grants or denies. +
>> @type grantDeny: C{bool} + @ivar privileges: Privileges to be
>> granted or denied. + @type privileges: C{list} of L{Privilege}
>> objects + """ + + def __init__(self, domroot=None): +
>> """ + Constructor should be called with either no parameters
>> + (create blank GrantDeny), or one parameter (a DOM tree).
>> + + @param domroot: A DOM tree (default: None). +
>> @type domroot: L{webdav.WebdavResponse.Element} object +
>> + @raise WebdavError: When non-valid parameters are passed
>> a L{WebdavError} is raised. + """ + self.grantDeny =
>> 0 # 0: deny, 1: grant + self.privileges = [] + + if
>> domroot: + self.grantDeny = (domroot.name ==
>> Constants.TAG_GRANT) + for child in domroot.children: +
>> if child.name == Constants.TAG_PRIVILEGE and child.ns ==
>> Constants.NS_DAV: +
>> self.privileges.append(Privilege(domroot=child)) +
>> else: + # This shouldn't happen, someone screwed
>> up with the params ... + raise
>> WebdavError('Non-privilege tag handed to GrantDeny constructor: %s'
>> \ + % child.name) + elif domroot ==
>> None: + # no param ==> blank object + pass +
>> else: + # This shouldn't happen, someone screwed up with
>> the params ... + raise WebdavError('Non-valid parameters
>> handed to GrantDeny constructor.') + + def __cmp__(self,
>> other): + """ Compares two GrantDeny instances. """ +
>> if not isinstance(other, GrantDeny): + return 1 +
>> if self.grantDeny == other.grantDeny: + equal = 1 +
>> for priv in self.privileges: + inList = 0 +
>> for otherPriv in other.privileges: + if priv ==
>> otherPriv: + inList = 1 + if
>> inList == 0: + equal = 0 + return not
>> equal + else: + return 1 + + def
>> __repr__(self): + """ Returns the representation of an
>> instance. """ + representation = '<class GrantDeny: ' +
>> if self.grantDeny: + representation += 'grant
>> privileges: [' + else: + representation += 'deny
>> privileges: [' + first = 1 + for priv in
>> self.privileges: + if first: +
>> representation += '%s' % priv + first = 0 +
>> else: + representation += ', %s' % priv +
>> return '%s]>' % (representation) + + def copy(self, other): +
>> """ + Copy a GrantDeny object. + + @param other:
>> Another grant or deny clause to copy. + @type other:
>> L{GrantDeny} object + + @raise WebdavError: When an object
>> that is not an L{GrantDeny} is passed + a L{WebdavError}
>> is raised. + """ + if not isinstance(other,
>> GrantDeny): + raise WebdavError('Non-GrantDeny object
>> passed to copy method: %s' \ + % other) +
>> self.grantDeny = other.grantDeny + if other.privileges: +
>> self.addPrivileges(other.privileges) + + def isGrant(self): +
>> """ + Returns whether the set of privileges is of type
>> "grant" + indicating true or false. + + @return:
>> Value whether the clause is of grant type. + @rtype:
>> C{bool} + """ + return self.grantDeny + + def
>> isDeny(self): + """ + Returns whether the set of
>> privileges is of type "deny" + indicating true or false. +
>> + @return: Value whether the clause is of deny type. +
>> @rtype: C{bool} + """ + return not self.grantDeny +
>> + def setGrantDeny(self, grantDeny): + """ + Sets
>> the set of privileges to given value for grantDeny. + +
>> @param grantDeny: Grant/deny value for clause (grant: True/1, deny:
>> False/0). + @type grantDeny: C{bool} + """ +
>> if grantDeny == 0 or grantDeny == 1: + self.grantDeny =
>> grantDeny + + def setGrant(self): + """ Sets the set of
>> privileges to type "grant". """ + self.grantDeny = 1 + +
>> def setDeny(self): + """ Sets the set of privileges to type
>> "deny". """ + self.grantDeny = 0 + + def isAll(self): +
>> """ + Checks whether the privileges contained are equal +
>> to aggregate DAV:all privilege. + + @return: Value whether
>> all un-aggregated privileges are present. + @rtype:
>> C{bool} + """ + + if len(self.privileges) == 1 and
>> self.privileges[0].name == Constants.TAG_ALL: + return
>> 1 + return 0 + + def addPrivilege(self, privilege): +
>> """ + Adds the passed privilege to list if it's not in it,
>> yet. + + @param privilege: A privilege. + @type
>> privilege: L{Privilege} object + """ + inList =
>> False + for priv in self.privileges: + if priv ==
>> privilege: + inList = True + if not inList: +
>> newPrivilege = Privilege() +
>> newPrivilege.copy(privilege) +
>> self.privileges.append(newPrivilege) + + def addPrivileges(self,
>> privileges): + """ + Adds the list of passed
>> privileges to list. + + @param privileges: Several
>> privileges. + @type privileges: sequence of L{Privilege}
>> objects + """ + for priv in privileges: +
>> self.addPrivilege(priv) + + def delPrivilege(self, privilege): +
>> """ + Deletes the passed privilege from list if it's in it.
>> + + @param privilege: A privilege. + @type
>> privilege: L{Privilege} object + + @raise WebdavError: A
>> L{WebdavError} is raised if the privilege to be +
>> deleted is not present. + """ + count = 0 +
>> index = 0 + for priv in self.privileges: + count
>> += 1 + if priv == privilege: + index =
>> count + if index: + self.privileges.pop(index -
>> 1) + else: + raise WebdavError('Privilege to be
>> deleted not in list: %s' % privilege) + + def
>> delPrivileges(self, privileges): + """ + Deletes the
>> list of passed privileges from list. + + @param privileges:
>> Several privileges. + @type privileges: sequence of
>> L{Privilege} objects + """ + for priv in privileges:
>> + self.delPrivilege(priv) + + def toXML(self): +
>> """ + Returns string of GrantDeny content to valid XML as
>> described in WebDAV ACP. + """ + assert
>> self.privileges, "GrantDeny object is not initialized or does not
>> contain content!" + + if self.isGrant(): + tag =
>> 'D:' + Constants.TAG_GRANT + else: + tag = 'D:' +
>> Constants.TAG_DENY + + res = '' + for privilege in
>> self.privileges: + res += privilege.toXML() +
>> return '<%s>%s</%s>' % (tag, res, tag) diff --git
>> a/src/webdav/acp/Makefile.am b/src/webdav/acp/Makefile.am new file
>> mode 100644 index 0000000..506eb92 --- /dev/null +++
>> b/src/webdav/acp/Makefile.am @@ -0,0 +1,12 @@ +sugardir =
>> $(pythondir)/webdav/acp +sugar_PYTHON = \ +
>> AceHandler.py \ + Ace.py \ +
>> Acl.py \ + GrantDeny.py \ +
>> __init__.py \ + Principal.py \ +
>> Privilege.py + + + diff --git a/src/webdav/acp/Principal.py
>> b/src/webdav/acp/Principal.py new file mode 100644 index
>> 0000000..a0d5ec9 --- /dev/null +++ b/src/webdav/acp/Principal.py @@
>> -0,0 +1,189 @@ +# Copyright 2008 German Aerospace Center (DLR) +#
>> +# Licensed under the Apache License, Version 2.0 (the "License");
>> +# you may not use this file except in compliance with the
>> License. +# You may obtain a copy of the License at +# +#
>> http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by
>> applicable law or agreed to in writing, software +# distributed
>> under the License is distributed on an "AS IS" BASIS, +# WITHOUT
>> WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#
>> See the License for the specific language governing permissions
>> and +# limitations under the License. + + +""" +Handling of
>> principals for ACEs according to WebDAV ACP specification. +""" +
>> + +from webdav import Constants +from webdav.Connection import
>> WebdavError + + +__version__ = "$LastChangedRevision$" + + +class
>> Principal(object): + """ + This class provides functionality
>> for handling + principals according to the WebDAV ACP. + +
>> @ivar displayname: Name of the principal for output + @type
>> displayname: C{string} + @ivar principalURL: URL under which
>> the principal can be referenced on the server. + @type
>> principalURL: C{string} + @ivar property: Information on
>> type of a pseudo/jproperty principal, e. g. + DAV:owner,
>> DAV:authenticated, etc. + @type property: C{string} + +
>> @cvar _TAG_LIST_PRINCIPALS: List of allowed XML tags within a
>> principal declaration. + @type _TAG_LIST_PRINCIPALS: C{tuple} of
>> C{string}s + @cvar _TAG_LIST_STATUS: List of XML tags for
>> the status of a pseudo principal. + @type _TAG_LIST_STATUS:
>> C{tuple} of C{string}s + """ + + # some local constants for
>> this class to make things easier/more readable: +
>> _TAG_LIST_PRINCIPALS = (Constants.TAG_HREF, # directly by URL +
>> Constants.TAG_ALL, Constants.TAG_AUTHENTICATED,
>> Constants.TAG_UNAUTHENTICATED, +
>> # by log-in status +
>> Constants.TAG_PROPERTY, # for property info, e. g. 'owner' +
>> Constants.TAG_SELF, # only if the resource is the principal
>> itself + Constants.TAG_PROP) #
>> contains property info like 'displayname' + _TAG_LIST_STATUS
>> = (Constants.TAG_ALL, Constants.TAG_AUTHENTICATED,
>> Constants.TAG_UNAUTHENTICATED) + + # restrict instance
>> variables + __slots__ = ('displayname', 'principalURL',
>> 'property') + + def __init__(self, domroot=None,
>> displayname=None, principalURL=None): + """ +
>> Constructor should be called with either no parameters (create
>> blank Principal), + one parameter (a DOM tree), or two
>> parameters (displayname and URL or property tag). + + @param
>> domroot: A DOM tree (default: None). + @type domroot:
>> L{webdav.WebdavResponse.Element} object + @param
>> displayname: The display name of a principal (default: None). +
>> @type displayname: C{string} + @param principalURL: The
>> URL representing a principal (default: None). + @type
>> principalURL: C{string} + + @raise WebdavError: When
>> non-valid parameters or sets of parameters are + passed
>> a L{WebdavError} is raised. + """ + self.displayname
>> = None + self.principalURL = None + self.property
>> = None + + if domroot: + for child in
>> domroot.children: + if child.ns == Constants.NS_DAV
>> and (child.name in self._TAG_LIST_PRINCIPALS): +
>> if child.name == Constants.TAG_PROP: +
>> self.displayname = \ +
>> child.find(Constants.PROP_DISPLAY_NAME, Constants.NS_DAV) +
>> elif child.name == Constants.TAG_HREF: +
>> self.principalURL = child.textof() + if
>> self.principalURL and self.property in self._TAG_LIST_STATUS: +
>> raise WebdavError('Principal cannot contain a URL and "%s"' %
>> (self.property)) + elif child.name ==
>> Constants.TAG_PROPERTY: + if child.count()
>> == 1: + if self.property: +
>> raise WebdavError('Property for principal has already been set: old
>> "%s", new "%s"' \ + %
>> (self.property, child.pop().name)) +
>> elif self.principalURL: + raise
>> WebdavError('Principal cannot contain a URL and "%s"' %
>> (self.property)) + else: +
>> self.property = child.pop().name + else: +
>> raise WebdavError("There should be only one value in the property
>> for a principal, we have: %s" \ + %
>> child.name) + else: + if
>> self.property: + raise
>> WebdavError('Property for principal has already been set: old "%s",
>> new "%s"' \ + % (self.property,
>> child.name)) + else: +
>> self.property = child.name + if
>> self.principalURL and self.property in self._TAG_LIST_STATUS: +
>> raise WebdavError('Principal cannot contain a URL and "%s"' %
>> (self.property)) + else: # This shouldn't happen,
>> something's wrong with the DOM tree + raise
>> WebdavError('Non-valid tag in principal DOM tree for constructor:
>> %s' % child.name) + elif displayname == None or principalURL
>> == None: + if displayname: +
>> self.displayname = displayname + if principalURL: +
>> self.principalURL = principalURL + else: + # This
>> shouldn't happen, someone screwed up with the params ... +
>> raise WebdavError('Non-valid parameters handed to Principal
>> constructor.') + + def __cmp__(self, other): + if not
>> isinstance(other, Principal): + return 1 + if
>> self.displayname == other.displayname \ + and
>> self.principalURL == other.principalURL \ + and
>> self.property == other.property: + return 0 +
>> else: + return 1 + + def __repr__(self): +
>> return '<class Principal: displayname: "%s", principalURL: "%s",
>> property: "%s">' \ + % (self.displayname,
>> self.principalURL, self.property) + + def copy(self, other): +
>> """Copy Principal object. + + @param other: Another
>> principal to copy. + @type other: L{Principal} object +
>> + @raise WebdavError: When an object that is not a
>> L{Principal} is passed + a L{WebdavError} is raised. +
>> """ + if not isinstance(other, Principal): +
>> raise WebdavError('Non-Principal object passed to copy method: ' %
>> other.__class__) + self.displayname = other.displayname +
>> self.principalURL = other.principalURL + self.property =
>> other.property + + def isValid(self): + """ +
>> Checks whether necessarry props for principal are set. + +
>> @return: Validity of principal. + @rtype: C{bool} +
>> """ + return (self.displayname and +
>> (self.principalURL or self.property) and + not
>> (self.principalURL and self.property)) + + def toXML(self,
>> invert=False, displayname=False, defaultNameSpace=None): +
>> """Returns string of Principal content in valid XML as described in
>> WebDAV ACP. + + @param defaultNameSpace: Name space
>> (default: None). + @type defaultNameSpace: C(string) +
>> @param invert: True if principal should be inverted
>> (default: False). + @type invert: C{bool} +
>> @param displayname: True if displayname should be in output
>> (default: False). + @type displayname: C{bool} +
>> """ + # this check is needed for setting principals only: +
>> # assert self.isValid(), "principal is not initialized or does not
>> contain valid content!" + + PRINCIPAL = 'D:' +
>> Constants.TAG_PRINCIPAL + res = '' + if
>> self.principalURL: + res += '<D:%s>%s</D:%s>' %
>> (Constants.TAG_HREF, self.principalURL, Constants.TAG_HREF) +
>> elif self.property in self._TAG_LIST_STATUS \ + or
>> self.property == Constants.TAG_SELF: + res += '<D:%s/>'
>> % (self.property) + elif self.property: + res +=
>> '<D:%s><D:%s/></D:%s>' \ + %
>> (Constants.TAG_PROPERTY, self.property, Constants.TAG_PROPERTY) +
>> if self.displayname and displayname: + res +=
>> '<D:%s><D:%s>%s</D:%s></D:%s>' \ + %
>> (Constants.TAG_PROP, Constants.PROP_DISPLAY_NAME, +
>> self.displayname, + Constants.PROP_DISPLAY_NAME,
>> Constants.TAG_PROP) + if invert: + res =
>> '<D:invert>%s</D:invert>' % (res) + return '<%s>%s</%s>' %
>> (PRINCIPAL, res, PRINCIPAL) diff --git
>> a/src/webdav/acp/Privilege.py b/src/webdav/acp/Privilege.py new
>> file mode 100644 index 0000000..abfdcf9 --- /dev/null +++
>> b/src/webdav/acp/Privilege.py @@ -0,0 +1,125 @@ +# Copyright 2008
>> German Aerospace Center (DLR) +# +# Licensed under the Apache
>> License, Version 2.0 (the "License"); +# you may not use this file
>> except in compliance with the License. +# You may obtain a copy of
>> the License at +# +#
>> http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by
>> applicable law or agreed to in writing, software +# distributed
>> under the License is distributed on an "AS IS" BASIS, +# WITHOUT
>> WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#
>> See the License for the specific language governing permissions
>> and +# limitations under the License. + + +""" +Handling for
>> privileges for grant and deny clauses in ACEs +according to WebDAV
>> ACP specification. +""" + + +from webdav import Constants +from
>> webdav.Connection import WebdavError + + +__version__ =
>> "$LastChangedRevision$" + + +class Privilege(object): + """This
>> class provides functionality for handling privileges for ACEs. +
>> + @ivar name: Name of the privilege. + @type name:
>> C{string} + + @cvar __privileges: List of allowed XML tags for
>> privileges. + @type __privileges: C{tuple} of C{string}s +
>> """ + + + __privileges = list() + + + def __init__(self,
>> privilege=None, domroot=None): + """ + Constructor
>> should be called with either no parameters (create blank
>> Privilege), + one parameter (a DOM tree or privilege name to
>> initialize it directly). + + @param domroot: A DOM tree
>> (default: None). + @type domroot:
>> L{webdav.WebdavResponse.Element} object + @param privilege:
>> The valid name of a privilege (default: None). + @type
>> privilege: C{string} + + @raise WebdavError: When non-valid
>> parameters or sets of parameters are +
>> passed a L{WebdavError} is raised. + """ + +
>> self.name = None + + if domroot: + if
>> len(domroot.children) != 1: + raise
>> WebdavError('Wrong number of elements for Privilege constructor, we
>> have: %i' \ + % (len(domroot.children))) +
>> else: + child = domroot.children[0] +
>> if child.ns == Constants.NS_DAV and child.name in
>> self.__privileges: + self.name = child.name +
>> else: + raise WebdavError('Not a valid privilege
>> tag, we have: %s%s' \ + % (child.ns,
>> child.name)) + elif privilege: + if privilege in
>> self.__privileges: + self.name = privilege +
>> else: + raise WebdavError('Not a valid privilege
>> tag, we have: %s.' % str(privilege)) + + @classmethod + def
>> registerPrivileges(cls, privileges): + """ +
>> Registers supported privilege tags. + + @param privileges:
>> List of privilege tags. + @type privileges: C{list} of
>> C{unicode} + """ + + for privilege in privileges: +
>> cls.__privileges.append(privilege) + + def __cmp__(self,
>> other): + """ Compares two Privilege instances. """ +
>> if not isinstance(other, Privilege): + return 1 +
>> if self.name != other.name: + return 1 + else: +
>> return 0 + + def __repr__(self): + """ Returns the string
>> representation of an instance. """ + return '<class
>> Privilege: name: "%s">' % (self.name) + + def copy(self,
>> other): + """ + Copy Privilege object. + +
>> @param other: Another privilege to copy. + @type other:
>> L{Privilege} object + + @raise WebdavError: When an object
>> that is not a L{Privilege} is passed + a L{WebdavError}
>> is raised. + """ + if not isinstance(other,
>> Privilege): + raise WebdavError('Non-Privilege object
>> passed to copy method: %s' % other.__class__) + self.name =
>> other.name + + def toXML(self): + """ + Returns
>> privilege content as string in valid XML as described in WebDAV
>> ACP. + + @param defaultNameSpace: Name space (default:
>> None). + @type defaultNameSpace: C(string) + """ +
>> assert self.name != None, "privilege is not initialized or does not
>> contain valid content!" + + privilege = 'D:' +
>> Constants.TAG_PRIVILEGE + return '<%s><D:%s/></%s>' %
>> (privilege, self.name, privilege) diff --git
>> a/src/webdav/acp/__init__.py b/src/webdav/acp/__init__.py new file
>> mode 100644 index 0000000..b5af299 --- /dev/null +++
>> b/src/webdav/acp/__init__.py @@ -0,0 +1,33 @@ +# Copyright 2008
>> German Aerospace Center (DLR) +# +# Licensed under the Apache
>> License, Version 2.0 (the "License"); +# you may not use this file
>> except in compliance with the License. +# You may obtain a copy of
>> the License at +# +#
>> http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by
>> applicable law or agreed to in writing, software +# distributed
>> under the License is distributed on an "AS IS" BASIS, +# WITHOUT
>> WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#
>> See the License for the specific language governing permissions
>> and +# limitations under the License. + + +from webdav import
>> Constants +from webdav.acp.Acl import ACL +from webdav.acp.Ace
>> import ACE +from webdav.acp.GrantDeny import GrantDeny +from
>> webdav.acp.Privilege import Privilege +from webdav.acp.Principal
>> import Principal + + +__version__ = "$LastChangedRevision$" + +
>> +privileges = [Constants.TAG_READ, Constants.TAG_WRITE,
>> Constants.TAG_WRITE_PROPERTIES, +
>> Constants.TAG_WRITE_CONTENT, Constants.TAG_UNLOCK,
>> Constants.TAG_READ_ACL, +
>> Constants.TAG_READ_CURRENT_USER_PRIVILEGE_SET,
>> Constants.TAG_WRITE_ACL, Constants.TAG_ALL, +
>> Constants.TAG_BIND, Constants.TAG_UNBIND,
>> Constants.TAG_TAMINO_SECURITY, +
>> Constants.TAG_BIND_COLLECTION, Constants.TAG_UNBIND_COLLECTION,
>> Constants.TAG_READ_PRIVATE_PROPERTIES, +
>> Constants.TAG_WRITE_PRIVATE_PROPERTIES]
>> +Privilege.registerPrivileges(privileges) diff --git
>> a/src/webdav/davlib.py b/src/webdav/davlib.py new file mode 100644
>> index 0000000..f4dac91 --- /dev/null +++ b/src/webdav/davlib.py @@
>> -0,0 +1,336 @@ +# pylint:
>> disable-msg=W0402,W0231,W0141,R0903,C0321,W0701,R0904,C0103,W0201,W0102,R0913,W0622,E1101,C0111,C0121,R0901
>>
>>
> +# DAV client library
>> +# +# Copyright (C) 1998-2000 Guido van Rossum. All Rights
>> Reserved. +# Written by Greg Stein. Given to Guido. Licensed using
>> the Python license. +# +# This module is maintained by Greg and is
>> available at: +# http://www.lyra.org/greg/python/davlib.py +# +#
>> Since this isn't in the Python distribution yet, we'll use the CVS
>> ID +# for tracking: +# $Id: davlib.py 3182 2008-02-22 15:57:55
>> +0000 (Fr, 22 Feb 2008) schlauch $ +# + +import httplib +import
>> urllib +import string +import types +import mimetypes +import
>> qp_xml + + +INFINITY = 'infinity' +XML_DOC_HEADER = '<?xml
>> version="1.0" encoding="utf-8"?>' +XML_CONTENT_TYPE = 'text/xml;
>> charset="utf-8"' + +# block size for copying files up to the
>> server +BLOCKSIZE = 16384 + + +class
>> HTTPProtocolChooser(httplib.HTTPSConnection): + def
>> __init__(self, *args, **kw): + self.protocol =
>> kw.pop('protocol') + if self.protocol == "https": +
>> self.default_port = 443 + else: +
>> self.default_port = 80 + +
>> apply(httplib.HTTPSConnection.__init__, (self,) + args, kw) + +
>> def connect(self): + if self.protocol == "https": +
>> httplib.HTTPSConnection.connect(self) + else: +
>> httplib.HTTPConnection.connect(self) + + +class
>> HTTPConnectionAuth(HTTPProtocolChooser): + def __init__(self,
>> *args, **kw): + apply(HTTPProtocolChooser.__init__, (self,)
>> + args, kw) + + self.__username = None +
>> self.__password = None + self.__nonce = None +
>> self.__opaque = None + + def setauth(self, username, password):
>> + self.__username = username + self.__password =
>> password + + +def _parse_status(elem): + text = elem.textof() +
>> idx1 = string.find(text, ' ') + idx2 = string.find(text, ' ',
>> idx1+1) + return int(text[idx1:idx2]), text[idx2+1:] + +class
>> _blank: + def __init__(self, **kw): +
>> self.__dict__.update(kw) +class _propstat(_blank): pass +class
>> _response(_blank): pass +class _multistatus(_blank): pass + +def
>> _extract_propstat(elem): + ps = _propstat(prop={}, status=None,
>> responsedescription=None) + for child in elem.children: +
>> if child.ns != 'DAV:': + continue + if child.name
>> == 'prop': + for prop in child.children: +
>> ps.prop[(prop.ns, prop.name)] = prop + elif child.name ==
>> 'status': + ps.status = _parse_status(child) +
>> elif child.name == 'responsedescription': +
>> ps.responsedescription = child.textof() + ### unknown
>> element name + + return ps + +def _extract_response(elem): +
>> resp = _response(href=[], status=None, responsedescription=None,
>> propstat=[]) + for child in elem.children: + if child.ns
>> != 'DAV:': + continue + if child.name == 'href':
>> + resp.href.append(child.textof()) + elif
>> child.name == 'status': + resp.status =
>> _parse_status(child) + elif child.name ==
>> 'responsedescription': + resp.responsedescription =
>> child.textof() + elif child.name == 'propstat': +
>> resp.propstat.append(_extract_propstat(child)) + ### unknown
>> child element + + return resp + +def _extract_msr(root): + if
>> root.ns != 'DAV:' or root.name != 'multistatus': + raise
>> 'invalid response: <DAV:multistatus> expected' + + msr =
>> _multistatus(responses=[ ], responsedescription=None) + + for
>> child in root.children: + if child.ns != 'DAV:': +
>> continue + if child.name == 'responsedescription': +
>> msr.responsedescription = child.textof() + elif child.name
>> == 'response': +
>> msr.responses.append(_extract_response(child)) + ### unknown
>> child element + + return msr + +def _extract_locktoken(root): +
>> if root.ns != 'DAV:' or root.name != 'prop': + raise
>> 'invalid response: <DAV:prop> expected' + elem =
>> root.find('lockdiscovery', 'DAV:') + if not elem: + raise
>> 'invalid response: <DAV:lockdiscovery> expected' + elem =
>> elem.find('activelock', 'DAV:') + if not elem: + raise
>> 'invalid response: <DAV:activelock> expected' + elem =
>> elem.find('locktoken', 'DAV:') + if not elem: + raise
>> 'invalid response: <DAV:locktoken> expected' + elem =
>> elem.find('href', 'DAV:') + if not elem: + raise 'invalid
>> response: <DAV:href> expected' + return elem.textof() + + +class
>> DAVResponse(httplib.HTTPResponse): + def
>> parse_multistatus(self): + self.root =
>> qp_xml.Parser().parse(self) + self.msr =
>> _extract_msr(self.root) + + def parse_lock_response(self): +
>> self.root = qp_xml.Parser().parse(self) + self.locktoken =
>> _extract_locktoken(self.root) + + +class DAV(HTTPConnectionAuth):
>> + + response_class = DAVResponse + + def get(self, url,
>> extra_hdrs={ }): + return self._request('GET', url,
>> extra_hdrs=extra_hdrs) + + def head(self, url, extra_hdrs={ }):
>> + return self._request('HEAD', url, extra_hdrs=extra_hdrs)
>> + + def post(self, url, data={ }, body=None, extra_hdrs={ }): +
>> headers = extra_hdrs.copy() + + assert body or data, "body
>> or data must be supplied" + assert not (body and data),
>> "cannot supply both body and data" + if data: +
>> body = '' + for key, value in data.items(): +
>> if isinstance(value, types.ListType): + for item
>> in value: + body = body + '&' + key + '=' +
>> urllib.quote(str(item)) + else: +
>> body = body + '&' + key + '=' + urllib.quote(str(value)) +
>> body = body[1:] + headers['Content-Type'] =
>> 'application/x-www-form-urlencoded' + + return
>> self._request('POST', url, body, headers) + + def options(self,
>> url='*', extra_hdrs={ }): + return self._request('OPTIONS',
>> url, extra_hdrs=extra_hdrs) + + def trace(self, url,
>> extra_hdrs={ }): + return self._request('TRACE', url,
>> extra_hdrs=extra_hdrs) + + def put(self, url, contents, +
>> content_type=None, content_enc=None, extra_hdrs={ }): + + if
>> not content_type: + content_type, content_enc =
>> mimetypes.guess_type(url) + + headers = extra_hdrs.copy() +
>> if content_type: + headers['Content-Type'] =
>> content_type + if content_enc: +
>> headers['Content-Encoding'] = content_enc + return
>> self._request('PUT', url, contents, headers) + + def
>> delete(self, url, extra_hdrs={ }): + return
>> self._request('DELETE', url, extra_hdrs=extra_hdrs) + + def
>> propfind(self, url, body=None, depth=None, extra_hdrs={ }): +
>> headers = extra_hdrs.copy() + headers['Content-Type'] =
>> XML_CONTENT_TYPE + if depth is not None: +
>> headers['Depth'] = str(depth) + return
>> self._request('PROPFIND', url, body, headers) + + def
>> proppatch(self, url, body, extra_hdrs={ }): + headers =
>> extra_hdrs.copy() + headers['Content-Type'] =
>> XML_CONTENT_TYPE + return self._request('PROPPATCH', url,
>> body, headers) + + def mkcol(self, url, extra_hdrs={ }): +
>> return self._request('MKCOL', url, extra_hdrs=extra_hdrs) + +
>> def move(self, src, dst, extra_hdrs={ }): + headers =
>> extra_hdrs.copy() + headers['Destination'] = dst +
>> return self._request('MOVE', src, extra_hdrs=headers) + + def
>> copy(self, src, dst, depth=None, extra_hdrs={ }): + headers
>> = extra_hdrs.copy() + headers['Destination'] = dst +
>> if depth is not None: + headers['Depth'] = str(depth) +
>> return self._request('COPY', src, extra_hdrs=headers) + + def
>> lock(self, url, owner='', timeout=None, depth=None, +
>> scope='exclusive', type='write', extra_hdrs={ }): + headers
>> = extra_hdrs.copy() + headers['Content-Type'] =
>> XML_CONTENT_TYPE + if depth is not None: +
>> headers['Depth'] = str(depth) + if timeout is not None: +
>> headers['Timeout'] = timeout + body = XML_DOC_HEADER + \ +
>> '<DAV:lockinfo xmlns:DAV="DAV:">' + \ +
>> '<DAV:lockscope><DAV:%s/></DAV:lockscope>' % scope + \ +
>> '<DAV:locktype><DAV:%s/></DAV:locktype>' % type + \ +
>> '<DAV:owner>' + owner + '</DAV:owner>' + \ +
>> '</DAV:lockinfo>' + return self._request('LOCK', url, body,
>> extra_hdrs=headers) + + def unlock(self, url, locktoken,
>> extra_hdrs={ }): + headers = extra_hdrs.copy() + if
>> locktoken[0] != '<': + locktoken = '<' + locktoken +
>> '>' + headers['Lock-Token'] = locktoken + return
>> self._request('UNLOCK', url, extra_hdrs=headers) + + def
>> _request(self, method, url, body=None, extra_hdrs={}): +
>> "Internal method for sending a request." + +
>> self.request(method, url, body, extra_hdrs) + return
>> self.getresponse() + + + # + # Higher-level methods for
>> typical client use + # + + def allprops(self, url,
>> depth=None): + body = XML_DOC_HEADER + \ +
>> '<DAV:propfind xmlns:DAV="DAV:"><DAV:allprop/></DAV:propfind>' +
>> return self.propfind(url, body, depth=depth) + + def
>> propnames(self, url, depth=None): + body = XML_DOC_HEADER +
>> \ + '<DAV:propfind
>> xmlns:DAV="DAV:"><DAV:propname/></DAV:propfind>' + return
>> self.propfind(url, body, depth) + + def getprops(self, url,
>> *names, **kw): + assert names, 'at least one property name
>> must be provided' + if kw.has_key('ns'): + xmlns
>> = ' xmlns:NS="' + kw['ns'] + '"' + ns = 'NS:' +
>> del kw['ns'] + else: + xmlns = ns = '' +
>> if kw.has_key('depth'): + depth = kw['depth'] +
>> del kw['depth'] + else: + depth = 0 +
>> assert not kw, 'unknown arguments' + body = XML_DOC_HEADER +
>> \ + '<DAV:propfind xmlns:DAV="DAV:"' + xmlns +
>> '><DAV:prop><' + ns + \ + string.joinfields(names,
>> '/><' + ns) + \ + '/></DAV:prop></DAV:propfind>' +
>> return self.propfind(url, body, depth) + + def delprops(self,
>> url, *names, **kw): + assert names, 'at least one property
>> name must be provided' + if kw.has_key('ns'): +
>> xmlns = ' xmlns:NS="' + kw['ns'] + '"' + ns = 'NS:' +
>> del kw['ns'] + else: + xmlns = ns = '' +
>> assert not kw, 'unknown arguments' + body = XML_DOC_HEADER +
>> \ + '<DAV:propertyupdate xmlns:DAV="DAV:"' + xmlns +
>> \ + '><DAV:remove><DAV:prop><' + ns + \ +
>> string.joinfields(names, '/><' + ns) + \ +
>> '/></DAV:prop></DAV:remove></DAV:propertyupdate>' + return
>> self.proppatch(url, body) + + def setprops(self, url, *xmlprops,
>> **props): + assert xmlprops or props, 'at least one property
>> must be provided' + xmlprops = list(xmlprops) + if
>> props.has_key('ns'): + xmlns = ' xmlns:NS="' +
>> props['ns'] + '"' + ns = 'NS:' + del
>> props['ns'] + else: + xmlns = ns = '' +
>> for key, value in props.items(): + if value: +
>> xmlprops.append('<%s%s>%s</%s%s>' % (ns, key, value, ns, key)) +
>> else: + xmlprops.append('<%s%s/>' % (ns, key)) +
>> elems = string.joinfields(xmlprops, '') + body =
>> XML_DOC_HEADER + \ + '<DAV:propertyupdate
>> xmlns:DAV="DAV:"' + xmlns + \ +
>> '><DAV:set><DAV:prop>' + \ + elems + \ +
>> '</DAV:prop></DAV:set></DAV:propertyupdate>' + return
>> self.proppatch(url, body) + + def get_lock(self, url, owner='',
>> timeout=None, depth=None): + response = self.lock(url,
>> owner, timeout, depth) + response.parse_lock_response() +
>> return response.locktoken + \ No newline at end of file diff --git
>> a/src/webdav/logger.py b/src/webdav/logger.py new file mode 100644
>> index 0000000..d2538ef --- /dev/null +++ b/src/webdav/logger.py @@
>> -0,0 +1,51 @@ +# Copyright 2008 German Aerospace Center (DLR) +# +#
>> Licensed under the Apache License, Version 2.0 (the "License"); +#
>> you may not use this file except in compliance with the License. +#
>> You may obtain a copy of the License at +# +#
>> http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by
>> applicable law or agreed to in writing, software +# distributed
>> under the License is distributed on an "AS IS" BASIS, +# WITHOUT
>> WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#
>> See the License for the specific language governing permissions
>> and +# limitations under the License. + + +"""" +Module provides
>> access to a configured logger instance. +The logger writes
>> C{sys.stdout}. +""" + + +import logging +import sys + +
>> +__version__ = "$LastChangedRevision$"[11:-2] + +
>> +_defaultLoggerName = "webdavLogger" +_fileLogFormat =
>> "%(asctime)s: %(levelname)s: %(message)s" + + +def
>> getDefaultLogger(handler=None): + """ + Returns a configured
>> logger object. + + @return: Logger instance. + @rtype:
>> C{logging.Logger} + """ + + myLogger =
>> logging.getLogger(_defaultLoggerName) + if
>> len(myLogger.handlers) == 0: + myLogger.level =
>> logging.DEBUG + formatter =
>> logging.Formatter(_fileLogFormat) + if handler is None: +
>> stdoutHandler = logging.StreamHandler(sys.stdout) +
>> stdoutHandler.setFormatter(formatter) +
>> myLogger.addHandler(stdoutHandler) + else: +
>> myLogger.addHandler(handler) + return myLogger diff --git
>> a/src/webdav/qp_xml.py b/src/webdav/qp_xml.py new file mode 100644
>> index 0000000..f167e1b --- /dev/null +++ b/src/webdav/qp_xml.py @@
>> -0,0 +1,240 @@ +# pylint:
>> disable-msg=W0311,E1101,E1103,W0201,C0103,W0622,W0402,W0706,R0911,W0613,W0612,R0912,W0141,C0111,C0121
>>
>>
> +
>> +# qp_xml: Quick Parsing for XML +# +# Written by Greg Stein.
>> Public Domain. +# No Copyright, no Rights Reserved, and no
>> Warranties. +# +# This module is maintained by Greg and is
>> available as part of the XML-SIG +# distribution. This module and
>> its changelog can be fetched at: +#
>> http://www.lyra.org/cgi-bin/viewcvs.cgi/xml/xml/utils/qp_xml.py +#
>> +# Additional information can be found on Greg's Python page at: +#
>> http://www.lyra.org/greg/python/ +# +# This module was added to the
>> XML-SIG distribution on February 14, 2000. +# As part of that
>> distribution, it falls under the XML distribution license. +# +
>> +import string +from xml.parsers import expat + + +error = __name__
>> + '.error' + + +# +# The parsing class. Instantiate and pass a
>> string/file to .parse() +# +class Parser: + def __init__(self): +
>> self.reset() + + def reset(self): + self.root = None +
>> self.cur_elem = None + + def find_prefix(self, prefix): + elem
>> = self.cur_elem + while elem: + if
>> elem.ns_scope.has_key(prefix): + return
>> elem.ns_scope[prefix] + elem = elem.parent + + if prefix ==
>> '': + return '' # empty URL for "no namespace" + +
>> return None + + def process_prefix(self, name, use_default): +
>> idx = string.find(name, ':') + if idx == -1: + if
>> use_default: + return self.find_prefix(''), name +
>> return '', name # no namespace + + if string.lower(name[:3])
>> == 'xml': + return '', name # name is reserved by XML.
>> don't break out a NS. + + ns = self.find_prefix(name[:idx]) +
>> if ns is None: + raise error, 'namespace prefix ("%s") not
>> found' % name[:idx] + + return ns, name[idx+1:] + + def
>> start(self, name, attrs): + elem = _element(name=name,
>> lang=None, parent=None, + children=[],
>> ns_scope={}, attrs={}, + first_cdata='',
>> following_cdata='') + + if self.cur_elem: + elem.parent =
>> self.cur_elem + elem.parent.children.append(elem) +
>> self.cur_elem = elem + else: + self.cur_elem = self.root =
>> elem + + work_attrs = [ ] + + # scan for namespace
>> declarations (and xml:lang while we're at it) + for name, value
>> in attrs.items(): + if name == 'xmlns': +
>> elem.ns_scope[''] = value + elif name[:6] == 'xmlns:': +
>> elem.ns_scope[name[6:]] = value + elif name == 'xml:lang': +
>> elem.lang = value + else: + work_attrs.append((name,
>> value)) + + # inherit xml:lang from parent + if elem.lang is
>> None and elem.parent: + elem.lang = elem.parent.lang + + #
>> process prefix of the element name + elem.ns, elem.name =
>> self.process_prefix(elem.name, 1) + + # process attributes'
>> namespace prefixes + for name, value in work_attrs: +
>> elem.attrs[self.process_prefix(name, 0)] = value + + def end(self,
>> name): + parent = self.cur_elem.parent + + del
>> self.cur_elem.ns_scope + del self.cur_elem.parent + +
>> self.cur_elem = parent + + def cdata(self, data): + elem =
>> self.cur_elem + if elem.children: + last =
>> elem.children[-1] + last.following_cdata =
>> last.following_cdata + data + else: + elem.first_cdata =
>> elem.first_cdata + data + + def parse(self, input): +
>> self.reset() + + p = expat.ParserCreate() +
>> p.StartElementHandler = self.start + p.EndElementHandler =
>> self.end + p.CharacterDataHandler = self.cdata + + try: +
>> if type(input) == type(''): + p.Parse(input, 1) +
>> else: + while 1: + s = input.read(_BLOCKSIZE) +
>> if not s: + p.Parse('', 1) + break + +
>> p.Parse(s, 0) + + finally: + if self.root: +
>> _clean_tree(self.root) + + return self.root + + +# +# handy
>> function for dumping a tree that is returned by Parser +# +def
>> dump(f, root): + f.write('<?xml version="1.0"?>\n') + namespaces
>> = _collect_ns(root) + _dump_recurse(f, root, namespaces,
>> dump_ns=1) + f.write('\n') + + +# +# This function returns the
>> element's CDATA. Note: this is not recursive -- +# it only returns
>> the CDATA immediately within the element, excluding the +# CDATA in
>> child elements. +# +def textof(elem): + return elem.textof() + +
>> +#########################################################################
>>
>>
> +#
>> +# private stuff for qp_xml +# + +_BLOCKSIZE = 16384 # chunk
>> size for parsing input + +class _element: + def __init__(self,
>> **kw): + self.__dict__.update(kw) + + def textof(self): +
>> '''Return the CDATA of this element. + + Note: this is not
>> recursive -- it only returns the CDATA immediately + within the
>> element, excluding the CDATA in child elements. + ''' + s =
>> self.first_cdata + for child in self.children: + s = s +
>> child.following_cdata + return s + + def find(self, name,
>> ns=''): + for elem in self.children: + if elem.name == name
>> and elem.ns == ns: + return elem + return None + + +def
>> _clean_tree(elem): + elem.parent = None + del elem.parent +
>> map(_clean_tree, elem.children) + + +def _collect_recurse(elem,
>> dict): + dict[elem.ns] = None + for ns, name in
>> elem.attrs.keys(): + dict[ns] = None + for child in
>> elem.children: + _collect_recurse(child, dict) + +def
>> _collect_ns(elem): + "Collect all namespaces into a NAMESPACE ->
>> PREFIX mapping." + d = { '' : None } + _collect_recurse(elem, d)
>> + del d[''] # make sure we don't pick up no-namespace entries +
>> keys = d.keys() + for i in range(len(keys)): + d[keys[i]] = i +
>> return d + +def _dump_recurse(f, elem, namespaces, lang=None,
>> dump_ns=0): + if elem.ns: + f.write('<ns%d:%s' %
>> (namespaces[elem.ns], elem.name)) + else: + f.write('<' +
>> elem.name) + for (ns, name), value in elem.attrs.items(): + if
>> ns: + f.write(' ns%d:%s="%s"' % (namespaces[ns], name,
>> value)) + else: + f.write(' %s="%s"' % (name, value)) + if
>> dump_ns: + for ns, id in namespaces.items(): + f.write('
>> xmlns:ns%d="%s"' % (id, ns)) + if elem.lang != lang: +
>> f.write(' xml:lang="%s"' % elem.lang) + if elem.children or
>> elem.first_cdata: + f.write('>' + elem.first_cdata) + for
>> child in elem.children: + _dump_recurse(f, child, namespaces,
>> elem.lang) + f.write(child.following_cdata) + if elem.ns: +
>> f.write('</ns%d:%s>' % (namespaces[elem.ns], elem.name)) +
>> else: + f.write('</%s>' % elem.name) + else: +
>> f.write('/>') diff --git a/src/webdav/uuid_.py
>> b/src/webdav/uuid_.py new file mode 100644 index 0000000..3b590e8
>> --- /dev/null +++ b/src/webdav/uuid_.py @@ -0,0 +1,476 @@ +r"""UUID
>> objects (universally unique identifiers) according to RFC 4122. +
>> +This module provides immutable UUID objects (class UUID) and the
>> functions +uuid1(), uuid3(), uuid4(), uuid5() for generating
>> version 1, 3, 4, and 5 +UUIDs as specified in RFC 4122. + +If all
>> you want is a unique ID, you should probably call uuid1() or
>> uuid4(). +Note that uuid1() may compromise privacy since it creates
>> a UUID containing +the computer's network address. uuid4() creates
>> a random UUID. + +Typical usage: + + >>> import uuid + + #
>> make a UUID based on the host ID and current time + >>>
>> uuid.uuid1() + UUID('a8098c1a-f86e-11da-bd1a-00112444be1e') + +
>> # make a UUID using an MD5 hash of a namespace UUID and a name +
>> >>> uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org') +
>> UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e') + + # make a random
>> UUID + >>> uuid.uuid4() +
>> UUID('16fd2706-8baf-433b-82eb-8c7fada847da') + + # make a UUID
>> using a SHA-1 hash of a namespace UUID and a name + >>>
>> uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org') +
>> UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d') + + # make a UUID
>> from a string of hex digits (braces and hyphens ignored) + >>> x
>> = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}') + + #
>> convert a UUID to a string of hex digits in standard form + >>>
>> str(x) + '00010203-0405-0607-0809-0a0b0c0d0e0f' + + # get the
>> raw 16 bytes of the UUID + >>> x.bytes +
>> '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f' + +
>> # make a UUID from a 16-byte string + >>>
>> uuid.UUID(bytes=x.bytes) +
>> UUID('00010203-0405-0607-0809-0a0b0c0d0e0f') +""" + +__author__ =
>> 'Ka-Ping Yee <ping at zesty.ca>' +__date__ = '$Date: 2006/06/12
>> 23:15:40 $'.split()[1].replace('/', '-') +__version__ = '$Revision:
>> 1.30 $'.split()[1] + +RESERVED_NCS, RFC_4122, RESERVED_MICROSOFT,
>> RESERVED_FUTURE = [ + 'reserved for NCS compatibility',
>> 'specified in RFC 4122', + 'reserved for Microsoft
>> compatibility', 'reserved for future definition'] + +class
>> UUID(object): + """Instances of the UUID class represent UUIDs
>> as specified in RFC 4122. + UUID objects are immutable,
>> hashable, and usable as dictionary keys. + Converting a UUID to
>> a string with str() yields something in the form +
>> '12345678-1234-1234-1234-123456789abc'. The UUID constructor
>> accepts + four possible forms: a similar string of hexadecimal
>> digits, or a + string of 16 raw bytes as an argument named
>> 'bytes', or a tuple of + six integer fields (with 32-bit,
>> 16-bit, 16-bit, 8-bit, 8-bit, and + 48-bit values respectively)
>> as an argument named 'fields', or a single + 128-bit integer as
>> an argument named 'int'. + + UUIDs have these read-only
>> attributes: + + bytes the UUID as a 16-byte string + +
>> fields a tuple of the six integer fields of the UUID, +
>> which are also available as six individual attributes +
>> and two derived attributes: + + time_low
>> the first 32 bits of the UUID + time_mid
>> the next 16 bits of the UUID + time_hi_version
>> the next 16 bits of the UUID + clock_seq_hi_variant
>> the next 8 bits of the UUID + clock_seq_low
>> the next 8 bits of the UUID + node
>> the last 48 bits of the UUID + + time
>> the 60-bit timestamp + clock_seq the
>> 14-bit sequence number + + hex the UUID as a
>> 32-character hexadecimal string + + int the UUID as
>> a 128-bit integer + + urn the UUID as a URN as
>> specified in RFC 4122 + + variant the UUID variant (one
>> of the constants RESERVED_NCS, + RFC_4122,
>> RESERVED_MICROSOFT, or RESERVED_FUTURE) + + version the
>> UUID version number (1 through 5, meaningful only +
>> when the variant is RFC_4122) + """ + + def __init__(self,
>> hex=None, bytes=None, fields=None, int=None, +
>> version=None): + r"""Create a UUID from either a string of
>> 32 hexadecimal digits, + a string of 16 bytes as the 'bytes'
>> argument, a tuple of six + integers (32-bit time_low, 16-bit
>> time_mid, 16-bit time_hi_version, + 8-bit
>> clock_seq_hi_variant, 8-bit clock_seq_low, 48-bit node) as +
>> the 'fields' argument, or a single 128-bit integer as the 'int' +
>> argument. When a string of hex digits is given, curly braces, +
>> hyphens, and a URN prefix are all optional. For example, these +
>> expressions all yield the same UUID: + +
>> UUID('{12345678-1234-5678-1234-567812345678}') +
>> UUID('12345678123456781234567812345678') +
>> UUID('urn:uuid:12345678-1234-5678-1234-567812345678') +
>> UUID(bytes='\x12\x34\x56\x78'*4) + UUID(fields=(0x12345678,
>> 0x1234, 0x5678, 0x12, 0x34, 0x567812345678)) +
>> UUID(int=0x12345678123456781234567812345678) + + Exactly one
>> of 'hex', 'bytes', 'fields', or 'int' must be given. + The
>> 'version' argument is optional; if given, the resulting UUID +
>> will have its variant and version number set according to RFC
>> 4122, + overriding bits in the given 'hex', 'bytes',
>> 'fields', or 'int'. + """ + + if [hex, bytes, fields,
>> int].count(None) != 3: + raise TypeError('need just one
>> of hex, bytes, fields, or int') + if hex is not None: +
>> hex = hex.replace('urn:', '').replace('uuid:', '') + hex
>> = hex.strip('{}').replace('-', '') + if len(hex) != 32:
>> + raise ValueError('badly formed hexadecimal UUID
>> string') + int = long(hex, 16) + if bytes is not
>> None: + if len(bytes) != 16: + raise
>> ValueError('bytes is not a 16-char string') + int =
>> long(('%02x'*16) % tuple(map(ord, bytes)), 16) + if fields
>> is not None: + if len(fields) != 6: +
>> raise ValueError('fields is not a 6-tuple') + (time_low,
>> time_mid, time_hi_version, + clock_seq_hi_variant,
>> clock_seq_low, node) = fields + if not 0 <= time_low <
>> 1<<32L: + raise ValueError('field 1 out of range
>> (need a 32-bit value)') + if not 0 <= time_mid <
>> 1<<16L: + raise ValueError('field 2 out of range
>> (need a 16-bit value)') + if not 0 <= time_hi_version <
>> 1<<16L: + raise ValueError('field 3 out of range
>> (need a 16-bit value)') + if not 0 <=
>> clock_seq_hi_variant < 1<<8L: + raise
>> ValueError('field 4 out of range (need an 8-bit value)') +
>> if not 0 <= clock_seq_low < 1<<8L: + raise
>> ValueError('field 5 out of range (need an 8-bit value)') +
>> if not 0 <= node < 1<<48L: + raise ValueError('field
>> 6 out of range (need a 48-bit value)') + clock_seq =
>> (clock_seq_hi_variant << 8L) | clock_seq_low + int =
>> ((time_low << 96L) | (time_mid << 80L) | +
>> (time_hi_version << 64L) | (clock_seq << 48L) | node) + if
>> int is not None: + if not 0 <= int < 1<<128L: +
>> raise ValueError('int is out of range (need a 128-bit value)') +
>> if version is not None: + if not 1 <= version <= 5: +
>> raise ValueError('illegal version number') + # Set the
>> variant to RFC 4122. + int &= ~(0xc000 << 48L) +
>> int |= 0x8000 << 48L + # Set the version number. +
>> int &= ~(0xf000 << 64L) + int |= version << 76L +
>> self.__dict__['int'] = int + + def __cmp__(self, other): +
>> if isinstance(other, UUID): + return cmp(self.int,
>> other.int) + return NotImplemented + + def
>> __hash__(self): + return hash(self.int) + + def
>> __int__(self): + return self.int + + def __repr__(self):
>> + return 'UUID(%r)' % str(self) + + def __setattr__(self,
>> name, value): + raise TypeError('UUID objects are
>> immutable') + + def __str__(self): + hex = '%032x' %
>> self.int + return '%s-%s-%s-%s-%s' % ( + hex[:8],
>> hex[8:12], hex[12:16], hex[16:20], hex[20:]) + + def
>> get_bytes(self): + bytes = '' + for shift in range(0,
>> 128, 8): + bytes = chr((self.int >> shift) & 0xff) +
>> bytes + return bytes + + bytes = property(get_bytes) + +
>> def get_fields(self): + return (self.time_low,
>> self.time_mid, self.time_hi_version, +
>> self.clock_seq_hi_variant, self.clock_seq_low, self.node) + +
>> fields = property(get_fields) + + def get_time_low(self): +
>> return self.int >> 96L + + time_low = property(get_time_low) + +
>> def get_time_mid(self): + return (self.int >> 80L) & 0xffff
>> + + time_mid = property(get_time_mid) + + def
>> get_time_hi_version(self): + return (self.int >> 64L) &
>> 0xffff + + time_hi_version = property(get_time_hi_version) + +
>> def get_clock_seq_hi_variant(self): + return (self.int >>
>> 56L) & 0xff + + clock_seq_hi_variant =
>> property(get_clock_seq_hi_variant) + + def
>> get_clock_seq_low(self): + return (self.int >> 48L) & 0xff
>> + + clock_seq_low = property(get_clock_seq_low) + + def
>> get_time(self): + return (((self.time_hi_version & 0x0fffL)
>> << 48L) | + (self.time_mid << 32L) | self.time_low)
>> + + time = property(get_time) + + def get_clock_seq(self): +
>> return (((self.clock_seq_hi_variant & 0x3fL) << 8L) | +
>> self.clock_seq_low) + + clock_seq = property(get_clock_seq) + +
>> def get_node(self): + return self.int & 0xffffffffffff + +
>> node = property(get_node) + + def get_hex(self): + return
>> '%032x' % self.int + + hex = property(get_hex) + + def
>> get_urn(self): + return 'urn:uuid:' + str(self) + + urn =
>> property(get_urn) + + def get_variant(self): + if not
>> self.int & (0x8000 << 48L): + return RESERVED_NCS +
>> elif not self.int & (0x4000 << 48L): + return RFC_4122 +
>> elif not self.int & (0x2000 << 48L): + return
>> RESERVED_MICROSOFT + else: + return
>> RESERVED_FUTURE + + variant = property(get_variant) + + def
>> get_version(self): + # The version bits are only meaningful
>> for RFC 4122 UUIDs. + if self.variant == RFC_4122: +
>> return int((self.int >> 76L) & 0xf) + + version =
>> property(get_version) + +def _ifconfig_getnode(): + """Get the
>> hardware address on Unix by running ifconfig.""" + import os +
>> for dir in ['', '/sbin/', '/usr/sbin']: + try: +
>> pipe = os.popen(os.path.join(dir, 'ifconfig')) + except
>> IOError: + continue + for line in pipe: +
>> words = line.lower().split() + for i in
>> range(len(words)): + if words[i] in ['hwaddr',
>> 'ether']: + return int(words[i + 1].replace(':',
>> ''), 16) + +def _ipconfig_getnode(): + """Get the hardware
>> address on Windows by running ipconfig.exe.""" + import os, re +
>> dirs = ['', r'c:\windows\system32', r'c:\winnt\system32'] +
>> try: + import ctypes + buffer =
>> ctypes.create_string_buffer(300) +
>> ctypes.windll.kernel32.GetSystemDirectoryA(buffer, 300) +
>> dirs.insert(0, buffer.value.decode('mbcs')) + except: +
>> pass + for dir in dirs: + try: + pipe =
>> os.popen(os.path.join(dir, 'ipconfig') + ' /all') + except
>> IOError: + continue + for line in pipe: +
>> value = line.split(':')[-1].strip().lower() + if
>> re.match('([0-9a-f][0-9a-f]-){5}[0-9a-f][0-9a-f]', value): +
>> return int(value.replace('-', ''), 16) + +def _netbios_getnode(): +
>> """Get the hardware address on Windows using NetBIOS calls. +
>> See http://support.microsoft.com/kb/118623 for details.""" +
>> import win32wnet, netbios + ncb = netbios.NCB() + ncb.Command
>> = netbios.NCBENUM + ncb.Buffer = adapters = netbios.LANA_ENUM()
>> + adapters._pack() + if win32wnet.Netbios(ncb) != 0: +
>> return + adapters._unpack() + for i in
>> range(adapters.length): + ncb.Reset() + ncb.Command =
>> netbios.NCBRESET + ncb.Lana_num = ord(adapters.lana[i]) +
>> if win32wnet.Netbios(ncb) != 0: + continue +
>> ncb.Reset() + ncb.Command = netbios.NCBASTAT +
>> ncb.Lana_num = ord(adapters.lana[i]) + ncb.Callname =
>> '*'.ljust(16) + ncb.Buffer = status =
>> netbios.ADAPTER_STATUS() + if win32wnet.Netbios(ncb) != 0: +
>> continue + status._unpack() + bytes = map(ord,
>> status.adapter_address) + return ((bytes[0]<<40L) +
>> (bytes[1]<<32L) + (bytes[2]<<24L) + +
>> (bytes[3]<<16L) + (bytes[4]<<8L) + bytes[5]) + +# Thanks to Thomas
>> Heller for ctypes and for his help with its use here. + +# If
>> ctypes is available, use it to find system routines for UUID
>> generation. +_uuid_generate_random = _uuid_generate_time =
>> _UuidCreate = None +try: + import ctypes, ctypes.util +
>> _buffer = ctypes.create_string_buffer(16) + + # The
>> uuid_generate_* routines are provided by libuuid on at least + #
>> Linux and FreeBSD, and provided by libc on Mac OS X. + for
>> libname in ['uuid', 'c']: + try: + lib =
>> ctypes.CDLL(ctypes.util.find_library(libname)) + except: +
>> continue + if hasattr(lib, 'uuid_generate_random'): +
>> _uuid_generate_random = lib.uuid_generate_random + if
>> hasattr(lib, 'uuid_generate_time'): +
>> _uuid_generate_time = lib.uuid_generate_time + + # On Windows
>> prior to 2000, UuidCreate gives a UUID containing the + #
>> hardware address. On Windows 2000 and later, UuidCreate makes a +
>> # random UUID and UuidCreateSequential gives a UUID containing the
>> + # hardware address. These routines are provided by the RPC
>> runtime. + try: + lib = ctypes.windll.rpcrt4 +
>> except: + lib = None + _UuidCreate = getattr(lib,
>> 'UuidCreateSequential', + getattr(lib,
>> 'UuidCreate', None)) +except: + pass + +def _unixdll_getnode():
>> + """Get the hardware address on Unix using ctypes.""" +
>> _uuid_generate_time(_buffer) + return
>> UUID(bytes=_buffer.raw).node + +def _windll_getnode(): + """Get
>> the hardware address on Windows using ctypes.""" + if
>> _UuidCreate(_buffer) == 0: + return
>> UUID(bytes=_buffer.raw).node + +def _random_getnode(): + """Get
>> a random node ID, with eighth bit set as suggested by RFC 4122."""
>> + import random + return random.randrange(0, 1<<48L) |
>> 0x010000000000L + +_node = None + +def getnode(): + """Get the
>> hardware address as a 48-bit integer. The first time this +
>> runs, it may launch a separate program, which could be quite slow.
>> If + all attempts to obtain the hardware address fail, we choose
>> a random + 48-bit number with its eighth bit set to 1 as
>> recommended in RFC 4122.""" + + global _node + if _node is
>> not None: + return _node + + import sys + if
>> sys.platform == 'win32': + getters = [_windll_getnode,
>> _netbios_getnode, _ipconfig_getnode] + else: + getters =
>> [_unixdll_getnode, _ifconfig_getnode] + + for getter in getters
>> + [_random_getnode]: + try: + _node = getter() +
>> except: + continue + if _node is not None: +
>> return _node + +def uuid1(node=None, clock_seq=None): +
>> """Generate a UUID from a host ID, sequence number, and the current
>> time. + If 'node' is not given, getnode() is used to obtain the
>> hardware + address. If 'clock_seq' is given, it is used as the
>> sequence number; + otherwise a random 14-bit sequence number is
>> chosen.""" + + # When the system provides a version-1 UUID
>> generator, use it (but don't + # use UuidCreate here because its
>> UUIDs don't conform to RFC 4122). + if _uuid_generate_time and
>> node is clock_seq is None: + _uuid_generate_time(_buffer) +
>> return UUID(bytes=_buffer.raw) + + import time + nanoseconds
>> = int(time.time() * 1e9) + # 0x01b21dd213814000 is the number of
>> 100-ns intervals between the + # UUID epoch 1582-10-15 00:00:00
>> and the Unix epoch 1970-01-01 00:00:00. + timestamp =
>> int(nanoseconds/100) + 0x01b21dd213814000L + if clock_seq is
>> None: + import random + clock_seq =
>> random.randrange(1<<14L) # instead of stable storage + time_low
>> = timestamp & 0xffffffffL + time_mid = (timestamp >> 32L) &
>> 0xffffL + time_hi_version = (timestamp >> 48L) & 0x0fffL +
>> clock_seq_low = clock_seq & 0xffL + clock_seq_hi_variant =
>> (clock_seq >> 8L) & 0x3fL + if node is None: + node =
>> getnode() + return UUID(fields=(time_low, time_mid,
>> time_hi_version, + clock_seq_hi_variant,
>> clock_seq_low, node), version=1) + +def uuid3(namespace, name): +
>> """Generate a UUID from the MD5 hash of a namespace UUID and a
>> name.""" + import md5 + hash = md5.md5(namespace.bytes +
>> name).digest() + return UUID(bytes=hash[:16], version=3) + +def
>> uuid4(): + """Generate a random UUID.""" + + # When the
>> system provides a version-4 UUID generator, use it. + if
>> _uuid_generate_random: + _uuid_generate_random(_buffer) +
>> return UUID(bytes=_buffer.raw) + + # Otherwise, get randomness
>> from urandom or the 'random' module. + try: + import os +
>> return UUID(bytes=os.urandom(16), version=4) + except: +
>> import random + bytes = [chr(random.randrange(256)) for i in
>> range(16)] + return UUID(bytes=bytes, version=4) + +def
>> uuid5(namespace, name): + """Generate a UUID from the SHA-1 hash
>> of a namespace UUID and a name.""" + import sha + hash =
>> sha.sha(namespace.bytes + name).digest() + return
>> UUID(bytes=hash[:16], version=5) + +# The following standard UUIDs
>> are for use with uuid3() or uuid5(). + +NAMESPACE_DNS =
>> UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8') +NAMESPACE_URL =
>> UUID('6ba7b811-9dad-11d1-80b4-00c04fd430c8') +NAMESPACE_OID =
>> UUID('6ba7b812-9dad-11d1-80b4-00c04fd430c8') +NAMESPACE_X500 =
>> UUID('6ba7b814-9dad-11d1-80b4-00c04fd430c8')
>
>
> --
> Anish Mangal
> Dextrose Project Manager
> Activity Central
- --
Anish Mangal
Dextrose Project Manager
Activity Central
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.12 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/
iQEcBAEBAgAGBQJPnUiNAAoJEBoxUdDHDZVpZNkH/RoUXLjO9TypnE7w2Ff+Oyiu
pf476SuKgE/Jxgymy5QiNHW/UNcFV/L5PuHVN8jI7nO2d5fbgQm3w6aXMSUAk44n
XPwRtHw5f1tGkCLEHxNsTwrhM38/l6l8pSmnQhgjAFJP1MwUDrz0jCTexso+WU19
0hb3wW/PinpnigD6JxXDqEHQZ4Fva+hsCo9x0mHTiuxNTVI0kWV0kzifcUfnztcw
v0mrr79FXovw3IjQMz0QtvPf5OrHW4j9zJN6fpvCuHPz1yAe/47QBaTOjToUag2M
2sujDLrpanwGfYSE5Euv+3wm217TLZZBN+2emVRzWenefMjGzUorttINYvtM9Lg=
=dDzc
-----END PGP SIGNATURE-----
More information about the Dextrose
mailing list