From 899508423642c284b1a42559aba8fd3be4bdd914 Mon Sep 17 00:00:00 2001 From: Sascha Silbe Date: Sun, 19 Jun 2011 20:53:44 +0000 Subject: WIP patch for remote mounts We need to identify mounts by URI since remote mounts doesn't necessarily have a local mount point. Previously we distinguished data store and "other" entries by examining the object id and relying on the fact that the data store never returns object ids with a trailing slash ("/"). Now we do almost the same, except that we rely on data store object ids never containing colons (":"). (This should disappear once we pass object_id everywhere) As with the previous code, we can't distinguish between multiple paths ("mount points") that don't have a corresponding gio.Mount (because they are considered "internal" storage). I.e. only a single "volume" showing something like ~/Documents is supported. TODO: - gvfs: WebDAV metadata write support - audit/cleanup code that uses metadata instead of object_id to identify an entry (done?) - pass object_id everywhere instead of using metadata['uid'] (done?) - explicitly remove metadata['uid'] from data store results - probably won't happen for now - maybe fix Sugar adding more extensions if the name of an externally written file does not match the primary extension Sugar uses. - maybe fix setting favourite star on non-Journal entries (e.g. ~/Documents) - write support - performance comparison - check all places doing URI parsing for escaping / quoting - add / use local / gio API for handling file paths (reading, writing) - maybe use convenience functions like gio.GFile.copy() - maybe unify webdav and local-storage funcs a bit --- diff --git a/src/jarabe/desktop/favoritesview.py b/src/jarabe/desktop/favoritesview.py index 2b8daf0..c5d2d15 100644 --- a/src/jarabe/desktop/favoritesview.py +++ b/src/jarabe/desktop/favoritesview.py @@ -18,6 +18,7 @@ import logging from gettext import gettext as _ import math +from urlparse import urlsplit import gobject import gconf @@ -30,7 +31,6 @@ from sugar.graphics.icon import Icon, CanvasIcon from sugar.graphics.menuitem import MenuItem from sugar.graphics.alert import Alert from sugar.graphics.xocolor import XoColor -from sugar.activity import activityfactory from sugar import dispatch from sugar.datastore import datastore @@ -386,7 +386,7 @@ class ActivityIcon(CanvasIcon): def _refresh(self): bundle_id = self._activity_info.get_bundle_id() properties = ['uid', 'title', 'icon-color', 'activity', 'activity_id', - 'mime_type', 'mountpoint'] + 'mime_type'] self._get_last_activity_async(bundle_id, properties) def __datastore_listener_updated_cb(self, **kwargs): @@ -395,8 +395,8 @@ class ActivityIcon(CanvasIcon): self._refresh() def __datastore_listener_deleted_cb(self, **kwargs): - for entry in self._journal_entries: - if entry['uid'] == kwargs['object_id']: + for object_uri, metadata_ in self._journal_entries: + if kwargs['object_id'] == urlsplit(object_uri)[2]: self._refresh() break @@ -412,9 +412,10 @@ class ActivityIcon(CanvasIcon): # If there's a problem with the DS index, we may get entries not # related to this activity. checked_entries = [] - for entry in entries: - if entry['activity'] == self.bundle_id: - checked_entries.append(entry) + for metadata in entries: + if metadata['activity'] == self.bundle_id: + object_uri = 'datastore:' + metadata['uid'] + checked_entries.append((object_uri, metadata)) self._journal_entries = checked_entries self._update() @@ -428,7 +429,7 @@ class ActivityIcon(CanvasIcon): xo_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), style.COLOR_TRANSPARENT.get_svg())) else: - xo_color = misc.get_icon_color(self._journal_entries[0]) + xo_color = misc.get_icon_color(self._journal_entries[0][1]) self.props.xo_color = xo_color def create_palette(self): @@ -440,8 +441,8 @@ class ActivityIcon(CanvasIcon): def __palette_activate_cb(self, palette): self._activate() - def __palette_entry_activate_cb(self, palette, metadata): - self._resume(metadata) + def __palette_entry_activate_cb(self, palette, object_uri): + self._resume(object_uri) def __hovering_changed_event_cb(self, icon, hovering): self._hovering = hovering @@ -486,17 +487,15 @@ class ActivityIcon(CanvasIcon): def __button_release_event_cb(self, icon, event): self._activate() - def _resume(self, journal_entry): - if not journal_entry['activity_id']: - journal_entry['activity_id'] = activityfactory.create_activity_id() - misc.resume(journal_entry, self._activity_info.get_bundle_id()) + def _resume(self, object_uri): + misc.resume(object_uri, self._activity_info.get_bundle_id()) def _activate(self): if self.palette is not None: self.palette.popdown(immediate=True) if self._resume_mode and self._journal_entries: - self._resume(self._journal_entries[0]) + self._resume(self._journal_entries[0][0]) else: misc.launch(self._activity_info) @@ -527,7 +526,7 @@ class FavoritePalette(ActivityPalette): __gsignals__ = { 'entry-activate': (gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, ([object])), + gobject.TYPE_NONE, [str]), } def __init__(self, activity_info, journal_entries): @@ -537,34 +536,36 @@ class FavoritePalette(ActivityPalette): xo_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), style.COLOR_TRANSPARENT.get_svg())) else: - xo_color = misc.get_icon_color(journal_entries[0]) + xo_color = misc.get_icon_color(journal_entries[0][1]) self.props.icon = Icon(file=activity_info.get_icon(), xo_color=xo_color, icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) - if journal_entries: - title = journal_entries[0]['title'] - self.props.secondary_text = glib.markup_escape_text(title) + if not journal_entries: + return + + first_title = journal_entries[0][1]['title'] + self.props.secondary_text = glib.markup_escape_text(first_title) - separator = gtk.SeparatorMenuItem() - self.menu.append(separator) - separator.show() + separator = gtk.SeparatorMenuItem() + self.menu.append(separator) + separator.show() - for entry in journal_entries: - icon_file_name = misc.get_icon_name(entry) - color = misc.get_icon_color(entry) + for object_uri, metadata in journal_entries: + icon_file_name = misc.get_icon_name(object_uri, metadata) + color = misc.get_icon_color(metadata) - menu_item = MenuItem(text_label=entry['title'], - file_name=icon_file_name, - xo_color=color) - menu_item.connect('activate', self.__resume_entry_cb, entry) - menu_item.show() - self.menu.append(menu_item) + menu_item = MenuItem(text_label=metadata['title'], + file_name=icon_file_name, + xo_color=color) + menu_item.connect('activate', self.__resume_entry_cb, object_uri) + menu_item.show() + self.menu.append(menu_item) - def __resume_entry_cb(self, menu_item, entry): - if entry is not None: - self.emit('entry-activate', entry) + def __resume_entry_cb(self, menu_item, object_uri): + if object_uri is not None: + self.emit('entry-activate', object_uri) class CurrentActivityIcon(CanvasIcon, hippo.CanvasItem): diff --git a/src/jarabe/frame/clipboardmenu.py b/src/jarabe/frame/clipboardmenu.py index 4c077d9..4cbf3a0 100644 --- a/src/jarabe/frame/clipboardmenu.py +++ b/src/jarabe/frame/clipboardmenu.py @@ -177,17 +177,19 @@ class ClipboardMenu(Palette): percent = self._cb_object.get_percent() if percent < 100 or menu_item.get_submenu() is not None: return - jobject = self._copy_to_journal() - misc.resume(jobject.metadata, self._get_activities()[0]) - jobject.destroy() + self._copy_to_journal_and_resume(self._get_activities()[0]) def _open_submenu_item_activate_cb(self, menu_item, service_name): logging.debug('_open_submenu_item_activate_cb') percent = self._cb_object.get_percent() if percent < 100: return + self._copy_to_journal_and_resume(service_name) + + def _copy_to_journal_and_resume(self, service_name): jobject = self._copy_to_journal() - misc.resume(jobject.metadata, service_name) + object_uri = 'datastore:' + jobject.object_id + misc.resume(object_uri, service_name) jobject.destroy() def _remove_item_activate_cb(self, menu_item): diff --git a/src/jarabe/journal/detailview.py b/src/jarabe/journal/detailview.py index aa8c039..613213f 100644 --- a/src/jarabe/journal/detailview.py +++ b/src/jarabe/journal/detailview.py @@ -37,6 +37,7 @@ class DetailView(gtk.VBox): def __init__(self, **kwargs): self._metadata = None + self._object_uri = None self._expanded_entry = None canvas = hippo.Canvas() @@ -68,22 +69,28 @@ class DetailView(gtk.VBox): if self._expanded_entry is None: self._expanded_entry = ExpandedEntry() self._root.append(self._expanded_entry, hippo.PACK_EXPAND) - self._expanded_entry.set_metadata(self._metadata) + self._expanded_entry.set_object(self._metadata, self._object_uri) def refresh(self): logging.debug('DetailView.refresh') - self._metadata = model.get(self._metadata['uid']) + self._metadata = model.get(self._object_uri) self._update_view() def get_metadata(self): return self._metadata - def set_metadata(self, metadata): + metadata = gobject.property(type=object, getter=get_metadata) + + def set_object(self, metadata, object_uri): self._metadata = metadata + self._object_uri = object_uri self._update_view() - metadata = gobject.property( - type=object, getter=get_metadata, setter=set_metadata) + def get_object_uri(self): + return self._object_uri + + object_uri = gobject.property(type=str, getter=get_object_uri) + class BackBar(hippo.CanvasBox): diff --git a/src/jarabe/journal/expandedentry.py b/src/jarabe/journal/expandedentry.py index 4e99dc2..c2a481b 100644 --- a/src/jarabe/journal/expandedentry.py +++ b/src/jarabe/journal/expandedentry.py @@ -18,7 +18,7 @@ import logging from gettext import gettext as _ import StringIO import time -import os +from urlparse import urlsplit import hippo import cairo @@ -74,6 +74,7 @@ class ExpandedEntry(hippo.CanvasBox): self.props.padding_top = style.DEFAULT_SPACING * 3 self._metadata = None + self._object_uri = None self._update_title_sid = None # Create header @@ -139,10 +140,12 @@ class ExpandedEntry(hippo.CanvasBox): self._buddy_list = hippo.CanvasBox() second_column.append(self._buddy_list) - def set_metadata(self, metadata): - if self._metadata == metadata: + def set_object(self, metadata, object_uri): + if self._object_uri == object_uri: return self._metadata = metadata + self._object_uri = object_uri + editable = model.is_editable(self._object_uri) self._keep_icon.keep = (int(metadata.get('keep', 0)) == 1) @@ -154,7 +157,7 @@ class ExpandedEntry(hippo.CanvasBox): title = self._title.props.widget title.props.text = metadata.get('title', _('Untitled')) - title.props.editable = model.is_editable(metadata) + title.props.editable = editable self._preview_box.clear() self._preview_box.append(self._create_preview()) @@ -167,11 +170,11 @@ class ExpandedEntry(hippo.CanvasBox): description = self._description.text_view_widget description.props.buffer.props.text = metadata.get('description', '') - description.props.editable = model.is_editable(metadata) + description.props.editable = editable tags = self._tags.text_view_widget tags.props.buffer.props.text = metadata.get('tags', '') - tags.props.editable = model.is_editable(metadata) + tags.props.editable = editable def _create_keep_icon(self): keep_icon = KeepIcon(False) @@ -179,7 +182,8 @@ class ExpandedEntry(hippo.CanvasBox): return keep_icon def _create_icon(self): - icon = CanvasIcon(file_name=misc.get_icon_name(self._metadata)) + icon_file_name = misc.get_icon_name(self._object_uri, self._metadata) + icon = CanvasIcon(file_name=icon_file_name) icon.connect_after('button-release-event', self._icon_button_release_event_cb) @@ -190,7 +194,7 @@ class ExpandedEntry(hippo.CanvasBox): xo_color = misc.get_icon_color(self._metadata) icon.props.xo_color = xo_color - icon.set_palette(ObjectPalette(self._metadata)) + icon.set_palette(ObjectPalette(self._metadata, self._object_uri)) return icon @@ -264,7 +268,7 @@ class ExpandedEntry(hippo.CanvasBox): _('Kind: %s') % (self._metadata.get('mime_type') or _('Unknown'),), _('Date: %s') % (self._format_date(),), _('Size: %s') % (format_size(int(self._metadata.get('filesize', - model.get_file_size(self._metadata['uid']))))), + model.get_file_size(self._object_uri))))), ] for line in lines: @@ -287,7 +291,7 @@ class ExpandedEntry(hippo.CanvasBox): timestamp = float(self._metadata['timestamp']) except (ValueError, TypeError): logging.warning('Invalid timestamp for %r: %r', - self._metadata['uid'], + self._object_uri, self._metadata['timestamp']) else: return time.strftime('%x', time.localtime(timestamp)) @@ -381,7 +385,7 @@ class ExpandedEntry(hippo.CanvasBox): self._update_entry() def _update_entry(self, needs_update=False): - if not model.is_editable(self._metadata): + if not model.is_editable(self._object_uri): return old_title = self._metadata.get('title', None) @@ -407,17 +411,28 @@ class ExpandedEntry(hippo.CanvasBox): needs_update = True if needs_update: - if self._metadata.get('mountpoint', '/') == '/': - model.write(self._metadata, update_mtime=False) - else: - old_file_path = os.path.join(self._metadata['mountpoint'], - model.get_file_name(old_title, - self._metadata['mime_type'])) - model.write(self._metadata, file_path=old_file_path, - update_mtime=False) + self._write_back_updates() self._update_title_sid = None + def _write_back_updates(self): + mount_uri = self._metadata['mount_uri'] + if mount_uri == 'datastore:': + model.write(self._metadata, mount_uri, self._object_uri, + update_mtime=False) + return + + #~ if urlsplit(mount_uri)[0] != 'file': + #~ logging.error('Cannot write back metadata to %r', + #~ self._metadata['mount_uri']) + #~ return + + old_file_path = model.get_file(self._object_uri) + self._object_uri = model.write(self._metadata, mount_uri, + self._object_uri, + file_path=old_file_path, + update_mtime=False) + def get_keep(self): return int(self._metadata.get('keep', 0)) == 1 @@ -431,10 +446,10 @@ class ExpandedEntry(hippo.CanvasBox): def _icon_button_release_event_cb(self, button, event): logging.debug('_icon_button_release_event_cb') - misc.resume(self._metadata) + misc.resume(self._object_uri) return True def _preview_box_button_release_event_cb(self, button, event): logging.debug('_preview_box_button_release_event_cb') - misc.resume(self._metadata) + misc.resume(self._object_uri) return True diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py index bb1c7f6..e211e09 100644 --- a/src/jarabe/journal/journalactivity.py +++ b/src/jarabe/journal/journalactivity.py @@ -17,6 +17,7 @@ import logging from gettext import gettext as _ +from urlparse import urlsplit import uuid import gtk @@ -71,13 +72,19 @@ class JournalActivityDBusService(dbus.service.Object): logging.debug('Trying to show object %s', object_id) - if self._parent.show_object(object_id): + object_uri = 'datastore:' + object_id + if self._parent.show_object(object_uri): self._parent.reveal() def _chooser_response_cb(self, chooser, response_id, chooser_id): logging.debug('JournalActivityDBusService._chooser_response_cb') if response_id == gtk.RESPONSE_ACCEPT: - object_id = chooser.get_selected_object_id() + object_uri = chooser.get_selected_object_uri() + scheme, netloc_, object_id = urlsplit(object_uri)[:3] + if scheme != 'datastore': + raise NotImplementedError('Current API does not support' + ' non-data store entries') + self.ObjectChooserResponse(chooser_id, object_id) else: self.ObjectChooserCancelled(chooser_id) @@ -183,7 +190,7 @@ class JournalActivity(JournalWindow): search_toolbar = self._main_toolbox.search_toolbar search_toolbar.connect('query-changed', self._query_changed_cb) - search_toolbar.set_mount_point('/') + search_toolbar.set_location('datastore:') def _setup_secondary_view(self): self._secondary_view = gtk.VBox() @@ -202,8 +209,8 @@ class JournalActivity(JournalWindow): if keyname == 'Escape': self.show_main_view() - def __detail_clicked_cb(self, list_view, object_id): - self._show_secondary_view(object_id) + def __detail_clicked_cb(self, list_view, object_uri): + self._show_secondary_view(object_uri) def __clear_clicked_cb(self, list_view): self._main_toolbox.search_toolbar.clear_query() @@ -224,10 +231,10 @@ class JournalActivity(JournalWindow): self.set_canvas(self._main_view) self._main_view.show() - def _show_secondary_view(self, object_id): - metadata = model.get(object_id) + def _show_secondary_view(self, object_uri): + metadata = model.get(object_uri) try: - self._detail_toolbox.entry_toolbar.set_metadata(metadata) + self._detail_toolbox.entry_toolbar.set_object(metadata, object_uri) except Exception: logging.exception('Exception while displaying entry:') @@ -235,58 +242,60 @@ class JournalActivity(JournalWindow): self._detail_toolbox.show() try: - self._detail_view.props.metadata = metadata + self._detail_view.set_object(metadata, object_uri) except Exception: logging.exception('Exception while displaying entry:') self.set_canvas(self._secondary_view) self._secondary_view.show() - def show_object(self, object_id): - metadata = model.get(object_id) + def show_object(self, object_uri): + metadata = model.get(object_uri) if metadata is None: return False else: - self._show_secondary_view(object_id) + self._show_secondary_view(object_uri) return True - def __volume_changed_cb(self, volume_toolbar, mount_point): - logging.debug('Selected volume: %r.', mount_point) - self._main_toolbox.search_toolbar.set_mount_point(mount_point) + def __volume_changed_cb(self, volume_toolbar, mount_uri): + logging.debug('Selected volume: %r', mount_uri) + self._main_toolbox.search_toolbar.set_location(mount_uri) self._main_toolbox.set_current_toolbar(0) def __model_created_cb(self, sender, **kwargs): - self._check_for_bundle(kwargs['object_id']) + self._check_for_bundle(kwargs['object_uri']) self._main_toolbox.search_toolbar.refresh_filters() self._check_available_space() def __model_updated_cb(self, sender, **kwargs): - self._check_for_bundle(kwargs['object_id']) + object_uri = kwargs['object_uri'] + self._check_for_bundle(object_uri) if self.canvas == self._secondary_view and \ - kwargs['object_id'] == self._detail_view.props.metadata['uid']: + object_uri == self._detail_view.props.object_uri: self._detail_view.refresh() self._check_available_space() def __model_deleted_cb(self, sender, **kwargs): + object_uri = kwargs['object_uri'] if self.canvas == self._secondary_view and \ - kwargs['object_id'] == self._detail_view.props.metadata['uid']: + object_uri == self._detail_view.props.object_uri: self.show_main_view() def _focus_in_event_cb(self, window, event): self.search_grab_focus() self._list_view.update_dates() - def _check_for_bundle(self, object_id): + def _check_for_bundle(self, object_uri): registry = bundleregistry.get_registry() - metadata = model.get(object_id) + metadata = model.get(object_uri) if metadata.get('progress', '').isdigit(): if int(metadata['progress']) < 100: return - bundle = misc.get_bundle(metadata) + bundle = misc.get_bundle(object_uri, metadata) if bundle is None: return @@ -312,7 +321,7 @@ class JournalActivity(JournalWindow): return metadata['bundle_id'] = bundle.get_bundle_id() - model.write(metadata) + model.write(metadata, 'datastore:', object_uri) def search_grab_focus(self): search_toolbar = self._main_toolbox.search_toolbar diff --git a/src/jarabe/journal/journalentrybundle.py b/src/jarabe/journal/journalentrybundle.py index c220c09..8edabf1 100644 --- a/src/jarabe/journal/journalentrybundle.py +++ b/src/jarabe/journal/journalentrybundle.py @@ -53,13 +53,14 @@ class JournalEntryBundle(Bundle): try: metadata = self._read_metadata(bundle_dir) metadata['uid'] = uid + object_uri = ('datastore:' + uid) if uid else None preview = self._read_preview(temp_uid, bundle_dir) if preview is not None: metadata['preview'] = dbus.ByteArray(preview) file_path = os.path.join(bundle_dir, temp_uid) - model.write(metadata, file_path) + model.write(metadata, 'datastore:', object_uri, file_path) finally: shutil.rmtree(bundle_dir, ignore_errors=True) diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py index 2aa4153..ed6c28b 100644 --- a/src/jarabe/journal/journaltoolbox.py +++ b/src/jarabe/journal/journaltoolbox.py @@ -84,7 +84,7 @@ class SearchToolbar(gtk.Toolbar): def __init__(self): gtk.Toolbar.__init__(self) - self._mount_point = None + self._mount_uri = None self._search_entry = iconentry.IconEntry() self._search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY, @@ -179,8 +179,8 @@ class SearchToolbar(gtk.Toolbar): def _build_query(self): query = {} - if self._mount_point: - query['mountpoints'] = [self._mount_point] + if self._mount_uri: + query['mount_uri'] = self._mount_uri if self._favorite_button.props.active: query['keep'] = 1 @@ -269,8 +269,8 @@ class SearchToolbar(gtk.Toolbar): self._search_entry.activate() return False - def set_mount_point(self, mount_point): - self._mount_point = mount_point + def set_location(self, mount_uri): + self._mount_uri = mount_uri new_query = self._build_query() if self._query != new_query: self._query = new_query @@ -380,6 +380,7 @@ class EntryToolbar(gtk.Toolbar): gtk.Toolbar.__init__(self) self._metadata = None + self._object_uri = None self._temp_file_path = None self._resume = ToolButton('activity-start') @@ -415,22 +416,23 @@ class EntryToolbar(gtk.Toolbar): self.add(erase_button) erase_button.show() - def set_metadata(self, metadata): + def set_object(self, metadata, object_uri): self._metadata = metadata + self._object_uri = object_uri self._refresh_copy_palette() self._refresh_duplicate_palette() self._refresh_resume_palette() def _resume_clicked_cb(self, button): - misc.resume(self._metadata) + misc.resume(self._object_uri) def _copy_clicked_cb(self, button): button.palette.popup(immediate=True, state=Palette.SECONDARY) def _duplicate_clicked_cb(self, button): - file_path = model.get_file(self._metadata['uid']) + file_path = model.get_file(self._object_uri) try: - model.copy(self._metadata, '/') + model.copy(self._object_uri, 'datastore:') except IOError, e: logging.exception('Error while copying the entry.') self.emit('volume-error', @@ -440,13 +442,13 @@ class EntryToolbar(gtk.Toolbar): def _erase_button_clicked_cb(self, button): registry = bundleregistry.get_registry() - bundle = misc.get_bundle(self._metadata) + bundle = misc.get_bundle(self._object_uri, self._metadata) if bundle is not None and registry.is_installed(bundle): registry.uninstall(bundle) - model.delete(self._metadata['uid']) + model.delete(self._object_uri) def _resume_menu_item_activate_cb(self, menu_item, service_name): - misc.resume(self._metadata, service_name) + misc.resume(self._object_uri, service_name) def _refresh_copy_palette(self): palette = self._copy.get_palette() @@ -455,17 +457,18 @@ class EntryToolbar(gtk.Toolbar): palette.menu.remove(menu_item) menu_item.destroy() - clipboard_menu = ClipboardMenu(self._metadata) + clipboard_menu = ClipboardMenu(self._object_uri) clipboard_menu.set_image(Icon(icon_name='toolbar-edit', icon_size=gtk.ICON_SIZE_MENU)) clipboard_menu.connect('volume-error', self.__volume_error_cb) palette.menu.append(clipboard_menu) clipboard_menu.show() - if self._metadata['mountpoint'] != '/': + if self._metadata['mount_uri'] != 'datastore:': client = gconf.client_get_default() color = XoColor(client.get_string('/desktop/sugar/user/color')) - journal_menu = VolumeMenu(self._metadata, _('Journal'), '/') + journal_menu = VolumeMenu(self._object_uri, _('Journal'), + 'datastore:') journal_menu.set_image(Icon(icon_name='activity-journal', xo_color=color, icon_size=gtk.ICON_SIZE_MENU)) @@ -476,10 +479,11 @@ class EntryToolbar(gtk.Toolbar): volume_monitor = gio.volume_monitor_get() icon_theme = gtk.icon_theme_get_default() for mount in volume_monitor.get_mounts(): - if self._metadata['mountpoint'] == mount.get_root().get_path(): + mount_uri = mount.get_root().get_uri() + if self._metadata['mount_uri'] == mount_uri: continue - volume_menu = VolumeMenu(self._metadata, mount.get_name(), - mount.get_root().get_path()) + volume_menu = VolumeMenu(self._object_uri, mount.get_name(), + mount_uri) for name in mount.get_icon().props.names: if icon_theme.has_icon(name): volume_menu.set_image(Icon(icon_name=name, @@ -492,7 +496,7 @@ class EntryToolbar(gtk.Toolbar): def _refresh_duplicate_palette(self): color = misc.get_icon_color(self._metadata) self._copy.get_icon_widget().props.xo_color = color - if self._metadata['mountpoint'] == '/': + if self._metadata['mount_uri'] == 'datastore:': self._duplicate.show() icon = self._duplicate.get_icon_widget() icon.props.xo_color = color diff --git a/src/jarabe/journal/listmodel.py b/src/jarabe/journal/listmodel.py index 417ff61..9553aa5 100644 --- a/src/jarabe/journal/listmodel.py +++ b/src/jarabe/journal/listmodel.py @@ -42,7 +42,7 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): 'progress': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), } - COLUMN_UID = 0 + COLUMN_OBJECT_URI = 0 COLUMN_FAVORITE = 1 COLUMN_ICON = 2 COLUMN_ICON_COLOR = 3 @@ -56,7 +56,7 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): COLUMN_BUDDY_3 = 11 _COLUMN_TYPES = { - COLUMN_UID: str, + COLUMN_OBJECT_URI: str, COLUMN_FAVORITE: bool, COLUMN_ICON: str, COLUMN_ICON_COLOR: object, @@ -100,7 +100,10 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): self._result_set.stop() def get_metadata(self, path): - return model.get(self[path][ListModel.COLUMN_UID]) + return model.get(self[path][ListModel.COLUMN_OBJECT_URI]) + + def get_object_uri(self, path): + return self[path][ListModel.COLUMN_OBJECT_URI] def on_get_n_columns(self): return len(ListModel._COLUMN_TYPES) @@ -125,13 +128,12 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): return None self._result_set.seek(index) - metadata = self._result_set.read() + object_uri, metadata = self._result_set.read() self._last_requested_index = index - self._cached_row = [] - self._cached_row.append(metadata['uid']) + self._cached_row = [object_uri] self._cached_row.append(metadata.get('keep', '0') == '1') - self._cached_row.append(misc.get_icon_name(metadata)) + self._cached_row.append(misc.get_icon_name(object_uri, metadata)) if misc.is_activity_bundle(metadata): xo_color = XoColor('%s,%s' % (style.COLOR_BUTTON_GREY.get_svg(), @@ -178,11 +180,11 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): buddies = simplejson.loads(metadata['buddies']).values() except simplejson.decoder.JSONDecodeError, exception: logging.warning('Cannot decode buddies for %r: %s', - metadata['uid'], exception) + object_uri, exception) if not isinstance(buddies, list): logging.warning('Content of buddies for %r is not a list: %r', - metadata['uid'], buddies) + object_uri, buddies) buddies = [] for n_ in xrange(0, 3): @@ -191,7 +193,7 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): nick, color = buddies.pop(0) except (AttributeError, ValueError), exception: logging.warning('Malformed buddies for %r: %s', - metadata['uid'], exception) + object_uri, exception) else: self._cached_row.append((nick, XoColor(color))) continue @@ -229,15 +231,15 @@ class ListModel(gtk.GenericTreeModel, gtk.TreeDragSource): return None def do_drag_data_get(self, path, selection): - uid = self[path][ListModel.COLUMN_UID] + object_uri = self[path][ListModel.COLUMN_OBJECT_URI] if selection.target == 'text/uri-list': # Get hold of a reference so the temp file doesn't get deleted - self._temp_drag_file_path = model.get_file(uid) + self._temp_drag_file_path = model.get_file(object_uri) logging.debug('putting %r in selection', self._temp_drag_file_path) selection.set(selection.target, 8, self._temp_drag_file_path) return True elif selection.target == 'journal-object-id': - selection.set(selection.target, 8, uid) + selection.set(selection.target, 8, object_uri) return True return False diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py index 57836f2..30d1cb5 100644 --- a/src/jarabe/journal/listview.py +++ b/src/jarabe/journal/listview.py @@ -28,6 +28,7 @@ from sugar.graphics import style from sugar.graphics.icon import CanvasIcon, Icon, CellRendererIcon from sugar.graphics.xocolor import XoColor from sugar import util +from sugar.logger import trace from jarabe.journal.listmodel import ListModel from jarabe.journal.palettes import ObjectPalette, BuddyPalette @@ -115,24 +116,21 @@ class BaseListView(gtk.Bin): model.updated.connect(self.__model_updated_cb) model.deleted.connect(self.__model_deleted_cb) - def __model_created_cb(self, sender, signal, object_id): - if self._is_new_item_visible(object_id): + def __model_created_cb(self, sender, signal, object_uri): + if self._is_new_item_visible(object_uri): self._set_dirty() - def __model_updated_cb(self, sender, signal, object_id): - if self._is_new_item_visible(object_id): + def __model_updated_cb(self, sender, signal, object_uri): + if self._is_new_item_visible(object_uri): self._set_dirty() - def __model_deleted_cb(self, sender, signal, object_id): - if self._is_new_item_visible(object_id): + def __model_deleted_cb(self, sender, signal, object_uri): + if self._is_new_item_visible(object_uri): self._set_dirty() - def _is_new_item_visible(self, object_id): + def _is_new_item_visible(self, object_uri): """Check if the created item is part of the currently selected view""" - if self._query['mountpoints'] == ['/']: - return not object_id.startswith('/') - else: - return object_id.startswith(self._query['mountpoints'][0]) + return object_uri.startswith(self._query['mount_uri']) def _add_columns(self): cell_favorite = CellRendererFavorite(self.tree_view) @@ -254,14 +252,16 @@ class BaseListView(gtk.Bin): def __favorite_clicked_cb(self, cell, path): row = self._model[path] - metadata = model.get(row[ListModel.COLUMN_UID]) - if not model.is_editable(metadata): + object_uri = row[ListModel.COLUMN_OBJECT_URI] + metadata = model.get(object_uri) + if not model.is_editable(object_uri): return if metadata.get('keep', 0) == '1': metadata['keep'] = '0' else: metadata['keep'] = '1' - model.write(metadata, update_mtime=False) + model.write(metadata, metadata['mount_uri'], object_uri, + update_mtime=False) def update_with_query(self, query_dict): logging.debug('ListView.update_with_query') @@ -313,10 +313,10 @@ class BaseListView(gtk.Bin): if len(tree_model) == 0: if self._is_query_empty(): - if self._query['mountpoints'] == ['/']: + documents_uri = 'file://' + model.get_documents_path() + if self._query['mount_uri'] == 'datastore:': self._show_message(_('Your Journal is empty')) - elif self._query['mountpoints'] == \ - [model.get_documents_path()]: + elif self._query['mount_uri'] == documents_uri: self._show_message(_('Your documents folder is empty')) else: self._show_message(_('The device is empty')) @@ -521,31 +521,35 @@ class ListView(BaseListView): return row = self.tree_view.get_model()[path] - metadata = model.get(row[ListModel.COLUMN_UID]) - self.cell_title.props.editable = model.is_editable(metadata) + object_uri = row[ListModel.COLUMN_OBJECT_URI] + self.cell_title.props.editable = model.is_editable(object_uri) tree_view.set_cursor_on_cell(path, column, start_editing=True) + @trace() def __detail_cell_clicked_cb(self, cell, path): row = self.tree_view.get_model()[path] - self.emit('detail-clicked', row[ListModel.COLUMN_UID]) + self.emit('detail-clicked', row[ListModel.COLUMN_OBJECT_URI]) - def __detail_clicked_cb(self, cell, uid): - self.emit('detail-clicked', uid) + @trace() + def __detail_clicked_cb(self, cell, object_uri): + self.emit('detail-clicked', object_uri) def __volume_error_cb(self, cell, message, severity): self.emit('volume-error', message, severity) def __icon_clicked_cb(self, cell, path): row = self.tree_view.get_model()[path] - metadata = model.get(row[ListModel.COLUMN_UID]) - misc.resume(metadata) + misc.resume(row[ListModel.COLUMN_OBJECT_URI]) def __cell_title_edited_cb(self, cell, path, new_text): row = self._model[path] - metadata = model.get(row[ListModel.COLUMN_UID]) + object_uri = row[ListModel.COLUMN_OBJECT_URI] + metadata = model.get(object_uri) metadata['title'] = new_text - model.write(metadata, update_mtime=False) + old_path = model.get_file(object_uri) + model.write(metadata, self._query['mount_uri'], object_uri, old_path, + update_mtime=False) self.cell_title.props.editable = False def __editing_canceled_cb(self, cell): @@ -613,17 +617,18 @@ class CellRendererActivityIcon(CellRendererIcon): return None tree_model = self.tree_view.get_model() - metadata = tree_model.get_metadata(self.props.palette_invoker.path) + object_uri = tree_model.get_object_uri(self.props.palette_invoker.path) + metadata = model.get(object_uri) - palette = ObjectPalette(metadata, detail=True) + palette = ObjectPalette(metadata, object_uri, detail=True) palette.connect('detail-clicked', self.__detail_clicked_cb) palette.connect('volume-error', self.__volume_error_cb) return palette - def __detail_clicked_cb(self, palette, uid): - self.emit('detail-clicked', uid) + def __detail_clicked_cb(self, palette, object_uri): + self.emit('detail-clicked', object_uri) def __volume_error_cb(self, palette, message, severity): self.emit('volume-error', message, severity) diff --git a/src/jarabe/journal/misc.py b/src/jarabe/journal/misc.py index 39740f5..40f081c 100644 --- a/src/jarabe/journal/misc.py +++ b/src/jarabe/journal/misc.py @@ -17,6 +17,7 @@ import logging import time import os +from urlparse import urlsplit from gettext import gettext as _ import gio @@ -57,17 +58,19 @@ def _get_icon_for_mime(mime_type): return file_name -def get_icon_name(metadata): +def get_icon_name(object_uri, metadata=None): """Determine file name of icon to use for given entry""" try: - return _get_icon_name(metadata) + return _get_icon_name(object_uri, metadata) except Exception, exception: logging.warn('Could not extract icon for %r: %s', metadata, exception) return None -def _get_icon_name(metadata): +def _get_icon_name(object_uri, metadata): file_name = None + if metadata is None: + metadata = model.get(object_uri) bundle_id = metadata.get('activity', '') if not bundle_id: @@ -79,7 +82,8 @@ def _get_icon_name(metadata): file_name = activity_info.get_icon() if file_name is None and is_activity_bundle(metadata): - file_path = model.get_file(metadata['uid']) + # FIXME: handle errors (either here or in caller) + file_path = model.get_file(object_uri) if file_path is not None and os.path.exists(file_path): try: bundle = ActivityBundle(file_path) @@ -117,24 +121,24 @@ def get_date(metadata): return _('No date') -def get_bundle(metadata): +def get_bundle(object_uri, metadata): try: if is_activity_bundle(metadata): - file_path = model.get_file(metadata['uid']) + file_path = model.get_file(object_uri) if not os.path.exists(file_path): logging.warning('Invalid path: %r', file_path) return None return ActivityBundle(file_path) elif is_content_bundle(metadata): - file_path = model.get_file(metadata['uid']) + file_path = model.get_file(object_uri) if not os.path.exists(file_path): logging.warning('Invalid path: %r', file_path) return None return ContentBundle(file_path) elif is_journal_bundle(metadata): - file_path = model.get_file(metadata['uid']) + file_path = model.get_file(object_uri) if not os.path.exists(file_path): logging.warning('Invalid path: %r', file_path) return None @@ -176,14 +180,14 @@ def get_activities(metadata): return activities -def resume(metadata, bundle_id=None): +def resume(object_uri, bundle_id=None): registry = bundleregistry.get_registry() + metadata = model.get(object_uri) if is_activity_bundle(metadata) and bundle_id is None: - logging.debug('Creating activity bundle') - file_path = model.get_file(metadata['uid']) + file_path = model.get_file(object_uri) bundle = ActivityBundle(file_path) if not registry.is_installed(bundle): logging.debug('Installing activity bundle') @@ -199,10 +203,9 @@ def resume(metadata, bundle_id=None): _launch_bundle(bundle) elif is_content_bundle(metadata) and bundle_id is None: - logging.debug('Creating content bundle') - file_path = model.get_file(metadata['uid']) + file_path = model.get_file(object_uri) bundle = ContentBundle(file_path) if not bundle.is_installed(): logging.debug('Installing content bundle') @@ -219,7 +222,8 @@ def resume(metadata, bundle_id=None): activity_bundle = registry.get_bundle(activities[0].get_bundle_id()) launch(activity_bundle, uri=uri) else: - activity_id = metadata.get('activity_id', '') + activity_id = metadata.get('activity_id', + activityfactory.create_activity_id()) if bundle_id is None: activities = get_activities(metadata) @@ -231,10 +235,11 @@ def resume(metadata, bundle_id=None): bundle = registry.get_bundle(bundle_id) - if metadata.get('mountpoint', '/') == '/': - object_id = metadata['uid'] - else: - object_id = model.copy(metadata, '/') + scheme, netloc_, object_id = urlsplit(object_uri)[:3] + if scheme != 'datastore': + new_object_uri = model.copy(object_uri, 'datastore:') + scheme, netloc_, object_id = urlsplit(new_object_uri)[:3] + assert scheme == 'datastore' launch(bundle, activity_id=activity_id, object_id=object_id, color=get_icon_color(metadata)) diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py index 5eac555..4d369e2 100644 --- a/src/jarabe/journal/model.py +++ b/src/jarabe/journal/model.py @@ -17,16 +17,17 @@ import cPickle import logging import os -import errno import subprocess from datetime import datetime +import errno import time import shutil import tempfile -from stat import S_IFLNK, S_IFMT, S_IFDIR, S_IFREG import re from operator import itemgetter import simplejson +import urllib +from urlparse import urlsplit import xapian from gettext import gettext as _ @@ -36,8 +37,10 @@ import gio import gconf from sugar import dispatch +from sugar import env from sugar import mime from sugar import util +from sugar.logger import trace DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' @@ -47,7 +50,7 @@ DS_DBUS_PATH = '/org/laptop/sugar/DataStore' # Properties the journal cares about. PROPERTIES = ['activity', 'activity_id', 'buddies', 'bundle_id', 'creation_time', 'filesize', 'icon-color', 'keep', 'mime_type', - 'mountpoint', 'mtime', 'progress', 'timestamp', 'title', 'uid'] + 'mtime', 'progress', 'timestamp', 'title', 'uid'] MIN_PAGES_TO_CACHE = 3 MAX_PAGES_TO_CACHE = 5 @@ -55,11 +58,20 @@ MAX_PAGES_TO_CACHE = 5 JOURNAL_0_METADATA_DIR = '.olpc.store' JOURNAL_METADATA_DIR = '.Sugar-Metadata' +SUGAR_WEBDAV_NAMESPACE = 'http://people.sugarlabs.org/silbe/webdavns/sugar' +_SUGAR_WEBDAV_PREFIX = 'webdav::%s::' % (urllib.quote(SUGAR_WEBDAV_NAMESPACE, + safe=''), ) +_QUERY_GIO_ATTRIBUTES = ','.join(['standard::*', 'webdav::*', + gio.FILE_ATTRIBUTE_ID_FILE, + gio.FILE_ATTRIBUTE_TIME_MODIFIED]) + _datastore = None created = dispatch.Signal() updated = dispatch.Signal() deleted = dispatch.Signal() +_documents_path = None + class _Cache(object): @@ -228,21 +240,25 @@ class DatastoreResultSet(BaseResultSet): byte_arrays=True) for entry in entries: - entry['mountpoint'] = '/' + entry['mount_uri'] = 'datastore:' - return entries, total_count + return [('datastore:' + entry['uid'], entry) + for entry in entries], total_count class InplaceResultSet(BaseResultSet): """Encapsulates the result of a query on a mount point """ - def __init__(self, query, page_size, mount_point): + + _NUM_ENTRIES_PER_REQUEST = 100 + + def __init__(self, query, page_size, uri): BaseResultSet.__init__(self, query, page_size) - self._mount_point = mount_point + self._uri = uri self._file_list = None self._pending_directories = [] - self._visited_directories = [] - self._pending_files = [] + self._visited_directories = set() + self._pending_entries = [] self._stopped = False query_text = query.get('query', '') @@ -256,7 +272,7 @@ class InplaceResultSet(BaseResultSet): else: self._regex = None - if query.get('timestamp', ''): + if query.get('stamp', ''): self._date_start = int(query['timestamp']['start']) self._date_end = int(query['timestamp']['end']) else: @@ -269,14 +285,15 @@ class InplaceResultSet(BaseResultSet): def setup(self): self._file_list = [] - self._pending_directories = [self._mount_point] - self._visited_directories = [] - self._pending_files = [] - gobject.idle_add(self._scan) + self._pending_directories = [gio.File(uri=self._uri)] + self._visited_directories = set() + self._pending_entries = [] + self._schedule_scan_iteration() def stop(self): self._stopped = True + @trace() def setup_ready(self): if self._sort[1:] == 'filesize': keygetter = itemgetter(3) @@ -304,152 +321,233 @@ class InplaceResultSet(BaseResultSet): files = self._file_list[offset:offset + limit] entries = [] - for file_path, stat, mtime_, size_, metadata in files: - if metadata is None: - metadata = _get_file_metadata(file_path, stat) - metadata['mountpoint'] = self._mount_point - entries.append(metadata) + for uri, mtime_, size_, metadata in files: + metadata['mount_uri'] = self._uri + entries.append((uri, metadata)) logging.debug('InplaceResultSet.find took %f s.', time.time() - t) return entries, total_count + @trace() def _scan(self): if self._stopped: return False self.progress.send(self) - if self._pending_files: - self._scan_a_file() - return True + if self._pending_entries: + self._scan_an_entry() + return False if self._pending_directories: self._scan_a_directory() - return True + return False self.setup_ready() - self._visited_directories = [] + self._visited_directories = set() return False - def _scan_a_file(self): - full_path = self._pending_files.pop(0) + @trace() + def _scan_an_entry(self): + directory, entry = self._pending_entries.pop(0) + logging.debug('Scanning entry %r / %r', directory.get_uri(), + entry.get_name()) + + if entry.get_is_symlink(): + self._scan_a_symlink(directory, entry) + elif entry.get_file_type() == gio.FILE_TYPE_DIRECTORY: + self._scan_a_directory_entry(directory, entry) + elif entry.get_file_type() == gio.FILE_TYPE_REGULAR: + # FIXME: process file entries in bulk + self._scan_a_file(directory, entry) + + @trace() + def _scan_a_file(self, directory, entry): + self._schedule_scan_iteration() metadata = None + gfile = directory.get_child(entry.get_name()) + logging.debug('gfile.uri=%r, self._uri=%r', gfile.get_uri(), self._uri) + path = urllib.unquote(urlsplit(gfile.get_uri())[2]) - try: - stat = os.lstat(full_path) - except OSError, e: - if e.errno != errno.ENOENT: - logging.exception( - 'Error reading metadata of file %r', full_path) - return - - if S_IFMT(stat.st_mode) == S_IFLNK: - try: - link = os.readlink(full_path) - except OSError, e: - logging.exception( - 'Error reading target of link %r', full_path) - return - - if not os.path.abspath(link).startswith(self._mount_point): - return - - try: - stat = os.stat(full_path) - - except OSError, e: - if e.errno != errno.ENOENT: - logging.exception( - 'Error reading metadata of linked file %r', full_path) - return - - if S_IFMT(stat.st_mode) == S_IFDIR: - id_tuple = stat.st_ino, stat.st_dev - if not id_tuple in self._visited_directories: - self._visited_directories.append(id_tuple) - self._pending_directories.append(full_path) - return - - if S_IFMT(stat.st_mode) != S_IFREG: - return - - if self._regex is not None and \ - not self._regex.match(full_path): - metadata = _get_file_metadata(full_path, stat, + if self._regex is not None and not self._regex.match(path): + metadata = _get_file_metadata(gfile, entry, path, fetch_preview=False) if not metadata: return + add_to_list = False for f in ['fulltext', 'title', 'description', 'tags']: - if f in metadata and \ - self._regex.match(metadata[f]): + if f in metadata and self._regex.match(metadata[f]): add_to_list = True break if not add_to_list: return - if self._date_start is not None and stat.st_mtime < self._date_start: + mtime = entry.get_modification_time() + if self._date_start is not None and mtime < self._date_start: return - if self._date_end is not None and stat.st_mtime > self._date_end: + if self._date_end is not None and mtime > self._date_end: return if self._mime_types: - mime_type = gio.content_type_guess(filename=full_path) - if mime_type not in self._mime_types: + if entry.get_content_type() not in self._mime_types: return - file_info = (full_path, stat, int(stat.st_mtime), stat.st_size, - metadata) + if metadata is None: + metadata = _get_file_metadata(gfile, entry, path, + fetch_preview=False) + + self._add_a_file(directory, entry, gfile.get_uri(), metadata) + + @trace() + def _add_a_file(self, directory, entry, uri, metadata): + mtime = entry.get_modification_time() + size = entry.get_size() + file_info = (uri, int(mtime), size, metadata) self._file_list.append(file_info) - return + @trace() + def _scan_a_symlink(self, directory, entry): + link = entry.get_symlink_target() + directory_uri = directory.get_uri().rstrip('/') + '/' + absolute_uri = urllib.basejoin(directory_uri, link) + logging.debug('symlink %r in %r => %r', link, directory_uri, + absolute_uri) + if not absolute_uri.startswith(self._uri): + self._schedule_scan_iteration() + return + + gfile = gio.File(uri=absolute_uri) + gfile.query_info_async(_QUERY_GIO_ATTRIBUTES, self.__symlink_query_cb) + + @trace() + def _scan_a_directory_entry(self, parent_directory, entry): + self._schedule_scan_iteration() + directory_id = entry.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILE) + if directory_id in self._visited_directories: + logging.debug('Skipping already visited directory %r (%r)', + entry.get_name(), directory_id) + return + + logging.debug('Scheduling directory %r (%r) for scanning', + entry.get_name(), directory_id) + directory = parent_directory.get_child(entry.get_name()) + self._visited_directories.add(directory_id) + self._pending_directories.append(directory) + + @trace() def _scan_a_directory(self): - dir_path = self._pending_directories.pop(0) + directory = self._pending_directories.pop(0) + logging.debug('Scanning directory %r', directory.get_uri()) + # TODO: pass a gio.Cancellable + directory.enumerate_children_async(_QUERY_GIO_ATTRIBUTES, + self.__enumerate_children_cb) + + @trace() + def __symlink_query_cb(self, symlink, result): + try: + entry = symlink.query_info_finish(result) + except gio.Error, exception: + logging.error('Could not examine symlink %s: %s', + symlink.get_uri(), exception) + self._schedule_scan_iteration() + return + self._pending_entries.append((symlink.get_parent(), entry)) + self._schedule_scan_iteration() + + @trace() + def __enumerate_children_cb(self, directory, result): try: - entries = os.listdir(dir_path) - except OSError, e: - if e.errno != errno.EACCES: - logging.exception('Error reading directory %r', dir_path) + enumerator = directory.enumerate_children_finish(result) + except gio.Error, exception: + logging.error('Could not enumerate %s: %s', directory.get_uri(), + exception) + self._schedule_scan_iteration() return - for entry in entries: - if entry.startswith('.'): - continue - self._pending_files.append(dir_path + '/' + entry) - return + enumerator.next_files_async(self._NUM_ENTRIES_PER_REQUEST, + self.__next_files_cb) + @trace() + def __next_files_cb(self, enumerator, result): + directory = enumerator.get_container() + try: + entries = enumerator.next_files_finish(result) + except gio.Error, exception: + logging.error('Error while enumerating %s: %s', + directory.get_uri(), exception) + self._schedule_scan_iteration() + return -def _get_file_metadata(path, stat, fetch_preview=True): + logging.debug('__next_files_cb: entries=%r', entries) + logging.debug('__next_files_cb: names=%r', + [entry.get_name() for entry in entries]) + self._pending_entries += [(directory, entry) for entry in entries + if not entry.get_name().startswith('.')] + + if len(entries) >= self._NUM_ENTRIES_PER_REQUEST: + enumerator.next_files_async(self._NUM_ENTRIES_PER_REQUEST, + self.__next_files_cb) + else: + self._schedule_scan_iteration() + + @trace() + def _schedule_scan_iteration(self): + gobject.idle_add(self._scan) + + +@trace() +def _get_file_metadata(gfile, info, path, fetch_preview=True): """Return the metadata from the corresponding file. Reads the metadata stored in the json file or create the metadata based on the file properties. """ - filename = os.path.basename(path) - dir_path = os.path.dirname(path) - metadata = _get_file_metadata_from_json(dir_path, filename, fetch_preview) - if metadata: - if 'filesize' not in metadata: - metadata['filesize'] = stat.st_size - return metadata - - return {'uid': path, - 'title': os.path.basename(path), - 'timestamp': stat.st_mtime, - 'filesize': stat.st_size, - 'mime_type': gio.content_type_guess(filename=path), - 'activity': '', - 'activity_id': '', - 'icon-color': '#000000,#ffffff', - 'description': path} + if gfile.is_native(): + filename = os.path.basename(path) + dir_path = os.path.dirname(path) + metadata = _get_file_metadata_from_json(dir_path, filename, + fetch_preview) + if metadata: + metadata['filesize'] = info.get_size() + #metadata['uid'] = gfile.get_uri() + return metadata + + metadata = {#'uid': gfile.get_uri(), + 'title': info.get_display_name(), + 'timestamp': info.get_modification_time(), + 'filesize': info.get_size(), + 'mime_type': info.get_content_type(), + 'activity': '', + 'activity_id': '', + 'icon-color': '#000000,#ffffff', + 'description': path} + + for attr_name in info.list_attributes('webdav'): + if not attr_name.startswith(_SUGAR_WEBDAV_PREFIX): + continue + attribute_type = info.get_attribute_type(attr_name) + if attribute_type != gio.FILE_ATTRIBUTE_TYPE_STRING: + logging.debug('%r is not a string: %s', attr_name, attribute_type) + continue + property_name = urllib.unquote(attr_name[len(_SUGAR_WEBDAV_PREFIX):]) + if property_name == 'filesize': + continue + + metadata[property_name] = info.get_attribute_string(attr_name) + + return metadata + + +@trace() def _get_file_metadata_from_json(dir_path, filename, fetch_preview): """Read the metadata from the json file and the preview stored on the external device. @@ -475,8 +573,6 @@ def _get_file_metadata_from_json(dir_path, filename, fetch_preview): logging.error('Could not read metadata for file %r on ' 'external device.', filename) return None - else: - metadata['uid'] = os.path.join(dir_path, filename) if not fetch_preview: if 'preview' in metadata: @@ -507,15 +603,15 @@ def _get_datastore(): def _datastore_created_cb(object_id): - created.send(None, object_id=object_id) + created.send(None, object_uri='datastore:' + object_id) def _datastore_updated_cb(object_id): - updated.send(None, object_id=object_id) + updated.send(None, object_uri='datastore:' + object_id) def _datastore_deleted_cb(object_id): - deleted.send(None, object_id=object_id) + deleted.send(None, object_uri='datastore:' + object_id) def find(query_, page_size): @@ -523,61 +619,87 @@ def find(query_, page_size): """ query = query_.copy() - mount_points = query.pop('mountpoints', ['/']) - if mount_points is None or len(mount_points) != 1: - raise ValueError('Exactly one mount point must be specified') + mount_uri = query.pop('mount_uri', None) + if mount_uri != 'datastore:': + return InplaceResultSet(query, page_size, mount_uri) - if mount_points[0] == '/': - return DatastoreResultSet(query, page_size) - else: - return InplaceResultSet(query, page_size, mount_points[0]) + return DatastoreResultSet(query, page_size) -def _get_mount_point(path): - dir_path = os.path.dirname(path) - while dir_path: - if os.path.ismount(dir_path): - return dir_path - else: - dir_path = dir_path.rsplit(os.sep, 1)[0] - return None +def _get_mount_uri(gfile): + try: + mount = gfile.find_enclosing_mount() + except gio.Error, exception: + if exception.domain != gio.ERROR or \ + exception.code != gio.ERROR_NOT_FOUND: + raise + else: + return mount.get_root().get_uri() + # find_enclosing_mount() doesn't work for local "internal" mounts. + # But since the XDG Documents folder is the only thing we show that + # could contain paths on "internal" mounts, we can just "hardcode" + # that directory. + return 'file://' + get_documents_path() -def get(object_id): + +def get(object_uri): """Returns the metadata for an object """ - if os.path.exists(object_id): - stat = os.stat(object_id) - metadata = _get_file_metadata(object_id, stat) - metadata['mountpoint'] = _get_mount_point(object_id) - else: + logging.debug('get(%r)', object_uri) + scheme, netloc_, quoted_path = urlsplit(object_uri)[:3] + if scheme == 'datastore': + object_id = quoted_path metadata = _get_datastore().get_properties(object_id, byte_arrays=True) - metadata['mountpoint'] = '/' + metadata['mount_uri'] = 'datastore:' + else: + gfile = gio.File(uri=object_uri) + info = gfile.query_info(_QUERY_GIO_ATTRIBUTES) + path = urllib.unquote(quoted_path) + metadata = _get_file_metadata(gfile, info, path) + metadata['mount_uri'] = _get_mount_uri(gfile) return metadata -def get_file(object_id): +@trace() +def get_file(object_uri): """Returns the file for an object """ - if os.path.exists(object_id): - logging.debug('get_file asked for file with path %r', object_id) - return object_id - else: - logging.debug('get_file asked for entry with id %r', object_id) + # TODO: add async interface + logging.debug('get_file(%r)', object_uri) + scheme, netloc_, quoted_path = urlsplit(object_uri)[:3] + if scheme == 'file': + # We use local files as-is, so no pruning / book-keeping required. + return gio.File(uri=object_uri).get_path() + + if scheme == 'datastore': + object_id = quoted_path file_path = _get_datastore().get_filename(object_id) - if file_path: - return util.TempFilePath(file_path) - else: - return None + else: + input_stream = gio.File(uri=object_uri).read() + file_path = tempfile.mktemp(dir=env.get_profile_path('data')) + output_stream = gio.File(file_path).create(gio.FILE_CREATE_PRIVATE) + shutil.copyfileobj(input_stream, output_stream) + input_stream.close() + output_stream.close() + if file_path: + return util.TempFilePath(file_path) + else: + return None -def get_file_size(object_id): + +def get_file_size(object_uri): """Return the file size for an object """ - logging.debug('get_file_size %r', object_id) - if os.path.exists(object_id): - return os.stat(object_id).st_size - + logging.debug('get_file_size(%r)', object_uri) + scheme, netloc_, quoted_path = urlsplit(object_uri)[:3] + if scheme != 'datastore': + gfile = gio.File(uri=object_uri) + info = gfile.query_info(gio.FILE_ATTRIBUTE_STANDARD_SIZE) + return info.get_attribute_uint64(gio.FILE_ATTRIBUTE_STANDARD_SIZE) + + object_id = quoted_path file_path = _get_datastore().get_filename(object_id) if file_path: size = os.stat(file_path).st_size @@ -594,68 +716,148 @@ def get_unique_values(key): return _get_datastore().get_uniquevaluesfor(key, empty_dict) -def delete(object_id): +def delete(object_uri): """Removes an object from persistent storage """ - if not os.path.exists(object_id): + scheme, netloc_, quoted_path = urlsplit(object_uri)[:3] + if scheme == 'datastore': + object_id = quoted_path _get_datastore().delete(object_id) - else: - os.unlink(object_id) - dir_path = os.path.dirname(object_id) - filename = os.path.basename(object_id) - old_files = [os.path.join(dir_path, JOURNAL_METADATA_DIR, - filename + '.metadata'), - os.path.join(dir_path, JOURNAL_METADATA_DIR, - filename + '.preview')] - for old_file in old_files: - if os.path.exists(old_file): - try: - os.unlink(old_file) - except EnvironmentError: - logging.error('Could not remove metadata=%s ' - 'for file=%s', old_file, filename) - deleted.send(None, object_id=object_id) + return + + gfile = gio.File(uri=object_uri) + gfile.delete() + if gfile.get_uri_scheme() == 'file': + _delete_metadata_files(gfile.get_path()) + + deleted.send(None, object_uri=object_uri) + + +def _delete_metadata_files(path): + """Delete Sugar metadata files associated with the given data file""" + dir_path = os.path.dirname(path) + filename = os.path.basename(path) + old_files = [os.path.join(dir_path, JOURNAL_METADATA_DIR, + filename + '.metadata'), + os.path.join(dir_path, JOURNAL_METADATA_DIR, + filename + '.preview')] + for old_file in old_files: + if os.path.exists(old_file): + try: + os.unlink(old_file) + except EnvironmentError: + logging.error('Could not remove metadata=%s ' + 'for file=%s', old_file, filename) -def copy(metadata, mount_point): +def copy(object_uri, mount_uri): """Copies an object to another mount point """ - metadata = get(metadata['uid']) - if mount_point == '/' and metadata['icon-color'] == '#000000,#ffffff': + metadata = get(object_uri) + color = metadata.get('icon-color') + if mount_uri == 'datastore:' and color == '#000000,#ffffff': client = gconf.client_get_default() metadata['icon-color'] = client.get_string('/desktop/sugar/user/color') - file_path = get_file(metadata['uid']) + file_path = get_file(object_uri) if file_path is None: file_path = '' - metadata['mountpoint'] = mount_point - del metadata['uid'] + metadata['mount_uri'] = mount_uri + metadata.pop('uid', None) - return write(metadata, file_path, transfer_ownership=False) + return write(metadata, mount_uri, None, file_path, + transfer_ownership=False) -def write(metadata, file_path='', update_mtime=True, transfer_ownership=True): - """Creates or updates an entry for that id +# FIXME: pass target uri (mount_uri) as parameter +@trace() +def write(metadata, mount_uri, object_uri=None, file_path='', + update_mtime=True, transfer_ownership=True): + """Create or update an entry on mounted object storage + + If object_uri is None, create a new entry, otherwise update an + existing entry. + + If update_mtime is True (default), update the time of last + modification in metadata (in-place). + + If transfer_ownership is True (default), move file_path into place. + If that is not possible, remove file_path after copying. + + For objects on POSIX file systems, the title property may be + updated in metadata (in-place). + + Return the URI of the written entry. Even for updates this may be + different from the original value. """ - logging.debug('model.write %r %r %r', metadata.get('uid', ''), file_path, + logging.debug('model.write %r %r %r', object_uri, file_path, update_mtime) + + assert mount_uri + if object_uri and not object_uri.startswith(mount_uri): + raise ValueError('Object to be updated not located on given mount.') + if update_mtime: metadata['mtime'] = datetime.now().isoformat() metadata['timestamp'] = int(time.time()) - if metadata.get('mountpoint', '/') == '/': - if metadata.get('uid', ''): - object_id = metadata['uid'] - _get_datastore().update(object_id, dbus.Dictionary(metadata), - file_path, transfer_ownership) - else: - object_id = _get_datastore().create(dbus.Dictionary(metadata), - file_path, - transfer_ownership) + if mount_uri == 'datastore:': + return _write_entry_to_datastore(object_uri, metadata, file_path, + transfer_ownership) + elif mount_uri.startswith('dav'): + return _write_entry_on_webdav_share(mount_uri, object_uri, metadata, + file_path, transfer_ownership) + elif mount_uri.startswith('file:'): + return _write_entry_on_external_device(mount_uri, object_uri, + metadata, file_path, + transfer_ownership) else: - object_id = _write_entry_on_external_device(metadata, file_path) + raise NotImplementedError("Don't know how to write to" + " %r" % (mount_uri, )) + + +def _write_entry_to_datastore(object_uri, metadata, file_path, + transfer_ownership): + if object_uri: + object_id = urlsplit(object_uri)[2] + _get_datastore().update(object_id, dbus.Dictionary(metadata), + file_path, transfer_ownership) + return object_uri + else: + object_id = _get_datastore().create(dbus.Dictionary(metadata), + file_path, transfer_ownership) + return 'datastore:' + object_id + + +def _write_entry_on_webdav_share(mount_uri, object_uri, metadata, file_path, + transfer_ownership): + title = metadata.get('title') or _('Untitled') + proposed_name = get_file_name(title, metadata.get('mime_type', '')) + output_stream, gfile = create_unique_file(mount_uri, proposed_name) + if file_path: + shutil.copyfileobj(file(file_path), output_stream) + + output_stream.close() + + info = gio.FileInfo() + attr_infos = gfile.query_settable_attributes() or [] + settable_attrs = [attr_info.name for attr_info in attr_infos] + if gio.FILE_ATTRIBUTE_TIME_MODIFIED in settable_attrs: + info.set_modification_time(metadata.get('timestamp', time.time())) + if gio.FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE in settable_attrs: + info.set_content_type(metadata.get('mime_type', + 'application/octet-stream')) + + if 'webdav' in gfile.query_writable_namespaces(): + for name, value in metadata.items(): + attr_name = _SUGAR_WEBDAV_PREFIX + urllib.quote(name) + if isinstance(value, basestring) and '\0' in value: + # strings with NUL bytes are not supported by gio + continue + + info.set_attribute_string(attr_name, str(value)) - return object_id + gfile.set_attributes_from_info(info) def _rename_entry_on_external_device(file_path, destination_path, @@ -678,9 +880,9 @@ def _rename_entry_on_external_device(file_path, destination_path, 'for file=%s', ofile, old_fname) -def _write_entry_on_external_device(metadata, file_path): - """Create and update an entry copied from the - DS to an external storage device. +def _write_entry_on_external_device(mount_uri, object_uri, metadata, + file_path, transfer_ownership): + """Create or update an entry on a mounted POSIX file system Besides copying the associated file a file for the preview and one for the metadata are stored in the hidden directory @@ -691,66 +893,66 @@ def _write_entry_on_external_device(metadata, file_path): handled failsafe. """ - if 'uid' in metadata and os.path.exists(metadata['uid']): - file_path = metadata['uid'] - if not file_path or not os.path.exists(file_path): raise ValueError('Entries without a file cannot be copied to ' 'removable devices') if not metadata.get('title'): metadata['title'] = _('Untitled') - file_name = get_file_name(metadata['title'], metadata['mime_type']) + destination_name = get_file_name(metadata['title'], metadata['mime_type']) - destination_path = os.path.join(metadata['mountpoint'], file_name) + mount_point = gio.File(mount_uri).get_path() + logging.debug('_write_entry_on_external_device: mount_point=%r,' + ' destination_name=%r', mount_point, destination_name) + destination_path = os.path.join(mount_point, destination_name) if destination_path != file_path: - file_name = get_unique_file_name(metadata['mountpoint'], file_name) - destination_path = os.path.join(metadata['mountpoint'], file_name) - clean_name, extension_ = os.path.splitext(file_name) + destination_name = get_unique_file_name(mount_point, destination_name) + destination_path = os.path.join(mount_point, destination_name) + clean_name, extension_ = os.path.splitext(destination_name) metadata['title'] = clean_name metadata_copy = metadata.copy() - metadata_copy.pop('mountpoint', None) + metadata_copy.pop('mount_uri') metadata_copy.pop('uid', None) metadata_copy.pop('filesize', None) - metadata_dir_path = os.path.join(metadata['mountpoint'], + metadata_dir_path = os.path.join(mount_point, JOURNAL_METADATA_DIR) if not os.path.exists(metadata_dir_path): os.mkdir(metadata_dir_path) - preview = None - if 'preview' in metadata_copy: - preview = metadata_copy['preview'] - preview_fname = file_name + '.preview' - metadata_copy.pop('preview', None) + preview = metadata_copy.pop('preview', None) + if preview: + preview_fname = destination_name + '.preview' try: metadata_json = simplejson.dumps(metadata_copy) except (UnicodeDecodeError, EnvironmentError): logging.error('Could not convert metadata to json.') else: - (fh, fn) = tempfile.mkstemp(dir=metadata['mountpoint']) + (fh, fn) = tempfile.mkstemp(dir=mount_point) os.write(fh, metadata_json) os.close(fh) - os.rename(fn, os.path.join(metadata_dir_path, file_name + '.metadata')) + os.rename(fn, os.path.join(metadata_dir_path, + destination_name + '.metadata')) if preview: - (fh, fn) = tempfile.mkstemp(dir=metadata['mountpoint']) + (fh, fn) = tempfile.mkstemp(dir=mount_point) os.write(fh, preview) os.close(fh) os.rename(fn, os.path.join(metadata_dir_path, preview_fname)) - if not os.path.dirname(destination_path) == os.path.dirname(file_path): + if not transfer_ownership: shutil.copy(file_path, destination_path) - else: + elif gio.File(object_uri).get_path() == file_path: _rename_entry_on_external_device(file_path, destination_path, metadata_dir_path) + else: + shutil.move(file_path, destination_path) - object_id = destination_path - created.send(None, object_id=object_id) - - return object_id + object_uri = gio.File(path=destination_path).get_uri() + created.send(None, object_uri=object_uri) + return object_uri def get_file_name(title, mime_type): @@ -791,11 +993,42 @@ def get_unique_file_name(mount_point, file_name): return file_name -def is_editable(metadata): - if metadata.get('mountpoint', '/') == '/': - return True - else: - return os.access(metadata['mountpoint'], os.W_OK) +@trace() +def create_unique_file(mount_uri, file_name): + """Create a new file with a unique name on given mount + + Create a new file on the mount identified by mount_uri. Use + file_name as the name on the mount if possible, otherwise use it + as a template for the name of the new file, inserting a decimal + number as necessary. Return both the gio.FileOutputStream instance + and the gio.File instance of the new object (in this order). + """ + gdir = gio.File(mount_uri) + name, extension = os.path.splitext(file_name) + last_error = None + for try_nr in range(255): + if not try_nr: + try_name = file_name + else: + try_name = '%s_%d%s' % (name, try_nr, extension) + + gfile = gdir.get_child(try_name) + try: + output_stream = gfile.create() + except gio.Error, exception: + last_error = exception + continue + + return output_stream, gfile + + raise last_error + +@trace() +def is_editable(object_uri): + scheme, netloc_, quoted_path = urlsplit(object_uri)[:3] + if scheme == 'file': + return os.access(urllib.unquote(quoted_path), os.W_OK) + return scheme in ['datastore', 'dav', 'davs'] def get_documents_path(): @@ -807,13 +1040,18 @@ def get_documents_path(): Returns: Path to $HOME/DOCUMENTS or None if an error occurs """ + global _documents_path + if _documents_path is not None: + return _documents_path + try: pipe = subprocess.Popen(['xdg-user-dir', 'DOCUMENTS'], stdout=subprocess.PIPE) documents_path = os.path.normpath(pipe.communicate()[0].strip()) if os.path.exists(documents_path) and \ os.environ.get('HOME') != documents_path: - return documents_path + _documents_path = documents_path + return _documents_path except OSError, exception: if exception.errno != errno.ENOENT: logging.exception('Could not run xdg-user-dir') diff --git a/src/jarabe/journal/objectchooser.py b/src/jarabe/journal/objectchooser.py index ecb8ecf..2fceb96 100644 --- a/src/jarabe/journal/objectchooser.py +++ b/src/jarabe/journal/objectchooser.py @@ -45,7 +45,7 @@ class ObjectChooser(gtk.Window): self.set_position(gtk.WIN_POS_CENTER_ALWAYS) self.set_border_width(style.LINE_WIDTH) - self._selected_object_id = None + self._selected_object_uri = None self.add_events(gtk.gdk.VISIBILITY_NOTIFY_MASK) self.connect('visibility-notify-event', @@ -88,7 +88,7 @@ class ObjectChooser(gtk.Window): vbox.pack_start(self._list_view) self._list_view.show() - self._toolbar.set_mount_point('/') + self._toolbar.set_location('datastore:') width = gtk.gdk.screen_width() - style.GRID_CELL_SIZE * 2 height = gtk.gdk.screen_height() - style.GRID_CELL_SIZE * 2 @@ -105,8 +105,8 @@ class ObjectChooser(gtk.Window): if window.get_xid() == parent.xid: self.destroy() - def __entry_activated_cb(self, list_view, uid): - self._selected_object_id = uid + def __entry_activated_cb(self, list_view, object_uri): + self._selected_object_uri = object_uri self.emit('response', gtk.RESPONSE_ACCEPT) def __delete_event_cb(self, chooser, event): @@ -120,15 +120,15 @@ class ObjectChooser(gtk.Window): def __close_button_clicked_cb(self, button): self.emit('response', gtk.RESPONSE_DELETE_EVENT) - def get_selected_object_id(self): - return self._selected_object_id + def get_selected_object_uri(self): + return self._selected_object_uri def __query_changed_cb(self, toolbar, query): self._list_view.update_with_query(query) - def __volume_changed_cb(self, volume_toolbar, mount_point): - logging.debug('Selected volume: %r.', mount_point) - self._toolbar.set_mount_point(mount_point) + def __volume_changed_cb(self, volume_toolbar, mount_uri): + logging.debug('Selected volume: %r', mount_uri) + self._toolbar.set_location(mount_uri) def __visibility_notify_event_cb(self, window, event): logging.debug('visibility_notify_event_cb %r', self) @@ -193,7 +193,7 @@ class ChooserListView(BaseListView): return False path, column_, x_, y_ = pos - uid = tree_view.get_model()[path][ListModel.COLUMN_UID] - self.emit('entry-activated', uid) + object_uri = tree_view.get_model()[path][ListModel.COLUMN_OBJECT_URI] + self.emit('entry-activated', object_uri) return False diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py index 8fc1e5d..530cd4b 100644 --- a/src/jarabe/journal/palettes.py +++ b/src/jarabe/journal/palettes.py @@ -49,12 +49,13 @@ class ObjectPalette(Palette): ([str, str])), } - def __init__(self, metadata, detail=False): + def __init__(self, metadata, object_uri, detail=False): self._metadata = metadata + self._object_uri = object_uri activity_icon = Icon(icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) - activity_icon.props.file = misc.get_icon_name(metadata) + activity_icon.props.file = misc.get_icon_name(object_uri, metadata) color = misc.get_icon_color(metadata) activity_icon.props.xo_color = color @@ -81,7 +82,7 @@ class ObjectPalette(Palette): menu_item = MenuItem(resume_with_label, 'activity-start') self.menu.append(menu_item) menu_item.show() - start_with_menu = StartWithMenu(self._metadata) + start_with_menu = StartWithMenu(metadata, object_uri) menu_item.set_submenu(start_with_menu) else: @@ -96,11 +97,11 @@ class ObjectPalette(Palette): menu_item.set_image(icon) self.menu.append(menu_item) menu_item.show() - copy_menu = CopyMenu(metadata) + copy_menu = CopyMenu(metadata, object_uri) copy_menu.connect('volume-error', self.__volume_error_cb) menu_item.set_submenu(copy_menu) - if self._metadata['mountpoint'] == '/': + if self._metadata['mount_uri'] == 'datastore:': menu_item = MenuItem(_('Duplicate')) icon = Icon(icon_name='edit-duplicate', xo_color=color, icon_size=gtk.ICON_SIZE_MENU) @@ -129,12 +130,11 @@ class ObjectPalette(Palette): menu_item.show() def __start_activate_cb(self, menu_item): - misc.resume(self._metadata) + misc.resume(self._object_uri) def __duplicate_activate_cb(self, menu_item): - file_path = model.get_file(self._metadata['uid']) try: - model.copy(self._metadata, '/') + model.copy(self._object_uri, 'datastore:') except IOError, e: logging.exception('Error while copying the entry. %s', e.strerror) self.emit('volume-error', @@ -142,17 +142,17 @@ class ObjectPalette(Palette): _('Error')) def __erase_activate_cb(self, menu_item): - model.delete(self._metadata['uid']) + model.delete(self._object_uri) def __detail_activate_cb(self, menu_item): - self.emit('detail-clicked', self._metadata['uid']) + self.emit('detail-clicked', self._object_uri) def __volume_error_cb(self, menu_item, message, severity): self.emit('volume-error', message, severity) def __friend_selected_cb(self, menu_item, buddy): logging.debug('__friend_selected_cb') - file_name = model.get_file(self._metadata['uid']) + file_name = model.get_file(self._object_uri) if not file_name or not os.path.exists(file_name): logging.warn('Entries without a file cannot be sent.') @@ -180,22 +180,24 @@ class CopyMenu(gtk.Menu): ([str, str])), } - def __init__(self, metadata): + def __init__(self, metadata, object_uri): gobject.GObject.__init__(self) self._metadata = metadata + self._object_uri = object_uri - clipboard_menu = ClipboardMenu(self._metadata) + clipboard_menu = ClipboardMenu(self._object_uri) clipboard_menu.set_image(Icon(icon_name='toolbar-edit', icon_size=gtk.ICON_SIZE_MENU)) clipboard_menu.connect('volume-error', self.__volume_error_cb) self.append(clipboard_menu) clipboard_menu.show() - if self._metadata['mountpoint'] != '/': + if self._metadata['mount_uri'] != 'datastore:': client = gconf.client_get_default() color = XoColor(client.get_string('/desktop/sugar/user/color')) - journal_menu = VolumeMenu(self._metadata, _('Journal'), '/') + journal_menu = VolumeMenu(self._object_uri, _('Journal'), + 'datastore:') journal_menu.set_image(Icon(icon_name='activity-journal', xo_color=color, icon_size=gtk.ICON_SIZE_MENU)) @@ -206,10 +208,11 @@ class CopyMenu(gtk.Menu): volume_monitor = gio.volume_monitor_get() icon_theme = gtk.icon_theme_get_default() for mount in volume_monitor.get_mounts(): - if self._metadata['mountpoint'] == mount.get_root().get_path(): + mount_uri = mount.get_root().get_uri() + if self._metadata['mount_uri'] == mount_uri: continue - volume_menu = VolumeMenu(self._metadata, mount.get_name(), - mount.get_root().get_path()) + volume_menu = VolumeMenu(self._object_uri, mount.get_name(), + mount_uri) for name in mount.get_icon().props.names: if icon_theme.has_icon(name): volume_menu.set_image(Icon(icon_name=name, @@ -231,13 +234,13 @@ class VolumeMenu(MenuItem): ([str, str])), } - def __init__(self, metadata, label, mount_point): + def __init__(self, object_uri, label, mount_uri): MenuItem.__init__(self, label) - self._metadata = metadata - self.connect('activate', self.__copy_to_volume_cb, mount_point) + self._object_uri = object_uri + self.connect('activate', self.__copy_to_volume_cb, mount_uri) - def __copy_to_volume_cb(self, menu_item, mount_point): - file_path = model.get_file(self._metadata['uid']) + def __copy_to_volume_cb(self, menu_item, mount_uri): + file_path = model.get_file(self._object_uri) if not file_path or not os.path.exists(file_path): logging.warn('Entries without a file cannot be copied.') @@ -247,7 +250,7 @@ class VolumeMenu(MenuItem): return try: - model.copy(self._metadata, mount_point) + model.copy(self._object_uri, mount_uri) except IOError, e: logging.exception('Error while copying the entry. %s', e.strerror) self.emit('volume-error', @@ -263,15 +266,15 @@ class ClipboardMenu(MenuItem): ([str, str])), } - def __init__(self, metadata): + def __init__(self, object_uri): MenuItem.__init__(self, _('Clipboard')) self._temp_file_path = None - self._metadata = metadata + self._object_uri = object_uri self.connect('activate', self.__copy_to_clipboard_cb) def __copy_to_clipboard_cb(self, menu_item): - file_path = model.get_file(self._metadata['uid']) + file_path = model.get_file(self._object_uri) if not file_path or not os.path.exists(file_path): logging.warn('Entries without a file cannot be copied.') self.emit('volume-error', @@ -286,7 +289,7 @@ class ClipboardMenu(MenuItem): def __clipboard_get_func_cb(self, clipboard, selection_data, info, data): # Get hold of a reference so the temp file doesn't get deleted - self._temp_file_path = model.get_file(self._metadata['uid']) + self._temp_file_path = model.get_file(self._object_uri) logging.debug('__clipboard_get_func_cb %r', self._temp_file_path) selection_data.set_uris(['file://' + self._temp_file_path]) @@ -336,9 +339,10 @@ class FriendsMenu(gtk.Menu): class StartWithMenu(gtk.Menu): __gtype_name__ = 'JournalStartWithMenu' - def __init__(self, metadata): + def __init__(self, metadata, object_uri): gobject.GObject.__init__(self) + self._object_uri = object_uri self._metadata = metadata for activity_info in misc.get_activities(metadata): @@ -365,7 +369,7 @@ class StartWithMenu(gtk.Menu): if mime_type: mime_registry = mimeregistry.get_registry() mime_registry.set_default_activity(mime_type, service_name) - misc.resume(self._metadata, service_name) + misc.resume(self._object_uri, service_name) class BuddyPalette(Palette): diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py index 1c30281..368d287 100644 --- a/src/jarabe/journal/volumestoolbar.py +++ b/src/jarabe/journal/volumestoolbar.py @@ -17,6 +17,7 @@ import logging import os import statvfs +from urlparse import urlsplit from gettext import gettext as _ import gobject @@ -39,7 +40,7 @@ class VolumesToolbar(gtk.Toolbar): __gsignals__ = { 'volume-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([str])), + [str]), 'volume-error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([str, str])), } @@ -100,12 +101,10 @@ class VolumesToolbar(gtk.Toolbar): def _add_button(self, mount): logging.debug('VolumeToolbar._add_button: %r', mount.get_name()) - if os.path.exists(os.path.join(mount.get_root().get_path(), - model.JOURNAL_0_METADATA_DIR)): - logging.debug('Convert DS-0 Journal entries: starting conversion') - gobject.idle_add(model.convert_datastore_0_entries, - mount.get_root().get_path()) + #~ if mount.get_root().get_path(): + #~ return self._add_local_button(mount) + #~ def _add_local_button(self, mount): button = VolumeButton(mount) button.props.group = self._volume_buttons[0] button.connect('toggled', self._button_toggled_cb) @@ -119,12 +118,24 @@ class VolumesToolbar(gtk.Toolbar): if len(self.get_children()) > 1: self.show() + mount_point = mount.get_root().get_path() + if not mount_point: + return + + if urlsplit(mount.get_root().get_uri())[0] != 'file': + return + + if os.path.exists(os.path.join(mount_point, + model.JOURNAL_0_METADATA_DIR)): + logging.debug('Convert DS-0 Journal entries: starting conversion') + gobject.idle_add(model.convert_datastore_0_entries, mount_point) + def __volume_error_cb(self, button, strerror, severity): self.emit('volume-error', strerror, severity) def _button_toggled_cb(self, button): if button.props.active: - self.emit('volume-changed', button.mount_point) + self.emit('volume-changed', button.mount_uri) def _unmount_activated_cb(self, menu_item, mount): logging.debug('VolumesToolbar._unmount_activated_cb: %r', mount) @@ -134,11 +145,11 @@ class VolumesToolbar(gtk.Toolbar): logging.debug('__unmount_cb %r %r', source, result) def _get_button_for_mount(self, mount): - mount_point = mount.get_root().get_path() + uri = mount.get_root().get_uri() for button in self.get_children(): - if button.mount_point == mount_point: + if button.mount_uri == uri: return button - logging.error('Couldnt find button with mount_point %r', mount_point) + logging.error('Could not find button with uri %r', uri) return None def _remove_button(self, mount): @@ -161,10 +172,10 @@ class BaseButton(RadioToolButton): ([str, str])), } - def __init__(self, mount_point): + def __init__(self, mount_uri): RadioToolButton.__init__(self) - self.mount_point = mount_point + self.mount_uri = mount_uri self.drag_dest_set(gtk.DEST_DEFAULT_ALL, [('journal-object-id', 0, 0)], @@ -173,9 +184,8 @@ class BaseButton(RadioToolButton): def _drag_data_received_cb(self, widget, drag_context, x, y, selection_data, info, timestamp): - object_id = selection_data.data - metadata = model.get(object_id) - file_path = model.get_file(metadata['uid']) + object_uri = selection_data.data + file_path = model.get_file(object_uri) if not file_path or not os.path.exists(file_path): logging.warn('Entries without a file cannot be copied.') self.emit('volume-error', @@ -184,7 +194,7 @@ class BaseButton(RadioToolButton): return try: - model.copy(metadata, self.mount_point) + model.copy(object_uri, self.mount_uri) except IOError, e: logging.exception('Error while copying the entry. %s', e.strerror) self.emit('volume-error', @@ -195,8 +205,7 @@ class BaseButton(RadioToolButton): class VolumeButton(BaseButton): def __init__(self, mount): self._mount = mount - mount_point = mount.get_root().get_path() - BaseButton.__init__(self, mount_point) + BaseButton.__init__(self, mount.get_root().get_uri()) icon_name = None icon_theme = gtk.icon_theme_get_default() @@ -225,7 +234,7 @@ class VolumeButton(BaseButton): class JournalButton(BaseButton): def __init__(self): - BaseButton.__init__(self, mount_point='/') + BaseButton.__init__(self, 'datastore:') self.props.named_icon = 'activity-journal' @@ -271,7 +280,8 @@ class JournalButtonPalette(Palette): class DocumentsButton(BaseButton): def __init__(self, documents_path): - BaseButton.__init__(self, mount_point=documents_path) + uri = 'file://' + documents_path + BaseButton.__init__(self, uri) self.props.named_icon = 'user-documents' diff --git a/src/jarabe/view/palettes.py b/src/jarabe/view/palettes.py index 6339882..1051c76 100644 --- a/src/jarabe/view/palettes.py +++ b/src/jarabe/view/palettes.py @@ -208,8 +208,12 @@ class VolumePalette(Palette): Palette.__init__(self, label=mount.get_name()) self._mount = mount - path = mount.get_root().get_path() - self.props.secondary_text = glib.markup_escape_text(path) + mount_point = mount.get_root().get_path() + if mount_point: + label = mount_point + else: + label = mount.get_root().get_uri() + self.props.secondary_text = glib.markup_escape_text(label) self._content_vbox = gtk.VBox() self._content_vbox.show() @@ -223,7 +227,8 @@ class VolumePalette(Palette): self._content_vbox.add(self._free_space_label) self._free_space_label.show() - self.connect('popup', self.__popup_cb) + if mount_point: + self.connect('popup', self.__popup_cb) menu_item = MenuItem(pgettext('Volume', 'Remove')) -- cgit v0.9.1