[Dextrose] [PATCH] 1-to-N Feature.
Anish Mangal
anish at activitycentral.com
Sun Apr 29 09:36:54 EDT 2012
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
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
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.12 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/
iQEcBAEBAgAGBQJPnUP1AAoJEBoxUdDHDZVpdk0H/0/0tP5Y9TUXh3gV+AaUQKVq
L/pc5JuaqL56o0lfixxJify0yntNAlqeOc6Mii37EVytkR9v67LESzzntz6CEH3W
nwW5Dmnk6DT+QRO9Dx6U3emesOOnouamki3SonIxstDp4WX9T6EzFRZzyM0mP+Pa
9TcDGQs77neiyMEE4TFZMQNpcq/GQeTISl4Bc8dTWtryHeHl04Rt9bMTMWfVqQ4E
LHZIvrwpQS9LaykpjrvkiL+ChVxAxZO1sDcfFSJkDuvTfWZV5VLYdQahqj8dvcip
H9qJ9QUKX2j2toGvKez5DXZLdTXIk2emgACrz+0JnuJydxtwk0zLGeb6DtK8By0=
=0U2i
-----END PGP SIGNATURE-----
More information about the Dextrose
mailing list