diff --git a/gramps/gen/config.py b/gramps/gen/config.py index e4ab366ff..d4a31333a 100644 --- a/gramps/gen/config.py +++ b/gramps/gen/config.py @@ -159,7 +159,9 @@ register('behavior.addons-url', "https://raw.githubusercontent.com/gramps-projec register('database.backend', 'bsddb') register('database.compress-backup', True) -register('database.autobackup', True) ## make backup when exiting, if there are changes +register('database.backup-path', USER_HOME) +register('database.backup-on-exit', True) +register('database.autobackup', 0) register('database.path', os.path.join(HOME_DIR, 'grampsdb')) register('export.proxy-order', diff --git a/gramps/gen/db/base.py b/gramps/gen/db/base.py index bded6d380..1e34048a1 100644 --- a/gramps/gen/db/base.py +++ b/gramps/gen/db/base.py @@ -1995,23 +1995,3 @@ class DbWriteBase(DbReadBase): person.birth_ref_index = birth_ref_index person.death_ref_index = death_ref_index - - def autobackup(self, user=None): - """ - Backup the current file as a backup file. - """ - from gramps.cli.user import User - if user is None: - user = User() - if self.is_open() and self.has_changed: - if user.uistate: - user.uistate.set_busy_cursor(True) - user.uistate.progress.show() - user.uistate.push_message(user.dbstate, _("Autobackup...")) - try: - self.backup(user=user) - except DbException as msg: - user.notify_error(_("Error saving backup data"), msg) - if user.uistate: - user.uistate.set_busy_cursor(False) - user.uistate.progress.hide() diff --git a/gramps/gen/db/generic.py b/gramps/gen/db/generic.py index 5b72c7220..a4240283a 100644 --- a/gramps/gen/db/generic.py +++ b/gramps/gen/db/generic.py @@ -646,8 +646,6 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): """ if self._directory: if update: - if config.get('database.autobackup'): - self.autobackup(user) # This is just a dummy file to indicate last modified time of # the database for gramps.cli.clidbman: filename = os.path.join(self._directory, "meta_data.db") @@ -2576,20 +2574,6 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): def redo(self, update_history=True): return self.undodb.redo(update_history) - def backup(self, user=None): - """ - If you wish to support an optional backup routine, put it here. - """ - from gramps.plugins.export.exportxml import XmlWriter - from gramps.cli.user import User - if user is None: - user = User() - compress = config.get('database.compress-backup') - writer = XmlWriter(self, user, strip_photos=0, compress=compress) - timestamp = '{0:%Y-%m-%d-%H-%M-%S}'.format(datetime.datetime.now()) - filename = os.path.join(self._directory, "backup-%s.gramps" % timestamp) - writer.write(filename) - def get_summary(self): """ Returns dictionary of summary item. diff --git a/gramps/gui/configure.py b/gramps/gui/configure.py index 6af9f2185..e5e5c17c2 100644 --- a/gramps/gui/configure.py +++ b/gramps/gui/configure.py @@ -1239,6 +1239,11 @@ class GrampsPreferences(ConfigureDialog): def date_calendar_changed(self, obj): config.set('preferences.calendar-format-report', obj.get_active()) + def autobackup_changed(self, obj): + active = obj.get_active() + config.set('database.autobackup', active) + self.uistate.set_autobackup_timer() + def add_date_panel(self, configdialog): grid = Gtk.Grid() grid.set_border_width(12) @@ -1464,6 +1469,33 @@ class GrampsPreferences(ConfigureDialog): current_line, 'behavior.autoload') current_line += 1 + self.backup_path_entry = Gtk.Entry() + self.add_path_box(grid, + _('Backup path'), + current_line, self.backup_path_entry, + config.get('database.backup-path'), + self.set_backup_path, self.select_backup_path) + current_line += 1 + + self.add_checkbox(grid, + _('Backup on exit'), + current_line, 'database.backup-on-exit') + current_line += 1 + + # Check for updates: + obox = Gtk.ComboBoxText() + formats = [_("Never"), + _("Every 15 minutes"), + _("Every 30 minutes"), + _("Every hour")] + list(map(obox.append_text, formats)) + active = config.get('database.autobackup') + obox.set_active(active) + obox.connect('changed', self.autobackup_changed) + lwidget = BasicLabel("%s: " % _('Autobackup')) + grid.attach(lwidget, 1, current_line, 1, 1) + grid.attach(obox, 2, current_line, 1, 1) + return _('Family Tree'), grid def __create_backend_combo(self): @@ -1543,6 +1575,31 @@ class GrampsPreferences(ConfigureDialog): self.dbpath_entry.set_text(val) f.destroy() + def set_backup_path(self, *obj): + path = self.backup_path_entry.get_text().strip() + config.set('database.backup-path', path) + + def select_backup_path(self, *obj): + f = Gtk.FileChooserDialog(title=_("Select backup directory"), + parent=self.window, + action=Gtk.FileChooserAction.SELECT_FOLDER, + buttons=(_('_Cancel'), + Gtk.ResponseType.CANCEL, + _('_Apply'), + Gtk.ResponseType.OK) + ) + backup_path = config.get('database.backup-path') + if not backup_path: + backup_path = config.get('database.path') + f.set_current_folder(os.path.dirname(backup_path)) + + status = f.run() + if status == Gtk.ResponseType.OK: + val = f.get_filename() + if val: + self.backup_path_entry.set_text(val) + f.destroy() + def update_idformat_entry(self, obj, constant): config.set(constant, obj.get_text()) self.dbstate.db.set_prefixes( diff --git a/gramps/gui/displaystate.py b/gramps/gui/displaystate.py index 971ccb072..d57337a97 100644 --- a/gramps/gui/displaystate.py +++ b/gramps/gui/displaystate.py @@ -371,6 +371,7 @@ class DisplayState(Callback): 'nameformat-changed' : None, 'grampletbar-close-changed' : None, 'update-available' : (list, ), + 'autobackup' : None, } #nav_type to message @@ -409,6 +410,7 @@ class DisplayState(Callback): self.disprel_active = None self.set_relationship_class() self.export = False + self.backup_timer = None formatter = logging.Formatter('%(levelname)s %(name)s: %(message)s') warnbtn = status.get_warning_button() @@ -421,6 +423,31 @@ class DisplayState(Callback): # but this connection is still made! # self.dbstate.connect('database-changed', self.db_changed) + def set_backup_timer(self): + """ + Set the backup timer. + """ + interval = config.get('database.autobackup') + if self.backup_timer is not None: + GLib.source_remove(self.backup_timer) + self.backup_timer = None + if interval == 1: + minutes = 15 + elif interval == 2: + minutes = 30 + elif interval == 3: + minutes = 60 + if interval > 0: + self.backup_timer = GLib.timeout_add_seconds( + minutes*60, self.__emit_autobackup) + + def __emit_autobackup(self): + """ + Emit an 'autobackup' signal. + """ + self.emit('autobackup') + return True + def screen_width(self): """ Return the width of the current screen. diff --git a/gramps/gui/viewmanager.py b/gramps/gui/viewmanager.py index abfe1a5c4..2adb98913 100644 --- a/gramps/gui/viewmanager.py +++ b/gramps/gui/viewmanager.py @@ -91,6 +91,7 @@ from .aboutdialog import GrampsAboutDialog from .navigator import Navigator from .views.tags import Tags from .actiongroup import ActionGroup +from gramps.gen.db.exceptions import DbWriteFailure #------------------------------------------------------------------------- # @@ -311,6 +312,9 @@ class ViewManager(CLIManager): # Need to call after plugins have been registered self.uistate.connect('update-available', self.process_updates) self.check_for_updates() + # Set autobackup + self.uistate.connect('autobackup', self.autobackup) + self.uistate.set_backup_timer() def check_for_updates(self): """ @@ -750,7 +754,11 @@ class ViewManager(CLIManager): # mark interface insenstitive to prevent unexpected events self.uistate.set_sensitive(False) - # backup data, and close the database + # backup data + if config.get('database.backup-on-exit'): + self.autobackup() + + # close the database if self.dbstate.is_open(): self.dbstate.db.close(user=self.user) @@ -1400,6 +1408,37 @@ class ViewManager(CLIManager): self.uistate.push_message(self.dbstate, _("Backup aborted")) window.destroy() + def autobackup(self): + """ + Backup the current family tree. + """ + if self.dbstate.db.is_open() and self.dbstate.db.has_changed: + self.uistate.set_busy_cursor(True) + self.uistate.progress.show() + self.uistate.push_message(self.dbstate, _("Autobackup...")) + try: + self.__backup() + except DbWriteFailure as msg: + self.uistate.push_message(self.dbstate, + _("Error saving backup data")) + self.uistate.set_busy_cursor(False) + self.uistate.progress.hide() + + def __backup(self): + """ + Backup database to a Gramps XML file. + """ + from gramps.plugins.export.exportxml import XmlWriter + backup_path = config.get('database.backup-path') + compress = config.get('database.compress-backup') + writer = XmlWriter(self.dbstate.db, self.user, strip_photos=0, + compress=compress) + timestamp = '{0:%Y-%m-%d-%H-%M-%S}'.format(datetime.datetime.now()) + backup_name = "%s-%s.gramps" % (self.dbstate.db.get_dbname(), + timestamp) + filename = os.path.join(backup_path, backup_name) + writer.write(filename) + def select_backup_path(self, widget, path_entry): """ Choose a backup folder. Make sure there is one highlighted in diff --git a/gramps/plugins/db/bsddb/write.py b/gramps/plugins/db/bsddb/write.py index 1a88877be..f8cc0563b 100644 --- a/gramps/plugins/db/bsddb/write.py +++ b/gramps/plugins/db/bsddb/write.py @@ -1507,7 +1507,6 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): """ if not self.db_is_open: return - self.autobackup(user) if self.txn: self.transaction_abort(self.transaction) self.env.txn_checkpoint() diff --git a/gramps/plugins/db/dbapi/inmemorydb.py b/gramps/plugins/db/dbapi/inmemorydb.py index 837c07efc..35f371b0f 100644 --- a/gramps/plugins/db/dbapi/inmemorydb.py +++ b/gramps/plugins/db/dbapi/inmemorydb.py @@ -54,13 +54,6 @@ class InMemoryDB(DBAPI): with open(versionpath, "w") as version_file: version_file.write(str(self.VERSION)) - def autobackup(self, user=None): - """ - Nothing to do, as we write it out anyway. - No backups for inmemory databases. - """ - pass - def load(self, directory, callback=None, mode=None, force_schema_upgrade=False, force_bsddb_upgrade=False,