diff options
author | Martin Abente <mabente@paraguayeduca.org> | 2010-06-22 19:59:13 (GMT) |
---|---|---|
committer | Sascha Silbe <sascha-pgp@silbe.org> | 2010-11-01 16:52:17 (GMT) |
commit | 940eb54bf1eac76c56cbd1a27b1c40335efbbab4 (patch) | |
tree | f6af1328b726ee73bc541d33580c2a97f2d8d1d7 | |
parent | 1dc60e50276bfb5ebb3e27a3a02bcc0228b2d4b8 (diff) |
Journal Volumes Backup and Restore
Add a basic backup and restore feature for the Sugar Journal.
It provides:
- Generic Backup and Restore dialog GUI.
- Process manager class as an abstraction layer between the dialog and
backup/restore scripts. (Allowing to work with many backup and restore
technologies, using the same GUI, with no need for script rewrite).
- Basic file system Volume Restore and Backup scripts implemented in Python.
- New backup and restore options for journal volumes palettes.
This patch is based on Esteban Arias (Plan Ceibal) Volume Backup and Restore
patch, with a few changes:
- Refactor original Backup dialog class into a generic dialog class.
- Create specialized VolumeBackupDialog and VolumeRestoreDialog subclasses.
- Rewrite backup and restore scripts in python for an easier sugar interaction.
- Add backup identification helpers to jarabe.journal.misc.
-rw-r--r-- | bin/Makefile.am | 4 | ||||
-rw-r--r-- | bin/journal-backup-volume | 57 | ||||
-rw-r--r-- | bin/journal-restore-volume | 67 | ||||
-rw-r--r-- | src/jarabe/journal/Makefile.am | 3 | ||||
-rw-r--r-- | src/jarabe/journal/misc.py | 27 | ||||
-rw-r--r-- | src/jarabe/journal/processdialog.py | 248 | ||||
-rw-r--r-- | src/jarabe/journal/volumestoolbar.py | 5 | ||||
-rw-r--r-- | src/jarabe/model/Makefile.am | 3 | ||||
-rw-r--r-- | src/jarabe/model/processmanagement.py | 98 | ||||
-rw-r--r-- | src/jarabe/view/palettes.py | 44 |
10 files changed, 551 insertions, 5 deletions
diff --git a/bin/Makefile.am b/bin/Makefile.am index 05a9215..8cc87b5 100644 --- a/bin/Makefile.am +++ b/bin/Makefile.am @@ -5,7 +5,9 @@ python_scripts = \ sugar-install-bundle \ sugar-launch \ sugar-session \ - sugar-ui-check + sugar-ui-check \ + journal-backup-volume \ + journal-restore-volume bin_SCRIPTS = \ sugar \ diff --git a/bin/journal-backup-volume b/bin/journal-backup-volume new file mode 100644 index 0000000..9246760 --- /dev/null +++ b/bin/journal-backup-volume @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# Copyright (C) 2010, Paraguay Educa <tecnologia@paraguayeduca.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# 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/>. +# + +import os +import sys +import subprocess +import logging + +from sugar import env +#from sugar.datastore import datastore + +backup_identifier = sys.argv[2] +volume_path = sys.argv[1] + +if len(sys.argv) != 3: + print 'Usage: %s <volume_path> <backup_identifier>' % sys.argv[0] + exit(1) + +logging.debug('Backup started') + +backup_path = os.path.join(volume_path, 'backup', backup_identifier) + +if not os.path.exists(backup_path): + os.makedirs(backup_path) + +#datastore.freeze() +#subprocess.call(['pkill', '-9', '-f', 'python.*datastore-service']) + +result = 0 +try: + cmd = ['tar', '-C', env.get_profile_path(), '-czf', \ + os.path.join(backup_path, 'datastore.tar.gz'), 'datastore'] + + subprocess.check_call(cmd) + +except Exception, e: + logging.error('Backup failed: %s', str(e)) + result = 1 + +#datastore.thaw() + +logging.debug('Backup finished') +exit(result) diff --git a/bin/journal-restore-volume b/bin/journal-restore-volume new file mode 100644 index 0000000..aa14ad0 --- /dev/null +++ b/bin/journal-restore-volume @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# Copyright (C) 2010, Paraguay Educa <tecnologia@paraguayeduca.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# 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/>. +# + +import os +import sys +import shutil +import logging +import subprocess + +from sugar import env +#from sugar.datastore import datastore + +backup_identifier = sys.argv[2] +volume_path = sys.argv[1] + +if len(sys.argv) != 3: + print 'Usage: %s <volume_path> <backup_identifier>' % sys.argv[0] + exit(1) + +logging.debug('Restore started') + +journal_path = os.path.join(env.get_profile_path(), 'datastore') +backup_path = os.path.join(volume_path, 'backup', backup_identifier, 'datastore.tar.gz') + +if not os.path.exists(backup_path): + logging.error('Could not find backup file %s', backup_path) + exit(1) + +#datastore.freeze() +subprocess.call(['pkill', '-9', '-f', 'python.*datastore-service']) + +result = 0 +try: + if os.path.exists(journal_path): + shutil.rmtree(journal_path) + + subprocess.check_call(['tar', '-C', env.get_profile_path(), '-xzf', backup_path]) + +except Exception, e: + logging.error('Restore failed: %s', str(e)) + result = 1 + +try: + shutil.rmtree(os.path.join(journal_path, 'index')) + os.remove(os.path.join(journal_path, 'index_updated')) + os.remove(os.path.join(journal_path, 'version')) +except: + logging.debug('Restore has no index files') + +#datastore.thaw() + +logging.debug('Restore finished') +exit(result) diff --git a/src/jarabe/journal/Makefile.am b/src/jarabe/journal/Makefile.am index f4bf273..a760869 100644 --- a/src/jarabe/journal/Makefile.am +++ b/src/jarabe/journal/Makefile.am @@ -14,4 +14,5 @@ sugar_PYTHON = \ model.py \ objectchooser.py \ palettes.py \ - volumestoolbar.py + volumestoolbar.py \ + processdialog.py diff --git a/src/jarabe/journal/misc.py b/src/jarabe/journal/misc.py index 24ad216..6e3cb95 100644 --- a/src/jarabe/journal/misc.py +++ b/src/jarabe/journal/misc.py @@ -1,4 +1,5 @@ # Copyright (C) 2007, One Laptop Per Child +# Copyright (C) 2010, Paraguay Educa <tecnologia@paraguayeduca.org> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -249,3 +250,29 @@ def get_icon_color(metadata): return XoColor(client.get_string('/desktop/sugar/user/color')) else: return XoColor(metadata['icon-color']) + +def get_backup_identifier(): + serial_number = get_xo_serial() + if serial_number is None: + serial_number = get_nick() + return serial_number + +def get_xo_serial(): + path = '/ofw/serial-number' + + if os.access(path, os.R_OK) == 0: + return None + + file_descriptor = open(path, 'r') + content = file_descriptor.read() + file_descriptor.close() + + if content: + return content.strip() + else: + logging.error('No serial number at %s', path) + return None + +def get_nick(): + client = gconf.client_get_default() + return client.get_string("/desktop/sugar/user/nick") diff --git a/src/jarabe/journal/processdialog.py b/src/jarabe/journal/processdialog.py new file mode 100644 index 0000000..b96abd9 --- /dev/null +++ b/src/jarabe/journal/processdialog.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python +# Copyright (C) 2010, Plan Ceibal <comunidad@plan.ceibal.edu.uy> +# Copyright (C) 2010, Paraguay Educa <tecnologia@paraguayeduca.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# 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/>. + +import gtk +import gobject +import gconf +import logging + +from gettext import gettext as _ +from sugar.graphics import style +from sugar.graphics.icon import Icon +from sugar.graphics.xocolor import XoColor + +from jarabe.journal import misc +from jarabe.model import shell +from jarabe.model import processmanagement +from jarabe.model.session import get_session_manager + +class ProcessDialog(gtk.Window): + + __gtype_name__ = 'SugarProcessDialog' + + def __init__(self, process_script='', process_params=[], restart_after=True): + + #FIXME: Workaround limitations of Sugar core modal handling + shell_model = shell.get_model() + shell_model.set_zoom_level(shell_model.ZOOM_HOME) + + gtk.Window.__init__(self) + + self._process_script = processmanagement.find_and_absolutize(process_script) + self._process_params = process_params + self._restart_after = restart_after + self._start_message = _('Running') + self._failed_message = _('Failed') + self._finished_message = _('Finished') + + self.set_border_width(style.LINE_WIDTH) + width = gtk.gdk.screen_width() + height = gtk.gdk.screen_height() + self.set_size_request(width, height) + self.set_position(gtk.WIN_POS_CENTER_ALWAYS) + self.set_decorated(False) + self.set_resizable(False) + self.set_modal(True) + + self._colored_box = gtk.EventBox() + self._colored_box.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("white")) + self._colored_box.show() + + self._vbox = gtk.VBox() + self._vbox.set_spacing(style.DEFAULT_SPACING) + self._vbox.set_border_width(style.GRID_CELL_SIZE) + + self._colored_box.add(self._vbox) + self.add(self._colored_box) + + self._setup_information() + self._setup_progress_bar() + self._setup_options() + + self._vbox.show() + + self.connect("realize", self.__realize_cb) + + self._process_management = processmanagement.ProcessManagement() + self._process_management.connect('process-management-running', self._set_status_updated) + self._process_management.connect('process-management-started', self._set_status_started) + self._process_management.connect('process-management-finished', self._set_status_finished) + self._process_management.connect('process-management-failed', self._set_status_failed) + + def _setup_information(self): + client = gconf.client_get_default() + color = XoColor(client.get_string('/desktop/sugar/user/color')) + + self._icon = Icon(icon_name='activity-journal', pixel_size=style.XLARGE_ICON_SIZE, xo_color=color) + self._icon.show() + + self._vbox.pack_start(self._icon, False) + + self._title = gtk.Label() + self._title.modify_fg(gtk.STATE_NORMAL, style.COLOR_BLACK.get_gdk_color()) + self._title.set_use_markup(True) + self._title.set_justify(gtk.JUSTIFY_CENTER) + self._title.show() + + self._vbox.pack_start(self._title, False) + + self._message = gtk.Label() + self._message.modify_fg(gtk.STATE_NORMAL, style.COLOR_BLACK.get_gdk_color()) + self._message.set_use_markup(True) + self._message.set_line_wrap(True) + self._message.set_justify(gtk.JUSTIFY_CENTER) + self._message.show() + + self._vbox.pack_start(self._message, True) + + def _setup_options(self): + hbox = gtk.HBox(True, 3) + hbox.show() + + icon = Icon(icon_name='dialog-ok') + + self._start_button = gtk.Button() + self._start_button.set_image(icon) + self._start_button.set_label(_('Start')) + self._start_button.connect('clicked', self.__start_cb) + self._start_button.show() + + icon = Icon(icon_name='dialog-cancel') + + self._close_button = gtk.Button() + self._close_button.set_image(icon) + self._close_button.set_label(_('Cancel')) + self._close_button.connect('clicked', self.__close_cb) + self._close_button.show() + + icon = Icon(icon_name='system-restart') + + self._restart_button = gtk.Button() + self._restart_button.set_image(icon) + self._restart_button.set_label(_('Restart')) + self._restart_button.connect('clicked', self.__restart_cb) + self._restart_button.hide() + + hbox.add(self._start_button) + hbox.add(self._close_button) + hbox.add(self._restart_button) + + halign = gtk.Alignment(1, 0, 0, 0) + halign.show() + halign.add(hbox) + + self._vbox.pack_start(halign, False, False, 3) + + def _setup_progress_bar(self): + alignment = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.5) + alignment.show() + + self._progress_bar = gtk.ProgressBar(adjustment=None) + self._progress_bar.hide() + + alignment.add(self._progress_bar) + self._vbox.pack_start(alignment) + + def __realize_cb(self, widget): + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) + self.window.set_accept_focus(True) + + def __close_cb(self, button): + self.destroy() + + def __start_cb(self, button): + self._process_management.do_process([self._process_script] + self._process_params) + + def __restart_cb(self, button): + session_manager = get_session_manager() + session_manager.logout() + + def _set_status_started(self, model, data=None): + self._message.set_markup(self._start_message) + + self._start_button.hide() + self._close_button.hide() + + self._progress_bar.set_fraction(0.05) + self._progress_bar_handler = gobject.timeout_add(1000, self.__progress_bar_handler_cb) + self._progress_bar.show() + + def __progress_bar_handler_cb(self): + self._progress_bar.pulse() + return True + + def _set_status_updated(self, model, data): + pass + + def _set_status_finished(self, model, data=None): + self._message.set_markup(self._finished_message) + + self._progress_bar.hide() + self._start_button.hide() + + if self._restart_after: + self._restart_button.show() + else: + self._close_button.show() + + def _set_status_failed(self, model, error_message=''): + self._message.set_markup('%s %s' % (self._failed_message, error_message)) + + self._progress_bar.hide() + self._start_button.show() + self._close_button.show() + + logging.error(error_message) + + +class VolumeBackupDialog(ProcessDialog): + + def __init__(self, volume_path): + ProcessDialog.__init__(self, 'journal-backup-volume', \ + [volume_path, misc.get_backup_identifier()], restart_after=False) + + self._resetup_information(volume_path) + + def _resetup_information(self, volume_path): + self._start_message = '%s %s. \n\n' % (_('Please wait, saving Journal content to'), volume_path) + \ + '<big><b>%s</b></big>' % _('Do not remove the storage device!') + + self._finished_message = _('The Journal content has been saved.') + + self._title.set_markup('<big><b>%s</b></big>' % _('Backup')) + + self._message.set_markup('%s %s' % (_('Journal content will be saved to'), volume_path)) + +class VolumeRestoreDialog(ProcessDialog): + + def __init__(self, volume_path): + ProcessDialog.__init__(self, 'journal-restore-volume', \ + [volume_path, misc.get_backup_identifier()]) + + self._resetup_information(volume_path) + + def _resetup_information(self, volume_path): + self._start_message = '%s %s. \n\n' % (_('Please wait, restoring Journal content from'), volume_path) + \ + '<big><b>%s</b></big>' % _('Do not remove the storage device!') + + self._finished_message = _('The Journal content has been restored.') + + self._title.set_markup('<big><b>%s</b></big>' % _('Restore')) + + self._message.set_markup('%s %s.\n\n' % (_('Journal content will be restored from'), volume_path) + \ + '<big><b>%s</b> %s</big>' % (_('Warning:'), _('Current Journal content will be deleted!'))) + diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py index a6acebe..84aeafa 100644 --- a/src/jarabe/journal/volumestoolbar.py +++ b/src/jarabe/journal/volumestoolbar.py @@ -27,7 +27,7 @@ from sugar.graphics.palette import Palette from sugar.graphics.xocolor import XoColor from jarabe.journal import model -from jarabe.view.palettes import VolumePalette +from jarabe.view.palettes import JournalVolumePalette class VolumesToolbar(gtk.Toolbar): __gtype_name__ = 'VolumesToolbar' @@ -184,11 +184,12 @@ class VolumeButton(BaseButton): self.props.xo_color = color def create_palette(self): - palette = VolumePalette(self._mount) + palette = JournalVolumePalette(self._mount) #palette.props.invoker = FrameWidgetInvoker(self) #palette.set_group_id('frame') return palette + class JournalButton(BaseButton): def __init__(self): BaseButton.__init__(self, mount_point='/') diff --git a/src/jarabe/model/Makefile.am b/src/jarabe/model/Makefile.am index e9f0700..8fdc552 100644 --- a/src/jarabe/model/Makefile.am +++ b/src/jarabe/model/Makefile.am @@ -15,4 +15,5 @@ sugar_PYTHON = \ shell.py \ screen.py \ session.py \ - sound.py + sound.py \ + processmanagement.py diff --git a/src/jarabe/model/processmanagement.py b/src/jarabe/model/processmanagement.py new file mode 100644 index 0000000..466e1f6 --- /dev/null +++ b/src/jarabe/model/processmanagement.py @@ -0,0 +1,98 @@ +# Copyright (C) 2010, Paraguay Educa <tecnologia@paraguayeduca.org> +# Copyright (C) 2010, Plan Ceibal <comunidad@plan.ceibal.edu.uy> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import gobject +import glib +import gio + +from sugar import env +from gettext import gettext as _ + +BYTES_TO_READ = 100 + +class ProcessManagement(gobject.GObject): + + __gtype_name__ = 'ProcessManagement' + + __gsignals__ = { + 'process-management-running' : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([str])), + 'process-management-started' : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'process-management-finished' : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'process-management-failed' : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([str])) + } + + def __init__(self): + gobject.GObject.__init__(self) + self._running = False + + def do_process(self, cmd): + self._run_cmd_async(cmd) + + def _report_process_status(self, stream, result): + data = stream.read_finish(result) + + if len(data): + self.emit('process-management-running', data) + stream.read_async(BYTES_TO_READ, self._report_process_status) + + def _report_process_error(self, stream, result, concat_err=''): + data = stream.read_finish(result) + concat_err = concat_err + data + + if len(data) == 0: + self.emit('process-management-failed', concat_err) + else: + stream.read_async(BYTES_TO_READ, self._report_process_error, user_data=concat_err) + + def _notify_error(self, stderr): + stdin_stream = gio.unix.InputStream(stderr, True) + stdin_stream.read_async(BYTES_TO_READ, self._report_process_error) + + def _notify_process_status(self, stdout): + stdin_stream = gio.unix.InputStream(stdout, True) + stdin_stream.read_async(BYTES_TO_READ, self._report_process_status) + + def _run_cmd_async(self, cmd): + if self._running == False: + try: + pid, stdin, stdout, stderr = glib.spawn_async(cmd, flags=glib.SPAWN_DO_NOT_REAP_CHILD, standard_output=True, standard_error=True) + gobject.child_watch_add(pid, _handle_process_end, (self, stderr)) + except Exception: + self.emit('process-management-failed', _("Error - Call process: ") + str(cmd)) + else: + self._notify_process_status(stdout) + self._running = True + self.emit('process-management-started') + +def _handle_process_end(pid, condition, (myself, stderr)): + myself._running = False + + if os.WIFEXITED(condition) and\ + os.WEXITSTATUS(condition) == 0: + myself.emit('process-management-finished') + else: + myself._notify_error(stderr) + +def find_and_absolutize(script_name): + paths = env.os.environ['PATH'].split(':') + for path in paths: + looking_path = path + '/' + script_name + if env.os.path.isfile(looking_path): + return looking_path + + return None diff --git a/src/jarabe/view/palettes.py b/src/jarabe/view/palettes.py index ad84f08..2fc4d5f 100644 --- a/src/jarabe/view/palettes.py +++ b/src/jarabe/view/palettes.py @@ -1,4 +1,6 @@ # Copyright (C) 2008 One Laptop Per Child +# Copyright (C) 2010, Plan Ceibal <comunidad@plan.ceibal.edu.uy> +# Copyright (C) 2010, Paraguay Educa <tecnologia@paraguayeduca.org> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -31,6 +33,7 @@ from sugar.graphics.xocolor import XoColor from sugar.activity import activityfactory from sugar.activity.activityhandle import ActivityHandle +from jarabe.journal.processdialog import VolumeBackupDialog, VolumeRestoreDialog from jarabe.model import shell from jarabe.view import launcher from jarabe.view.viewsource import setup_view_source @@ -258,3 +261,44 @@ class VolumePalette(Palette): self._free_space_label.props.label = _('%(free_space)d MB Free') % \ {'free_space': free_space / (1024 * 1024)} + +class JournalVolumePalette(VolumePalette): + + __gtype_name__ = 'JournalVolumePalette' + + def __init__(self, mount): + VolumePalette.__init__(self, mount) + + journal_separator = gtk.SeparatorMenuItem() + journal_separator.show() + + self.menu.prepend(journal_separator) + + icon = Icon(icon_name='transfer-from', icon_size=gtk.ICON_SIZE_MENU) + icon.show() + + menu_item_journal_restore = MenuItem(_('Restore Journal')) + menu_item_journal_restore.set_image(icon) + menu_item_journal_restore.connect('activate', self.__journal_restore_activate_cb, mount.get_root().get_path()) + menu_item_journal_restore.show() + + self.menu.prepend(menu_item_journal_restore) + + icon = Icon(icon_name='transfer-to', icon_size=gtk.ICON_SIZE_MENU) + icon.show() + + menu_item_journal_backup = MenuItem(_('Backup Journal')) + menu_item_journal_backup.set_image(icon) + menu_item_journal_backup.connect('activate', self.__journal_backup_activate_cb, mount.get_root().get_path()) + menu_item_journal_backup.show() + + self.menu.prepend(menu_item_journal_backup) + + def __journal_backup_activate_cb(self, menu_item, mount_path): + dialog = VolumeBackupDialog(mount_path) + dialog.show() + + def __journal_restore_activate_cb(self, menu_item, mount_path): + dialog = VolumeRestoreDialog(mount_path) + dialog.show() + |