Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSascha Silbe <silbe@activitycentral.com>2011-06-19 20:53:44 (GMT)
committer Sascha Silbe <silbe@activitycentral.com>2011-12-20 12:50:23 (GMT)
commit899508423642c284b1a42559aba8fd3be4bdd914 (patch)
treecd7c3860ec433ffe6123bc673ee0519151a30834
parent0767d9810f53635fc8004574589055ec68d52323 (diff)
WIP patch for remote mountswebdav-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
-rw-r--r--src/jarabe/desktop/favoritesview.py71
-rw-r--r--src/jarabe/frame/clipboardmenu.py10
-rw-r--r--src/jarabe/journal/detailview.py17
-rw-r--r--src/jarabe/journal/expandedentry.py57
-rw-r--r--src/jarabe/journal/journalactivity.py55
-rw-r--r--src/jarabe/journal/journalentrybundle.py3
-rw-r--r--src/jarabe/journal/journaltoolbox.py42
-rw-r--r--src/jarabe/journal/listmodel.py28
-rw-r--r--src/jarabe/journal/listview.py65
-rw-r--r--src/jarabe/journal/misc.py41
-rw-r--r--src/jarabe/journal/model.py674
-rw-r--r--src/jarabe/journal/objectchooser.py22
-rw-r--r--src/jarabe/journal/palettes.py64
-rw-r--r--src/jarabe/journal/volumestoolbar.py50
-rw-r--r--src/jarabe/view/palettes.py11
15 files changed, 759 insertions, 451 deletions
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'))