Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSascha Silbe <sascha-pgp@silbe.org>2010-08-30 09:18:29 (GMT)
committer Sascha Silbe <sascha-pgp@silbe.org>2010-08-30 09:18:29 (GMT)
commitbc917b35e8988f97dd4689b2e7064c6654060570 (patch)
treeea13afc1a52a323853c83151a0ddf6cafd466e50
parentf1eb9444dd3e8f4beb338b8d0cc88aa750630095 (diff)
Revert "create Restore activity based on Backup"
This reverts commit f1eb9444dd3e8f4beb338b8d0cc88aa750630095. Will split this up into two commits so the git rename-detection logic hopefully works better.
-rw-r--r--activity/Backup_icon.svg102
-rw-r--r--activity/Restore_icon.svg30
-rw-r--r--activity/activity.info12
-rw-r--r--activity/mimetypes.xml7
-rw-r--r--backup.py730
-rw-r--r--icons/journal-export.svg100
-rw-r--r--icons/journal-import.svg30
-rw-r--r--restore.py577
8 files changed, 938 insertions, 650 deletions
diff --git a/activity/Backup_icon.svg b/activity/Backup_icon.svg
new file mode 100644
index 0000000..e72ef90
--- /dev/null
+++ b/activity/Backup_icon.svg
@@ -0,0 +1,102 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#666666">
+ <!ENTITY fill_color "#FFFFFF">
+]>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ id="Icon"
+ width="44.365"
+ height="42.918"
+ viewBox="0 0 44.365 42.918"
+ overflow="visible"
+ enable-background="new 0 0 44.365 42.918"
+ xml:space="preserve"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="stock-open.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"><metadata
+ id="metadata26"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs24"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 21.459 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="44.365002 : 21.459 : 1"
+ inkscape:persp3d-origin="22.182501 : 14.306 : 1"
+ id="perspective28" /></defs><sodipodi:namedview
+ inkscape:window-height="975"
+ inkscape:window-width="1680"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="false"
+ inkscape:zoom="13.563637"
+ inkscape:cx="22.182502"
+ inkscape:cy="28.758498"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:current-layer="Icon" />
+
+
+
+<g
+ id="g2414"
+ transform="translate(9.0634189,-3.9384509)"><g
+ transform="translate(2.6055351e-2,5.183866e-2)"
+ id="g2405"><g
+ id="g3"
+ transform="matrix(0.736868,0,0,0.7153409,0,12.146464)">
+ <g
+ id="g5">
+ <path
+ d="M 1.75,41.168 L 23.673,41.168 C 25.663,41.168 26.749,39.873 26.749,38.09 L 26.749,13.699 C 26.749,12.152 25.2,10.622 23.673,10.622 L 19.75,10.622"
+ id="path7"
+ style="fill:none;stroke:&stroke_color;;stroke-width:3.5;stroke-linejoin:round" />
+ </g>
+</g><g
+ id="g9"
+ transform="matrix(0.6976744,0,0,0.7277711,0,11.605042)">
+ <g
+ id="g11">
+ <path
+ d="M 19.75,31.859 C 19.75,33.502 18.548,34.723 16.673,35.478 L 1.75,41.168 L 1.75,10.623 L 16.673,2.935 C 18.664,2.583 19.75,3.686 19.75,5.469 L 19.75,31.859 z"
+ id="path13"
+ style="fill:none;stroke:&stroke_color;;stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round" />
+ </g>
+</g><line
+ x1="5.5922894"
+ y1="39.528976"
+ x2="5.5922894"
+ y2="17.763372"
+ id="line15"
+ style="fill:none;stroke:&stroke_color;;stroke-width:2.70127702;stroke-linecap:round;stroke-linejoin:round" /></g><g
+ transform="matrix(0.5891883,0,0,0.6267357,6.6476984e-2,7.8435405)"
+ id="g17">
+
+ <line
+ style="fill:none;stroke:&stroke_color;;stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round"
+ id="line19"
+ y2="11.828"
+ x2="32.536999"
+ y1="1.75"
+ x1="42.615002" />
+ <polyline
+ style="fill:none;stroke:&stroke_color;;stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round"
+ id="polyline21"
+ points="33.322,2.539 42.615,1.75 41.826,11.043 " />
+</g></g>
+</svg>
diff --git a/activity/Restore_icon.svg b/activity/Restore_icon.svg
deleted file mode 100644
index b6eb3a3..0000000
--- a/activity/Restore_icon.svg
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
- <!ENTITY stroke_color "#010101">
- <!ENTITY fill_color "#FFFFFF">
-]>
-<svg enable-background="new 0 0 55 55" height="55px" id="Layer_1" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px">
-<g display="block" id="document-save">
- <g>
- <g>
- <g>
- <path d="M6.736,49.002 h24.52c2.225,0,3.439-1.447,3.439-3.441V18.281c0-1.73-1.732-3.441-3.439-3.441h-4.389" fill="&fill_color;" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
- </g>
- </g>
- <g>
- <g>
- <path d="M26.867,38.592 c0,1.836-1.345,3.201-3.441,4.047l-16.69,6.363V14.84l16.69-8.599c2.228-0.394,3.441,0.84,3.441,2.834V38.592z" fill="&fill_color;" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
- </g>
- </g>
- <path d="M9.424,42.607 c0,0-1.351-0.543-2.702-0.543s-2.703,0.543-2.703,0.543" fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25"/>
- <path d="M9.424,32.006 c0,0-1.239-0.543-2.815-0.543c-1.577,0-2.59,0.543-2.59,0.543" fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25"/>
- <path d="M9.424,21.678 c0,0-1.125-0.544-2.927-0.544c-1.802,0-2.478,0.544-2.478,0.544" fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25"/>
-
- <line fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25" x1="13.209" x2="13.209" y1="46.533" y2="11.505"/>
-
- <g>
- <line fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" x1="41.17" x2="52.441" y1="16.188" y2="4.917"/>
- <polyline fill="none" points=" 51.562,15.306 41.17,16.188 42.053,5.794 " stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
- </g>
- </g>
-</g>
-</svg>
diff --git a/activity/activity.info b/activity/activity.info
index 2b6f2e3..21e76f1 100644
--- a/activity/activity.info
+++ b/activity/activity.info
@@ -1,9 +1,9 @@
[Activity]
-name = Restore
-activity_version = 1
-bundle_id = org.sugarlabs.Restore
-exec = sugar-activity restore.RestoreActivity
-icon = Restore_icon
-mime_types = application/vnd.sugar-multi-journal-entry
+name = Backup
+activity_version = 2
+bundle_id = org.sugarlabs.Backup
+exec = sugar-activity backup.BackupActivity
+icon = Backup_icon
+mime_types =
license = GPLv3
diff --git a/activity/mimetypes.xml b/activity/mimetypes.xml
deleted file mode 100644
index c9e1bc5..0000000
--- a/activity/mimetypes.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
-<mime-type type="application/vnd.sugar-multi-journal-entry">
-<comment xml:lang="en">Multi-entry Sugar Journal Entry Bundle</comment>
-<glob pattern="*.xmj"/>
-</mime-type>
-</mime-info>
diff --git a/backup.py b/backup.py
new file mode 100644
index 0000000..35d86ca
--- /dev/null
+++ b/backup.py
@@ -0,0 +1,730 @@
+#!/usr/bin/env python
+#
+# Author: Sascha Silbe <sascha-pgp@silbe.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3
+# as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""Backup. Activity to back up the Sugar Journal to external media.
+"""
+
+import gettext
+import logging
+import os
+import select
+import statvfs
+import sys
+import tempfile
+import time
+import traceback
+import zipfile
+
+import dbus
+import gconf
+import gobject
+import gtk
+
+#from sugar.activity.widgets import ActivityToolbarButton
+from sugar.activity.widgets import StopButton
+from sugar.activity import activity
+import sugar.env
+from sugar.graphics.icon import CellRendererIcon
+from sugar.graphics import style
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics.toolbarbox import ToolbarBox
+from sugar.graphics.xocolor import XoColor
+import sugar.logger
+from sugar import profile
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+
+DS_DBUS_SERVICE = "org.laptop.sugar.DataStore"
+DS_DBUS_INTERFACE1 = "org.laptop.sugar.DataStore"
+DS_DBUS_PATH1 = "/org/laptop/sugar/DataStore"
+DS_DBUS_INTERFACE2 = "org.laptop.sugar.DataStore2"
+DS_DBUS_PATH2 = "/org/laptop/sugar/DataStore2"
+
+HAL_SERVICE_NAME = 'org.freedesktop.Hal'
+HAL_MANAGER_PATH = '/org/freedesktop/Hal/Manager'
+HAL_MANAGER_IFACE = 'org.freedesktop.Hal.Manager'
+HAL_DEVICE_IFACE = 'org.freedesktop.Hal.Device'
+HAL_VOLUME_IFACE = 'org.freedesktop.Hal.Device.Volume'
+
+
+def format_size(size):
+ if not size:
+ return _('Empty')
+ elif size < 10*1024:
+ return _('%4d B') % size
+ elif size < 10*1024**2:
+ return _('%4d KiB') % (size // 1024)
+ elif size < 10*1024**3:
+ return _('%4d MiB') % (size // 1024**2)
+ else:
+ return _('%4d GiB') % (size // 1024**3)
+
+
+class BackupButton(ToolButton):
+
+ def __init__(self, **kwargs):
+ ToolButton.__init__(self, 'journal-export', **kwargs)
+ self.props.tooltip = _('Backup Journal').encode('utf-8')
+ self.props.accelerator = '<Alt>b'
+
+
+class AsyncBackup(gobject.GObject):
+ """
+ Run a data store backup asynchronously.
+ """
+
+ _METADATA_JSON_NAME = '_metadata.json'
+
+ __gsignals__ = {
+ 'progress': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([int, int])),
+ 'done': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
+ 'error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([str])),
+ }
+
+ def __init__(self, mount_point):
+ gobject.GObject.__init__(self)
+ self._mount_point = mount_point
+ self._path = None
+ self._bundle = None
+ self._child_pid = None
+ self._pipe_from_child = None
+ self._pipe_to_child = None
+ self._pipe_from_child_watch_id = None
+ self._num_entries = None
+ self._entries = None
+ self._data_store = None
+ self._user_name = profile.get_nick_name().replace('/', ' ')
+ self._key_hash = profile.get_profile().privkey_hash
+
+ if '\0' in self._user_name:
+ raise ValueError('Invalid user name')
+
+
+ def start(self):
+ """Start the backup process."""
+ to_child_read_fd, to_child_write_fd = os.pipe()
+ from_child_read_fd, from_child_write_fd = os.pipe()
+
+ self._child_pid = os.fork()
+ if not self._child_pid:
+ os.close(from_child_read_fd)
+ os.close(to_child_write_fd)
+ self._pipe_from_child = os.fdopen(from_child_write_fd, 'w')
+ self._pipe_to_child = os.fdopen(to_child_read_fd, 'r')
+ self._child_run()
+ sys.exit(0)
+ else:
+ os.close(from_child_write_fd)
+ os.close(to_child_read_fd)
+ self._pipe_from_child = os.fdopen(from_child_read_fd, 'r')
+ self._pipe_to_child = os.fdopen(to_child_write_fd, 'w')
+ self._pipe_from_child_watch_id = gobject.io_add_watch(
+ self._pipe_from_child,
+ gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP,
+ self._child_io_cb)
+
+ def abort(self):
+ """Abort the backup and clean up."""
+ self._pipe_to_child.write('abort\n')
+ self._parent_close()
+
+ def _child_io_cb(self, source_, condition):
+ """Receive and handle message from child."""
+ if condition in [gobject.IO_ERR, gobject.IO_HUP]:
+ logging.debug('error condition: %r', condition)
+ self.emit('error',
+ _('Lost connection to child process').encode('utf-8'))
+ self._parent_close()
+ return False
+
+ status = self._read_line_from_child()
+ if status == 'progress':
+ position = int(self._read_line_from_child())
+ num_entries = int(self._read_line_from_child())
+ self.emit('progress', position, num_entries)
+ return True
+
+ elif status == 'done':
+ self.emit('done')
+ self._parent_close()
+ return False
+
+ elif status == 'error':
+ message = unicode(self._read_line_from_child(), 'utf-8')
+ trace = unicode(self._pipe_from_child.read().strip(), 'utf-8')
+ logging.error('Child reported error: %s\n%s', message, trace)
+ self.emit('error', message.encode('utf-8'))
+ self._parent_close()
+ return False
+
+ else:
+ logging.error('Unknown status %r from child process', status)
+ self.emit('error', 'Unknown status %r from child process', status)
+ self.abort()
+ return False
+
+ def _read_line_from_child(self):
+ """Read a line from the child process using low-level IO.
+
+ This is a hack to work around the fact that file.readline() buffers
+ data without us knowing about it. If we call readline() a second
+ time when no data is buffered, it may block (=> the UI would hang).
+ If OTOH there is another line already in the buffer, we won't get
+ notified about it by select() as it already is in userspace.
+ There are cleaner ways to handle this (e.g. using the asyncore module),
+ but they are much more complex.
+ """
+ line = []
+ while True:
+ character = os.read(self._pipe_from_child.fileno(), 1)
+ if character == '\n':
+ return ''.join(line)
+
+ line.append(character)
+
+ def _parent_close(self):
+ """Close connections to child and wait for it."""
+ gobject.source_remove(self._pipe_from_child_watch_id)
+ self._pipe_from_child.close()
+ self._pipe_to_child.close()
+ pid_, status = os.waitpid(self._child_pid, 0)
+ if os.WIFEXITED(status):
+ logging.debug('Child exited with rc=%d', os.WEXITSTATUS(status))
+ elif os.WIFSIGNALED(status):
+ logging.debug('Child killed by signal %d', os.WTERMSIG(status))
+ else:
+ logging.error('Sudden infant death syndrome')
+
+ def _child_run(self):
+ """Main program of child."""
+ try:
+ self._connect_to_data_store()
+ self._entries, self._num_entries = self._find_entries()
+ assert self._num_entries == len(self._entries)
+ self._path, self._bundle = self._create_bundle()
+
+ for position, entry in enumerate(self._entries):
+ self._client_check_command()
+
+ self._send_to_parent('progress\n%d\n%d\n' % (position,
+ self._num_entries))
+ logging.debug('processing entry %r', entry)
+ self._add_entry(self._bundle, entry)
+
+ self._send_to_parent('progress\n%d\n%d\n' % (
+ self._num_entries, self._num_entries))
+ self._bundle.fp.flush()
+ self._bundle.close()
+ self._send_to_parent('done\n')
+
+ # pylint: disable=W0703
+ except Exception, exception:
+ self._pipe_from_child.write('error\n')
+ message = unicode(exception).encode('utf-8')
+ self._pipe_from_child.write(message+'\n')
+ trace = unicode(traceback.format_exc()).encode('utf-8')
+ self._pipe_from_child.write(trace)
+ self._remove_bundle()
+ sys.exit(2)
+
+ def _send_to_parent(self, message):
+ self._pipe_from_child.write(message)
+ self._pipe_from_child.flush()
+
+ def _client_check_command(self):
+ """Check for and execute command from the parent."""
+ in_ready, out_ready_, errors_on_ = select.select([self._pipe_to_child],
+ [], [], 0)
+ if not in_ready:
+ return
+
+ command = self._pipe_to_child.readline().strip()
+ logging.debug('command %r received', command)
+ if command == 'abort':
+ self._remove_bundle()
+ sys.exit(1)
+ else:
+ raise ValueError('Unknown command %r' % (command, ))
+
+ def _create_bundle(self):
+ """Create / open bundle (zip file) with unique file name."""
+ date = time.strftime('%x')
+ if '/' in date:
+ date = time.strftime('%Y-%m-%d')
+
+ prefix = _('Journal backup of %s (%s) on %s') % (self._user_name,
+ self._key_hash, date)
+ bundle_fd, path = self._create_file(self._mount_point, prefix, '.xmj')
+ try:
+ return path, zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED)
+ finally:
+ os.close(bundle_fd)
+
+ def _remove_bundle(self):
+ """Close bundle and remove it from permanent storage."""
+ if self._path:
+ os.remove(self._path)
+
+ if self._bundle and self._bundle.fp and not self._bundle.fp.closed:
+ self._bundle.close()
+
+ def _create_file(self, directory, prefix, suffix):
+ """Create a unique file with given prefix and suffix in directory.
+
+ Append random ASCII characters only if necessary.
+ """
+ path = '%s/%s%s' % (directory, prefix, suffix)
+ flags = os.O_CREAT | os.O_EXCL
+ mode = 0600
+ try:
+ return os.open(path, flags, mode), path
+
+ except OSError:
+ return tempfile.mkstemp(dir=directory, prefix=prefix + ' ',
+ suffix=suffix)
+
+ def _add_entry(self, bundle, entry):
+ """Add data store entry identified by entry to bundle."""
+ if 'version_id' in entry:
+ object_id = (entry['tree_id'], entry['version_id'])
+ object_id_s = '%s,%s' % object_id
+ else:
+ object_id = entry['uid']
+ object_id_s = object_id
+
+ metadata = self._get_metadata(object_id)
+ data_path = self._get_data(object_id)
+ if data_path:
+ bundle.write(data_path, os.path.join(object_id_s, object_id_s))
+
+ for name, value in metadata.items():
+ is_binary = False
+ try:
+ value.encode('utf-8')
+ except UnicodeDecodeError:
+ is_binary = True
+
+ if is_binary or len(value) > 8192:
+ logging.debug('adding binary/large property %r', name)
+ bundle.writestr(os.path.join(object_id_s, str(name),
+ object_id_s), value)
+ del metadata[name]
+
+ bundle.writestr(os.path.join(object_id_s, self._METADATA_JSON_NAME),
+ json.dumps(metadata))
+
+ def _connect_to_data_store(self):
+ """Open a connection to a Sugar data store."""
+ # We forked => need to use a private connection and make sure we
+ # never allow the main loop to run
+ # http://lists.freedesktop.org/archives/dbus/2007-April/007498.html
+ # http://lists.freedesktop.org/archives/dbus/2007-August/008359.html
+ bus = dbus.SessionBus(private=True)
+ try:
+ self._data_store = dbus.Interface(bus.get_object(DS_DBUS_SERVICE,
+ DS_DBUS_PATH2), DS_DBUS_INTERFACE2)
+ self._data_store.find({'tree_id': 'invalid'},
+ {'metadata': ['tree_id']})
+ logging.info('Data store with version support found')
+ return
+
+ except dbus.DBusException:
+ logging.debug('No data store with version support found')
+
+ self._data_store = dbus.Interface(bus.get_object(DS_DBUS_SERVICE,
+ DS_DBUS_PATH1), DS_DBUS_INTERFACE1)
+ self._data_store.find({'uid': 'invalid'}, ['uid'])
+ logging.info('Data store without version support found')
+
+ def _find_entries(self):
+ """Retrieve a list of all entries from the data store."""
+ if self._data_store.dbus_interface == DS_DBUS_INTERFACE2:
+ return self._data_store.find({}, {'metadata':
+ ['tree_id', 'version_id'], 'all_versions': True},
+ timeout=5*60, byte_arrays=True)
+ else:
+ return self._data_store.find({}, ['uid'], byte_arrays=True)
+
+ def _get_metadata(self, object_id):
+ """Return metadata for data store entry identified by object_id."""
+ if self._data_store.dbus_interface == DS_DBUS_INTERFACE2:
+ tree_id, version_id = object_id
+ return self._data_store.find(
+ {'tree_id': tree_id, 'version_id': version_id}, {},
+ byte_arrays=True)[0][0]
+ else:
+ return self._data_store.get_properties(object_id, byte_arrays=True)
+
+ def _get_data(self, object_id):
+ """Return path to data for data store entry identified by object_id."""
+ if self._data_store.dbus_interface == DS_DBUS_INTERFACE2:
+ tree_id, version_id = object_id
+ return self._data_store.get_data(tree_id, version_id,
+ byte_arrays=True)
+ else:
+ return self._data_store.get_filename(object_id, byte_arrays=True)
+
+
+class BackupActivity(activity.Activity):
+
+ _METADATA_JSON_NAME = '_metadata.json'
+ _MEDIA_COMBO_ICON_COLUMN = 0
+ _MEDIA_COMBO_NAME_COLUMN = 1
+ _MEDIA_COMBO_PATH_COLUMN = 2
+ _MEDIA_COMBO_FREE_COLUMN = 3
+
+ def __init__(self, handle):
+ activity.Activity.__init__(self, handle, create_jobject=False)
+ self.max_participants = 1
+ self._progress_bar = None
+ self._message_box = None
+ self._media_combo_model = None
+ self._media_combo = None
+ self._backup_button = None
+ self._backup = None
+ self._hal_devices = {}
+ client = gconf.client_get_default()
+ self._color = XoColor(client.get_string('/desktop/sugar/user/color'))
+ self._setup_widgets()
+ self._find_media()
+
+ def read_file(self, file_path):
+ """We don't have any state to save in the Journal."""
+ return
+
+ def write_file(self, file_path):
+ """We don't have any state to save in the Journal."""
+ return
+
+ def close(self, skip_save=False):
+ """We don't have any state to save in the Journal."""
+ activity.Activity.close(self, skip_save=True)
+
+ def _setup_widgets(self):
+ self._setup_toolbar()
+ self._setup_main_view()
+
+ def _setup_main_view(self):
+ vbox = gtk.VBox()
+ self.set_canvas(vbox)
+ vbox.show()
+
+ def _setup_toolbar(self):
+ toolbar_box = ToolbarBox()
+
+ self._media_combo_model = gtk.ListStore(str, str, str, str)
+ self._media_combo = gtk.ComboBox(self._media_combo_model)
+ icon_renderer = CellRendererIcon(self._media_combo)
+ icon_renderer.props.xo_color = self._color
+ icon_renderer.props.width = style.STANDARD_ICON_SIZE + \
+ style.DEFAULT_SPACING
+ icon_renderer.props.height = style.STANDARD_ICON_SIZE
+ icon_renderer.props.size = style.STANDARD_ICON_SIZE
+ icon_renderer.props.xpad = style.DEFAULT_PADDING
+ icon_renderer.props.ypad = style.DEFAULT_PADDING
+ self._media_combo.pack_start(icon_renderer, False)
+ self._media_combo.add_attribute(icon_renderer, 'icon_name',
+ self._MEDIA_COMBO_ICON_COLUMN)
+ name_renderer = gtk.CellRendererText()
+ self._media_combo.pack_start(name_renderer, False)
+ self._media_combo.add_attribute(name_renderer, 'text',
+ self._MEDIA_COMBO_NAME_COLUMN)
+ free_renderer = gtk.CellRendererText()
+ self._media_combo.pack_start(free_renderer, False)
+ self._media_combo.add_attribute(free_renderer, 'text',
+ self._MEDIA_COMBO_FREE_COLUMN)
+
+ tool_item = gtk.ToolItem()
+ tool_item.add(self._media_combo)
+ # FIXME: looks like plain GTK, not like Sugar
+ tooltip_text = _('Storage medium to store the backup on')
+ tool_item.set_tooltip_text(tooltip_text.encode('utf-8'))
+ toolbar_box.toolbar.insert(tool_item, -1)
+
+ self._backup_button = BackupButton()
+ self._backup_button.connect('clicked', self._backup_cb)
+ self._backup_button.set_sensitive(False)
+ toolbar_box.toolbar.insert(self._backup_button, -1)
+
+ separator = gtk.SeparatorToolItem()
+ separator.props.draw = False
+ separator.set_expand(True)
+ toolbar_box.toolbar.insert(separator, -1)
+
+ stop_button = StopButton(self)
+ toolbar_box.toolbar.insert(stop_button, -1)
+
+ self.set_toolbar_box(toolbar_box)
+ toolbar_box.show_all()
+
+ def _backup_cb(self, button):
+ """Callback for Backup button."""
+ if not len(self._media_combo_model):
+ return
+
+ row = self._media_combo_model[self._media_combo.get_active()]
+ mount_point = row[self._MEDIA_COMBO_PATH_COLUMN]
+ self._setup_backup_view(mount_point)
+ self._start_backup(mount_point)
+
+ def _start_backup(self, mount_point):
+ """Set up and start background worker process."""
+ self._backup = AsyncBackup(mount_point)
+ self._backup.connect('progress', self._progress_cb)
+ self._backup.connect('error', self._error_cb)
+ self._backup.connect('done', self._done_cb)
+ self._backup.start()
+
+ def _setup_backup_view(self, mount_point):
+ """Set up UI for showing feedback from worker process."""
+ vbox = gtk.VBox(False)
+
+ label_text = _('Backing up Journal to %s') % (mount_point, )
+ label = gtk.Label(label_text.encode('utf-8'))
+ label.show()
+ vbox.pack_start(label)
+
+ alignment = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.5)
+ alignment.show()
+
+ self._progress_bar = gtk.ProgressBar()
+ self._progress_bar.props.text = _('Scanning Journal').encode('utf-8')
+ self._progress_bar.show()
+ alignment.add(self._progress_bar)
+ vbox.add(alignment)
+
+ self._message_box = gtk.Label()
+ vbox.pack_start(self._message_box)
+
+ # FIXME
+# self._close_button = gtk.Button(_('Abort'))
+# self._close_button.connect('clicked', self._close_cb)
+# self._close_button.show()
+# button_box = gtk.HButtonBox()
+# button_box.show()
+# button_box.add(self._close_button)
+# vbox.pack_start(button_box, False)
+
+ vbox.show()
+ self.set_canvas(vbox)
+ self.show()
+
+ def _progress_cb(self, backup, position, num_entries):
+ """Update progress bar with information from child process."""
+ self._progress_bar.props.text = '%d / %d' % (position, num_entries)
+ self._progress_bar.props.fraction = float(position) / num_entries
+
+ def _done_cb(self, backup):
+ """Backup finished."""
+ logging.debug('_done_cb')
+# self._close_button.set_label(_('Finish'))
+
+ def _error_cb(self, backup, message):
+ """Receive error message from child process."""
+ self._show_error(unicode(message, 'utf-8'))
+
+ def _show_error(self, message):
+ """Present error message to user."""
+ self._message_box.props.label = unicode(message).encode('utf-8')
+ self._message_box.show()
+
+# def _close_cb(self, button):
+# if not self._done:
+# self._backup.abort()
+
+# self.emit('close')
+
+ def _find_media(self):
+ """Fill the combo box with available storage media.
+
+ Also sets up a callback to keep the list current.
+ """
+ try:
+ import gio
+ except ImportError:
+ return self._find_media_hal()
+
+ volume_monitor = gio.volume_monitor_get()
+ volume_monitor.connect('mount-added', self._gio_mount_added_cb)
+ volume_monitor.connect('mount-removed', self._gio_mount_removed_cb)
+ for mount in volume_monitor.get_mounts():
+ self._gio_mount_added_cb(volume_monitor, mount)
+
+ def _gio_mount_added_cb(self, volume_monitor, mount):
+ """Handle notification from GIO that a medium was mounted."""
+ icon_name = self._choose_icon_name(mount.get_icon().props.names)
+ path = mount.get_root().get_path()
+ name = mount.get_name()
+ self._add_medium(path, name, icon_name)
+
+ def _gio_mount_removed_cb(self, volume_monitor, mount):
+ """Handle notification from GIO that a medium was unmounted."""
+ path = mount.get_root().get_path()
+ self._remove_medium(path)
+
+ def _find_media_hal(self):
+ """Use HAL to fill in the available storage media."""
+ bus = dbus.SystemBus()
+ proxy = bus.get_object(HAL_SERVICE_NAME, HAL_MANAGER_PATH)
+ hal_manager = dbus.Interface(proxy, HAL_MANAGER_IFACE)
+ hal_manager.connect_to_signal('DeviceAdded',
+ self._hal_device_added_cb)
+
+ for udi in hal_manager.FindDeviceByCapability('volume'):
+ self._hal_device_added_cb(udi)
+
+ def _hal_device_added_cb(self, udi):
+ """Handle notification from GIO that a device was added."""
+ bus = dbus.SystemBus()
+ device_object = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(device_object, HAL_DEVICE_IFACE)
+
+ # A just-added device might lack one of the attributes we're
+ # looking for, so we need to listen for changes on ALL devices.
+ device.connect_to_signal('PropertyModified',
+ lambda *args: self._hal_property_modified_cb(udi, *args))
+
+ try:
+ if device.GetProperty('volume.fsusage') != 'filesystem':
+ logging.debug('Ignoring non-filesystem UDI %s', udi)
+ return
+ except dbus.DBusException:
+ logging.debug('Ignoring UDI %s without volume.fsusage', udi)
+ return
+
+ self._hal_try_device(device, udi)
+
+ def _hal_try_device(self, device, udi):
+ """Possibly add device to UI."""
+ if not device.GetProperty('volume.is_mounted'):
+ return
+
+ path = device.GetProperty('volume.mount_point')
+ if path == '/':
+ return
+
+ name = device.GetProperty('volume.label')
+ if not name:
+ name = device.GetProperty('volume.uuid')
+
+ self._hal_devices[udi] = path
+ bus = dbus.SystemBus()
+ bus.add_signal_receiver(self._hal_device_removed_cb,
+ 'DeviceRemoved',
+ HAL_MANAGER_IFACE, HAL_SERVICE_NAME,
+ HAL_MANAGER_PATH, arg0=udi)
+
+ self._add_medium(path, name,
+ self._choose_icon_name(self._hal_get_icons_for_volume(device)))
+
+ def _hal_device_removed_cb(self, udi):
+ """Handle notification from GIO that a device was removed."""
+ path = self._hal_devices.pop(udi, None)
+ if not path:
+ return
+
+ self._remove_medium(path)
+
+ def _hal_property_modified_cb(self, udi, count, changes):
+ """Handle notification from HAL that a property has changed."""
+ if 'volume.is_mounted' in [change[0] for change in changes]:
+ logging.debug('volume.is_mounted changed for UDI %s', udi)
+
+ bus = dbus.SystemBus()
+ device_object = bus.get_object(HAL_SERVICE_NAME, udi)
+ device = dbus.Interface(device_object, HAL_DEVICE_IFACE)
+
+ # We can get multiple notifications, so need to figure out
+ # the current state and ignore non-changes.
+ if device.GetProperty('volume.is_mounted'):
+ if udi not in self._hal_devices:
+ self._hal_try_device(device, udi)
+ else:
+ logging.debug('Ignoring duplicate notification')
+ else:
+ if udi in self._hal_devices:
+ self._hal_device_removed_cb(udi)
+ else:
+ logging.debug('Ignoring duplicate notification')
+ else:
+ logging.debug('Ignoring change for UDI %s', udi)
+
+ def _hal_get_icons_for_volume(self, device):
+ bus = dbus.SystemBus()
+ storage_udi = device.GetProperty('block.storage_device')
+ obj = bus.get_object(HAL_SERVICE_NAME, storage_udi)
+ storage_device = dbus.Interface(obj, HAL_DEVICE_IFACE)
+
+ storage_drive_type = storage_device.GetProperty('storage.drive_type')
+ bus_type = storage_device.GetProperty('storage.bus')
+ if storage_drive_type == 'sd_mmc':
+ return ['media-flash-sd', 'media-flash-sd-mmc', 'media']
+ elif bus_type == 'usb':
+ return ['media-flash-usb', 'media', 'media-disk']
+ else:
+ return ['media', 'media-disk', 'media-flash-usb']
+
+ def _add_medium(self, path, medium_name, icon_name):
+ """Make storage medium selectable in the UI."""
+ # FIXME: update space information periodically or at least after
+ # backup run
+ stat = os.statvfs(path)
+ free_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BAVAIL]
+# total_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BLOCKS]
+ self._media_combo_model.append([icon_name, medium_name, path,
+ _('%s Free') % (format_size(free_space), )])
+ self._backup_button.set_sensitive(True)
+ if self._media_combo.get_active() == -1:
+ self._media_combo.set_active(0)
+
+ def _remove_medium(self, path):
+ """Remove storage medium from UI."""
+ active = self._media_combo.get_active()
+ for position, row in enumerate(self._media_combo_model):
+ if path == row[self._MEDIA_COMBO_PATH_COLUMN]:
+ del self._media_combo_model[position]
+
+ if not len(self._media_combo_model):
+ self._backup_button.set_sensitive(False)
+ return
+
+ if position != active:
+ return
+
+ self._media_combo.set_active(max(0, position-1))
+ return
+
+ logging.warning("Asked to remove %s from UI, but didn't find it!",
+ path)
+
+ def _choose_icon_name(self, names):
+ """Choose the first valid icon name or fall back to a default name."""
+ theme = gtk.icon_theme_get_default()
+ for name in names:
+ if theme.lookup_icon(name, gtk.ICON_SIZE_LARGE_TOOLBAR, 0):
+ return name
+
+ return 'drive'
+
+
+# pylint isn't smart enough for the gettext.install() magic
+_ = lambda msg: msg
+gettext.install('backup', 'po', unicode=True)
+sugar.logger.start()
diff --git a/icons/journal-export.svg b/icons/journal-export.svg
new file mode 100644
index 0000000..cb1baf1
--- /dev/null
+++ b/icons/journal-export.svg
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 12.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ id="Icon"
+ width="44.365"
+ height="42.918"
+ viewBox="0 0 44.365 42.918"
+ overflow="visible"
+ enable-background="new 0 0 44.365 42.918"
+ xml:space="preserve"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="stock-open.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"><metadata
+ id="metadata26"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs24"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 21.459 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="44.365002 : 21.459 : 1"
+ inkscape:persp3d-origin="22.182501 : 14.306 : 1"
+ id="perspective28" /></defs><sodipodi:namedview
+ inkscape:window-height="975"
+ inkscape:window-width="1680"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="false"
+ inkscape:zoom="13.563637"
+ inkscape:cx="22.182502"
+ inkscape:cy="28.758498"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:current-layer="Icon" />
+
+
+
+<g
+ id="g2414"
+ transform="translate(9.0634189,-3.9384509)"><g
+ transform="translate(2.6055351e-2,5.183866e-2)"
+ id="g2405"><g
+ id="g3"
+ transform="matrix(0.736868,0,0,0.7153409,0,12.146464)">
+ <g
+ id="g5">
+ <path
+ d="M 1.75,41.168 L 23.673,41.168 C 25.663,41.168 26.749,39.873 26.749,38.09 L 26.749,13.699 C 26.749,12.152 25.2,10.622 23.673,10.622 L 19.75,10.622"
+ id="path7"
+ style="fill:none;stroke:#ffffff;stroke-width:3.5;stroke-linejoin:round" />
+ </g>
+</g><g
+ id="g9"
+ transform="matrix(0.6976744,0,0,0.7277711,0,11.605042)">
+ <g
+ id="g11">
+ <path
+ d="M 19.75,31.859 C 19.75,33.502 18.548,34.723 16.673,35.478 L 1.75,41.168 L 1.75,10.623 L 16.673,2.935 C 18.664,2.583 19.75,3.686 19.75,5.469 L 19.75,31.859 z"
+ id="path13"
+ style="fill:none;stroke:#ffffff;stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round" />
+ </g>
+</g><line
+ x1="5.5922894"
+ y1="39.528976"
+ x2="5.5922894"
+ y2="17.763372"
+ id="line15"
+ style="fill:none;stroke:#ffffff;stroke-width:2.70127702;stroke-linecap:round;stroke-linejoin:round" /></g><g
+ transform="matrix(0.5891883,0,0,0.6267357,6.6476984e-2,7.8435405)"
+ id="g17">
+
+ <line
+ style="fill:none;stroke:#ffffff;stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round"
+ id="line19"
+ y2="11.828"
+ x2="32.536999"
+ y1="1.75"
+ x1="42.615002" />
+ <polyline
+ style="fill:none;stroke:#ffffff;stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round"
+ id="polyline21"
+ points="33.322,2.539 42.615,1.75 41.826,11.043 " />
+</g></g>
+</svg> \ No newline at end of file
diff --git a/icons/journal-import.svg b/icons/journal-import.svg
deleted file mode 100644
index b84a374..0000000
--- a/icons/journal-import.svg
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
- <!ENTITY stroke_color "#010101">
- <!ENTITY fill_color "#FFFFFF">
-]>
-<svg enable-background="new 0 0 55 55" height="55px" id="Layer_1" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px">
-<g display="block" id="document-save">
- <g>
- <g>
- <g>
- <path d="M6.736,49.002 h24.52c2.225,0,3.439-1.447,3.439-3.441V18.281c0-1.73-1.732-3.441-3.439-3.441h-4.389" fill="&fill_color;" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
- </g>
- </g>
- <g>
- <g>
- <path d="M26.867,38.592 c0,1.836-1.345,3.201-3.441,4.047l-16.69,6.363V14.84l16.69-8.599c2.228-0.394,3.441,0.84,3.441,2.834V38.592z" fill="&fill_color;" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
- </g>
- </g>
- <path d="M9.424,42.607 c0,0-1.351-0.543-2.702-0.543s-2.703,0.543-2.703,0.543" fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25"/>
- <path d="M9.424,32.006 c0,0-1.239-0.543-2.815-0.543c-1.577,0-2.59,0.543-2.59,0.543" fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25"/>
- <path d="M9.424,21.678 c0,0-1.125-0.544-2.927-0.544c-1.802,0-2.478,0.544-2.478,0.544" fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25"/>
-
- <line fill="none" stroke="&stroke_color;" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25" x1="13.209" x2="13.209" y1="46.533" y2="11.505"/>
-
- <g>
- <line fill="none" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" x1="41.17" x2="52.441" y1="16.188" y2="4.917"/>
- <polyline fill="none" points=" 51.562,15.306 41.17,16.188 42.053,5.794 " stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
- </g>
- </g>
-</g>
-</svg> \ No newline at end of file
diff --git a/restore.py b/restore.py
deleted file mode 100644
index 4c700cb..0000000
--- a/restore.py
+++ /dev/null
@@ -1,577 +0,0 @@
-#!/usr/bin/env python
-#
-# Author: Sascha Silbe <sascha-pgp@silbe.org>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 3
-# as published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-"""Restore. Activity to write back a Sugar Journal backup in JEB format.
-"""
-
-import gettext
-import logging
-import os
-import select
-import sys
-import tempfile
-import time
-import traceback
-import zipfile
-
-import dbus
-import gobject
-import gtk
-
-from sugar.activity.widgets import StopButton
-from sugar.activity import activity
-import sugar.env
-from sugar.graphics.toolbutton import ToolButton
-from sugar.graphics.toolbarbox import ToolbarBox
-import sugar.logger
-
-try:
- import json
-except ImportError:
- import simplejson as json
-
-
-DS_DBUS_SERVICE = "org.laptop.sugar.DataStore"
-DS_DBUS_INTERFACE1 = "org.laptop.sugar.DataStore"
-DS_DBUS_PATH1 = "/org/laptop/sugar/DataStore"
-DS_DBUS_INTERFACE2 = "org.laptop.sugar.DataStore2"
-DS_DBUS_PATH2 = "/org/laptop/sugar/DataStore2"
-
-CTIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
-
-
-def format_size(size):
- if not size:
- return _('Empty')
- elif size < 10*1024:
- return _('%4d B') % size
- elif size < 10*1024**2:
- return _('%4d KiB') % (size // 1024)
- elif size < 10*1024**3:
- return _('%4d MiB') % (size // 1024**2)
- else:
- return _('%4d GiB') % (size // 1024**3)
-
-
-class MalformedBundleException(Exception):
- """Trying to read an invalid bundle."""
- pass
-
-
-class RestoreButton(ToolButton):
-
- def __init__(self, **kwargs):
- ToolButton.__init__(self, 'journal-import', **kwargs)
- self.props.tooltip = _('Restore Journal').encode('utf-8')
- self.props.accelerator = '<Alt>r'
-
-
-class AsyncRestore(gobject.GObject):
- """
- Restore a backup to the Sugar data store asynchronously.
- """
-
- _METADATA_JSON_NAME = '_metadata.json'
-
- __gsignals__ = {
- 'progress': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([int, int])),
- 'done': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
- 'error': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([str])),
- }
-
- def __init__(self, path):
- gobject.GObject.__init__(self)
- self._path = path
- self._bundle = None
- self._child_pid = None
- self._pipe_from_child = None
- self._pipe_to_child = None
- self._pipe_from_child_watch_id = None
- self._data_store = None
-
-
- def start(self):
- """Start the restore process."""
- to_child_read_fd, to_child_write_fd = os.pipe()
- from_child_read_fd, from_child_write_fd = os.pipe()
-
- self._child_pid = os.fork()
- if not self._child_pid:
- os.close(from_child_read_fd)
- os.close(to_child_write_fd)
- self._pipe_from_child = os.fdopen(from_child_write_fd, 'w')
- self._pipe_to_child = os.fdopen(to_child_read_fd, 'r')
- self._child_run()
- sys.exit(0)
- else:
- os.close(from_child_write_fd)
- os.close(to_child_read_fd)
- self._pipe_from_child = os.fdopen(from_child_read_fd, 'r')
- self._pipe_to_child = os.fdopen(to_child_write_fd, 'w')
- self._pipe_from_child_watch_id = gobject.io_add_watch(
- self._pipe_from_child,
- gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP,
- self._child_io_cb)
-
- def abort(self):
- """Abort the restore."""
- self._pipe_to_child.write('abort\n')
- self._parent_close()
-
- def _child_io_cb(self, source_, condition):
- """Receive and handle message from child."""
- if condition in [gobject.IO_ERR, gobject.IO_HUP]:
- logging.debug('error condition: %r', condition)
- self.emit('error',
- _('Lost connection to child process').encode('utf-8'))
- self._parent_close()
- return False
-
- status = self._read_line_from_child()
- if status == 'progress':
- position = int(self._read_line_from_child())
- num_entries = int(self._read_line_from_child())
- self.emit('progress', position, num_entries)
- return True
-
- elif status == 'done':
- self.emit('done')
- self._parent_close()
- return False
-
- elif status == 'error':
- message = unicode(self._read_line_from_child(), 'utf-8')
- trace = unicode(self._pipe_from_child.read().strip(), 'utf-8')
- logging.error('Child reported error: %s\n%s', message, trace)
- self.emit('error', message.encode('utf-8'))
- self._parent_close()
- return False
-
- else:
- logging.error('Unknown status %r from child process', status)
- self.emit('error', 'Unknown status %r from child process', status)
- self.abort()
- return False
-
- def _read_line_from_child(self):
- """Read a line from the child process using low-level IO.
-
- This is a hack to work around the fact that file.readline() buffers
- data without us knowing about it. If we call readline() a second
- time when no data is buffered, it may block (=> the UI would hang).
- If OTOH there is another line already in the buffer, we won't get
- notified about it by select() as it already is in userspace.
- There are cleaner ways to handle this (e.g. using the asyncore module),
- but they are much more complex.
- """
- line = []
- while True:
- character = os.read(self._pipe_from_child.fileno(), 1)
- if character == '\n':
- return ''.join(line)
-
- line.append(character)
-
- def _parent_close(self):
- """Close connections to child and wait for it."""
- gobject.source_remove(self._pipe_from_child_watch_id)
- self._pipe_from_child.close()
- self._pipe_to_child.close()
- pid_, status = os.waitpid(self._child_pid, 0)
- if os.WIFEXITED(status):
- logging.debug('Child exited with rc=%d', os.WEXITSTATUS(status))
- elif os.WIFSIGNALED(status):
- logging.debug('Child killed by signal %d', os.WTERMSIG(status))
- else:
- logging.error('Sudden infant death syndrome')
-
- def _child_run(self):
- """Main program of child."""
- try:
- self._connect_to_data_store()
- self._bundle = zipfile.ZipFile(self._path, 'r')
- self._check_bundle()
-
- entries = self._get_directories().items()
- num_entries = len(entries)
- for position, (object_id, file_paths) in enumerate(entries):
- self._client_check_command()
-
- if len(object_id) < 36:
- logging.warning('Ignoring unknown directory %r', object_id)
- continue
-
- if self._METADATA_JSON_NAME not in file_paths:
- logging.warning('Ignoring directory %r without %s',
- object_id, self._METADATA_JSON_NAME)
- continue
-
- logging.debug('processing entry %r', object_id)
-
- try:
- self._install_entry(object_id, file_paths)
- # pylint: disable=W0703
- except Exception:
- # TODO: relay to UI
- logging.exception('Error installing Journal entry %r:',
- object_id)
-
- self._send_to_parent('progress\n%d\n%d\n' % (position,
- num_entries))
-
- self._send_to_parent('progress\n%d\n%d\n' % (num_entries,
- num_entries))
- self._close_bundle()
- self._send_to_parent('done\n')
-
- # pylint: disable=W0703
- except Exception, exception:
- self._pipe_from_child.write('error\n')
- message = unicode(exception).encode('utf-8')
- self._pipe_from_child.write(message+'\n')
- trace = unicode(traceback.format_exc()).encode('utf-8')
- self._pipe_from_child.write(trace)
- self._close_bundle()
- sys.exit(2)
-
- def _send_to_parent(self, message):
- self._pipe_from_child.write(message)
- self._pipe_from_child.flush()
-
- def _client_check_command(self):
- """Check for and execute command from the parent."""
- in_ready, out_ready_, errors_on_ = select.select([self._pipe_to_child],
- [], [], 0)
- if not in_ready:
- return
-
- command = self._pipe_to_child.readline().strip()
- logging.debug('command %r received', command)
- if command == 'abort':
- self._remove_bundle()
- sys.exit(1)
- else:
- raise ValueError('Unknown command %r' % (command, ))
-
- def _check_bundle(self):
- """Check bundle for validity."""
- # potentially expensive, but avoids trouble during unpacking
- if self._bundle.testzip() is not None:
- raise MalformedBundleException(_('Corrupt zip file'))
-
- file_names = self._bundle.namelist()
- if not file_names:
- raise MalformedBundleException(_('Empty bundle'))
-
- metadata_seen = False
- for name in file_names:
- for part in name.split('/'):
- if part.startswith('.'):
- raise MalformedBundleException(
- _('Path component starts with dot: %r') % (name, ))
-
- if name.split('/')[-1] == self._METADATA_JSON_NAME:
- metadata_seen = True
-
- if not metadata_seen:
- raise MalformedBundleException('No metadata file found')
-
- def _read_data(self, object_id):
- """Read data for given object from bundle."""
- # TODO: verify this uses the activity data dir
- data_fd, data_file_name = tempfile.mkstemp(prefix='Restore')
- data_file = os.fdopen(data_fd, 'w')
- try:
- # TODO: handle large files better (i.e. use external tool)
- # TODO: predict disk-full
- data_file.write(self._bundle.read(
- os.path.join(object_id, object_id)))
- return data_file_name
- finally:
- data_file.close()
-
- def _read_metadata(self, object_id):
- """Read metadata for given object from bundle."""
- metadata_path = os.path.join(object_id, self._METADATA_JSON_NAME)
- json_data = self._bundle.read(metadata_path)
- return json.loads(json_data)
-
- def _get_directories(self):
- """Get the names of top-level directories in bundle and of their files.
- """
- contents = {}
- for path in self._bundle.namelist():
- if path.endswith('/'):
- continue
-
- directory, file_name = path.lstrip('/').split('/', 1)
- contents.setdefault(directory, []).append(file_name)
-
- return contents
-
- def _install_entry(self, object_id, file_paths):
- """Reassemble the given entry and save it to the data store.
-
- file_paths is destroyed as a side effect."""
- file_paths.remove(self._METADATA_JSON_NAME)
- metadata = self._read_metadata(object_id)
-
- data_file_name = ''
- if object_id in file_paths:
- file_paths.remove(object_id)
- data_file_name = self._read_data(object_id)
-
- for path in file_paths:
- components = path.split('/')
- if len(components) != 2 or components[1] != object_id:
- logging.warning('Ignoring unknown file %r', path)
-
- name = components[0]
- value = self._bundle.read(os.path.join(object_id, path))
- metadata[name] = dbus.ByteArray(value)
-
- del file_paths[:]
- self._save_entry(metadata, data_file_name)
-
- def _close_bundle(self):
- """Ensure the bundle is closed."""
- if self._bundle and self._bundle.fp and not self._bundle.fp.closed:
- self._bundle.close()
-
- def _connect_to_data_store(self):
- """Open a connection to a Sugar data store."""
- # We forked => need to use a private connection and make sure we
- # never allow the main loop to run
- # http://lists.freedesktop.org/archives/dbus/2007-April/007498.html
- # http://lists.freedesktop.org/archives/dbus/2007-August/008359.html
- bus = dbus.SessionBus(private=True)
- try:
- self._data_store = dbus.Interface(bus.get_object(DS_DBUS_SERVICE,
- DS_DBUS_PATH2), DS_DBUS_INTERFACE2)
- self._data_store.find({'tree_id': 'invalid'},
- {'metadata': ['tree_id']})
- logging.info('Data store with version support found')
- return
-
- except dbus.DBusException:
- logging.debug('No data store with version support found')
-
- self._data_store = dbus.Interface(bus.get_object(DS_DBUS_SERVICE,
- DS_DBUS_PATH1), DS_DBUS_INTERFACE1)
- self._data_store.find({'uid': 'invalid'}, ['uid'])
- logging.info('Data store without version support found')
-
- def _save_entry(self, metadata, data_path):
- """Store object in data store."""
- if self._data_store.dbus_interface == DS_DBUS_INTERFACE2:
- tree_id = metadata.get('tree_id') or metadata['uid']
- version_id = metadata.get('version_id', '')
- parent_id = metadata.get('parent_id', '')
- if self._find_entry_v2(tree_id, version_id):
- logging.info('Skipping existing entry %r / %r', tree_id,
- version_id)
- return
-
- # FIXME: cannot restore version_id
- self._data_store.save(tree_id, parent_id, metadata, data_path,
- True)
- else:
- uid = metadata.get('uid') or metadata['tree_id']
- timestamp = metadata.get('timestamp') or \
- time.strftime(CTIME_FORMAT, metadata['ctime'])
- entry = self._find_entry_v1(uid)
- if entry:
- ds_timestamp = entry.get('timestamp') or \
- time.strftime(CTIME_FORMAT, entry[0]['ctime'])
-
- if ds_timestamp >= timestamp:
- logging.info('Skipping outdated entry for %r', uid)
- return
-
- self._data_store.update(uid, metadata, data_path, True)
-
- else:
- self._data_store.create(metadata, data_path, True)
-
- def _find_entry_v1(self, uid):
- """Retrieve given entry from v1 data store if it exists.
- """
- try:
- return self._data_store.get_properties(uid)
-
- except dbus.DBusException, exception:
- exception_name = exception.get_dbus_name()
- if exception_name.startswith('org.freedesktop.DBus.Python'):
- return None
-
- raise
-
- def _find_entry_v2(self, tree_id, version_id):
- """Retrieve given entry from v2 data store if it exists.
- """
- query = {'tree_id': tree_id}
- if version_id:
- query['version_id'] = version_id
-
- entries = self._data_store.find(query,{}, byte_arrays=True)[0]
- if entries:
- return entries[0]
- return None
-
-
-class RestoreActivity(activity.Activity):
-
- def __init__(self, handle):
- activity.Activity.__init__(self, handle, create_jobject=False)
- self.max_participants = 1
- self._progress_bar = None
- self._message_box = None
- self._restore = None
- self._restore_button = None
- self._no_bundle_warning = None
- self._path = None
- self._setup_widgets()
-
- def read_file(self, file_path):
- """Set path to bundle to restore."""
- self._path = file_path
- self._no_bundle_warning.hide()
- self._restore_button.set_sensitive(True)
-
- def write_file(self, file_path):
- """We don't have any state to save in the Journal."""
- return
-
- def save(self):
- """We don't have any state to save in the Journal."""
- return
-
- def close(self, skip_save=False):
- """We don't have any state to save in the Journal."""
- activity.Activity.close(self, skip_save=True)
-
- def _setup_widgets(self):
- self._setup_toolbar()
- self._setup_main_view()
-
- def _setup_main_view(self):
- vbox = gtk.VBox()
- warning = _('No bundle selected. Please close this activity and'
- ' choose a bundle to restore from the Journal.')
- self._no_bundle_warning = gtk.Label(warning.encode('utf-8'))
- vbox.pack_start(self._no_bundle_warning, True)
- self.set_canvas(vbox)
- vbox.show_all()
-
- def _setup_toolbar(self):
- toolbar_box = ToolbarBox()
-
- self._restore_button = RestoreButton()
- self._restore_button.connect('clicked', self._restore_cb)
- self._restore_button.set_sensitive(False)
- toolbar_box.toolbar.insert(self._restore_button, -1)
-
- separator = gtk.SeparatorToolItem()
- separator.props.draw = False
- separator.set_expand(True)
- toolbar_box.toolbar.insert(separator, -1)
-
- stop_button = StopButton(self)
- toolbar_box.toolbar.insert(stop_button, -1)
-
- self.set_toolbar_box(toolbar_box)
- toolbar_box.show_all()
-
- def _restore_cb(self, button):
- """Callback for Restore button."""
- self._setup_restore_view()
- self._start_restore()
-
- def _start_restore(self):
- """Set up and start background worker process."""
- self._restore = AsyncRestore(self._path)
- self._restore.connect('progress', self._progress_cb)
- self._restore.connect('error', self._error_cb)
- self._restore.connect('done', self._done_cb)
- self._restore.start()
-
- def _setup_restore_view(self):
- """Set up UI for showing feedback from worker process."""
- self._restore_button.set_sensitive(False)
- vbox = gtk.VBox(False)
-
- label_text = _('Restoring Journal from %s') % (self._path, )
- label = gtk.Label(label_text.encode('utf-8'))
- label.show()
- vbox.pack_start(label)
-
- alignment = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.5)
- alignment.show()
-
- self._progress_bar = gtk.ProgressBar()
- self._progress_bar.props.text = _('Scanning bundle').encode('utf-8')
- self._progress_bar.show()
- alignment.add(self._progress_bar)
- vbox.add(alignment)
-
- self._message_box = gtk.Label()
- vbox.pack_start(self._message_box)
-
- # FIXME
-# self._close_button = gtk.Button(_('Abort'))
-# self._close_button.connect('clicked', self._close_cb)
-# self._close_button.show()
-# button_box = gtk.HButtonBox()
-# button_box.show()
-# button_box.add(self._close_button)
-# vbox.pack_start(button_box, False)
-
- vbox.show()
- self.set_canvas(vbox)
- self.show()
-
- def _progress_cb(self, restore_, position, num_entries):
- """Update progress bar with information from child process."""
- self._progress_bar.props.text = '%d / %d' % (position, num_entries)
- self._progress_bar.props.fraction = float(position) / num_entries
-
- def _done_cb(self, restore_):
- """Restore finished."""
- logging.debug('_done_cb')
- self._restore_button.set_sensitive(True)
-# self._close_button.set_label(_('Finish'))
-
- def _error_cb(self, restore_, message):
- """Receive error message from child process."""
- self._show_error(unicode(message, 'utf-8'))
- self._restore_button.set_sensitive(True)
-
- def _show_error(self, message):
- """Present error message to user."""
- self._message_box.props.label = unicode(message).encode('utf-8')
- self._message_box.show()
-
-# def _close_cb(self, button):
-# if not self._done:
-# self._restore.abort()
-
-# self.emit('close')
-
-
-# pylint isn't smart enough for the gettext.install() magic
-_ = lambda msg: msg
-gettext.install('restore', 'po', unicode=True)
-sugar.logger.start()