diff options
author | Ajay Garg <ajay@activitycentral.com> | 2012-10-13 16:18:52 (GMT) |
---|---|---|
committer | Ajay Garg <ajay@activitycentral.com> | 2012-10-15 15:48:23 (GMT) |
commit | 81ada148b643f50d645a7ad650608430c1f494cf (patch) | |
tree | b496f98e8653199f6f4d9e3296392eaa7bdb99bd | |
parent | 1fd85e51a40faff942221a9cfde5c4f38152a307 (diff) |
Backup/Restore journal to/from volume(s).
Signed-off-by: Ajay Garg <ajay@activitycentral.com>
-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 | 1 | ||||
-rw-r--r-- | src/jarabe/journal/misc.py | 16 | ||||
-rw-r--r-- | src/jarabe/journal/processdialog.py | 263 | ||||
-rw-r--r-- | src/jarabe/journal/volumestoolbar.py | 4 | ||||
-rw-r--r-- | src/jarabe/model/Makefile.am | 1 | ||||
-rw-r--r-- | src/jarabe/model/processmanagement.py | 120 | ||||
-rw-r--r-- | src/jarabe/view/palettes.py | 46 |
10 files changed, 574 insertions, 5 deletions
diff --git a/bin/Makefile.am b/bin/Makefile.am index 845816c..df53e04 100644 --- a/bin/Makefile.am +++ b/bin/Makefile.am @@ -3,7 +3,9 @@ python_scripts = \ sugar-emulator \ sugar-install-bundle \ sugar-launch \ - sugar-session + sugar-session \ + 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..f3ad6d8 --- /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 98effcf..df8f961 100644 --- a/src/jarabe/journal/Makefile.am +++ b/src/jarabe/journal/Makefile.am @@ -15,5 +15,6 @@ sugar_PYTHON = \ model.py \ objectchooser.py \ palettes.py \ + processdialog.py \ volumestoolbar.py \ webdavmanager.py diff --git a/src/jarabe/journal/misc.py b/src/jarabe/journal/misc.py index 877c1ab..f627c1b 100644 --- a/src/jarabe/journal/misc.py +++ b/src/jarabe/journal/misc.py @@ -40,6 +40,7 @@ from jarabe.journal.journalentrybundle import JournalEntryBundle from jarabe.journal import model from jarabe.journal import journalwindow +_NOT_AVAILABLE = _('Not available') def _get_icon_for_mime(mime_type): generic_types = mime.get_all_generic_types() @@ -318,7 +319,6 @@ def get_xo_serial(): _OFW_TREE = '/ofw' _PROC_TREE = '/proc/device-tree' _SN = 'serial-number' - _not_available = _('Not available') serial_no = None if os.path.exists(os.path.join(_OFW_TREE, _SN)): @@ -327,7 +327,7 @@ def get_xo_serial(): serial_no = read_file(os.path.join(_PROC_TREE, _SN)) if serial_no is None: - serial_no = _not_available + serial_no = _NOT_AVAILABLE # Remove the trailing binary character, else DBUS will crash. return serial_no.rstrip('\x00') @@ -346,3 +346,15 @@ def read_file(path): else: logging.debug('No information in file or directory: %s', path) return None + + +def get_nick(): + client = GConf.Client.get_default() + return client.get_string("/desktop/sugar/user/nick") + + +def get_backup_identifier(): + serial_number = get_xo_serial() + if serial_number is _NOT_AVAILABLE: + serial_number = get_nick() + return serial_number diff --git a/src/jarabe/journal/processdialog.py b/src/jarabe/journal/processdialog.py new file mode 100644 index 0000000..d738303 --- /dev/null +++ b/src/jarabe/journal/processdialog.py @@ -0,0 +1,263 @@ +#!/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/>. + + +from gi.repository import GObject +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import GConf + +import logging + +from gettext import gettext as _ +from sugar3.graphics import style +from sugar3.graphics.icon import Icon +from sugar3.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._prerequisite_message = ('Prerequisites were not met') + + self.set_border_width(style.LINE_WIDTH) + width = Gdk.Screen.width() + height = Gdk.Screen.height() + self.set_size_request(width, height) + self.set_position(Gtk.WindowPosition.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.StateType.NORMAL, Gdk.Color.parse("white")[1]) + 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, False, 0) + + self._title = Gtk.Label() + self._title.modify_fg(Gtk.StateType.NORMAL, style.COLOR_BLACK.get_gdk_color()) + self._title.set_use_markup(True) + self._title.set_justify(Gtk.Justification.CENTER) + self._title.show() + + self._vbox.pack_start(self._title, False, False, 0) + + self._message = Gtk.Label() + self._message.modify_fg(Gtk.StateType.NORMAL, style.COLOR_BLACK.get_gdk_color()) + self._message.set_use_markup(True) + self._message.set_line_wrap(True) + self._message.set_justify(Gtk.Justification.CENTER) + self._message.show() + + self._vbox.pack_start(self._message, True, True, 0) + + 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(_('Close')) + 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(xalign=1, yalign=0, xscale=0, yscale=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() + self._progress_bar.hide() + + alignment.add(self._progress_bar) + self._vbox.pack_start(alignment, False, False, 0) + + def __realize_cb(self, widget): + self.get_window().set_accept_focus(True) + + def __close_cb(self, button): + self.destroy() + + def __start_cb(self, button): + if self._check_prerequisites(): + self._process_management.do_process([self._process_script] + self._process_params) + else: + self._set_status_failed(self, error_message=self._prerequisite_message) + + def __restart_cb(self, button): + session_manager = get_session_manager() + session_manager.logout() + + def _check_prerequisites(self): + return True + + 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=None, 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() + self._restart_button.hide() + + 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!'))) + + self._prerequisite_message = _(', please close all the running activities.') + + def _check_prerequisites(self): + return len(shell.get_model()) <= 2 diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py index ed2a0a3..1bf81bb 100644 --- a/src/jarabe/journal/volumestoolbar.py +++ b/src/jarabe/journal/volumestoolbar.py @@ -37,7 +37,7 @@ from sugar3.graphics.xocolor import XoColor from sugar3 import env from jarabe.journal import model -from jarabe.view.palettes import VolumePalette, RemoteSharePalette +from jarabe.view.palettes import JournalVolumePalette, RemoteSharePalette _JOURNAL_0_METADATA_DIR = '.olpc.store' @@ -445,7 +445,7 @@ 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 diff --git a/src/jarabe/model/Makefile.am b/src/jarabe/model/Makefile.am index 2fc6b1c..d40fb8d 100644 --- a/src/jarabe/model/Makefile.am +++ b/src/jarabe/model/Makefile.am @@ -12,6 +12,7 @@ sugar_PYTHON = \ neighborhood.py \ network.py \ notifications.py \ + processmanagement.py \ shell.py \ screen.py \ session.py \ diff --git a/src/jarabe/model/processmanagement.py b/src/jarabe/model/processmanagement.py new file mode 100644 index 0000000..cb429f6 --- /dev/null +++ b/src/jarabe/model/processmanagement.py @@ -0,0 +1,120 @@ +# 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 + +from gi.repository import GObject +from gi.repository import Gio + +import os +import glib + + +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.SignalFlags.RUN_FIRST, None, ([str])), + 'process-management-started' : (GObject.SignalFlags.RUN_FIRST, None, ([])), + 'process-management-finished' : (GObject.SignalFlags.RUN_FIRST, None, ([])), + 'process-management-failed' : (GObject.SignalFlags.RUN_FIRST, 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, user_data=None): + data = stream.read_finish(result) + + if data != 0: + self.emit('process-management-running', data) + stream.read_async([], + BYTES_TO_READ, + GObject.PRIORITY_LOW, + None, + self._report_process_status, + None) + + def _report_process_error(self, stream, result, concat_err=''): + data = stream.read_finish(result) + concat_err = concat_err + data + + if data != 0: + self.emit('process-management-failed', concat_err) + else: + stream.read_async([], + BYTES_TO_READ, + GObject.PRIORITY_LOW, + None, + self._report_process_error, + concat_err) + + def _notify_error(self, stderr): + stdin_stream = Gio.UnixInputStream(fd=stderr, close_fd=True) + stdin_stream.read_async([], + BYTES_TO_READ, + GObject.PRIORITY_LOW, + None, + self._report_process_error, + None) + + def _notify_process_status(self, stdout): + stdin_stream = Gio.UnixInputStream(fd=stdout, close_fd=True) + stdin_stream.read_async([], + BYTES_TO_READ, + GObject.PRIORITY_LOW, + None, + self._report_process_status, + None) + + 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 4ccf8bc..bbbf822 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 @@ -33,6 +35,7 @@ from sugar3.graphics import style from sugar3.graphics.xocolor import XoColor from sugar3.activity.i18n import pgettext +from jarabe.journal.processdialog import VolumeBackupDialog, VolumeRestoreDialog from jarabe.model import shell from jarabe.view.viewsource import setup_view_source from jarabe.journal import misc @@ -319,3 +322,46 @@ class RemoteSharePalette(Palette): def __popup_cb(self, palette): pass + + + + +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() |