sqlite3.OperationalError: database or disk is full

For https://gramps-project.org/bugs/view.php?id=12306
1. Add check disk space before close db, save config file, autobackup and XML
backup.
2. Add new function get_avail_disk_size().

I have problem: I can't restore connection to window in quit() if user abort to
quit.
This commit is contained in:
Stanislav Bolshakov 2021-05-23 20:31:34 +03:00
parent 24a763b1f9
commit 8e219d525a
2 changed files with 213 additions and 15 deletions

View File

@ -1,3 +1,5 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# #
# Gramps - a GTK+/GNOME based genealogy program # Gramps - a GTK+/GNOME based genealogy program
# #
@ -86,6 +88,27 @@ def get_new_filename(ext, folder='~/'):
ix = ix + 1 ix = ix + 1
return os.path.expanduser(_NEW_NAME_PATTERN % (folder, os.path.sep, ix, ext)) return os.path.expanduser(_NEW_NAME_PATTERN % (folder, os.path.sep, ix, ext))
# TODO check this function, add 'try - exeption', add check "dbfolder are present?"
def get_avail_disk_size(path_to_folder):
"""
Check available disk space in MB
"""
if win(): # for windows:
# import psutil
# DISK = get disk from 'dbfolder'
# freedisk = psutil.disk_usage(DISK).free/(1024*1024)
# print(f"{freedisk:.4} Mb free on disk {DISK}")
pass
elif mac(): # for mac:
pass
else: # for linux:
fd = os.open(path_to_folder, os.O_RDONLY)
stats = os.fstatvfs(fd)
os.close(fd)
avail_disk = round(stats[1] * stats[4]/(1024*1024))
return avail_disk
def get_empty_tempdir(dirname): def get_empty_tempdir(dirname):
""" Return path to TEMP_DIR/dirname, a guaranteed empty directory """ Return path to TEMP_DIR/dirname, a guaranteed empty directory

View File

@ -1,3 +1,5 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# #
# Gramps - a GTK+/GNOME based genealogy program # Gramps - a GTK+/GNOME based genealogy program
# #
@ -86,7 +88,7 @@ from gramps.gen.errors import WindowActiveError
from .dialog import ErrorDialog, WarningDialog, QuestionDialog2, InfoDialog from .dialog import ErrorDialog, WarningDialog, QuestionDialog2, InfoDialog
from .widgets import Statusbar from .widgets import Statusbar
from .undohistory import UndoHistory from .undohistory import UndoHistory
from gramps.gen.utils.file import media_path_full from gramps.gen.utils.file import media_path_full, get_avail_disk_size
from .dbloader import DbLoader from .dbloader import DbLoader
from .display import display_help, display_url from .display import display_help, display_url
from .configure import GrampsPreferences from .configure import GrampsPreferences
@ -594,11 +596,57 @@ class ViewManager(CLIManager):
# backup data # backup data
if config.get('database.backup-on-exit'): if config.get('database.backup-on-exit'):
self.autobackup() # ******************
if not self.autobackup():
if QuestionDialog2(_("Backup on quit fail"),
_("Your backup disk have not place for backup file.\n"
"Do you want resume quit without backup?"),
_("Abort of Quit"), _("Resume quit without backup"), parent=self.uistate.window).run():
# TODO Restore connections to window
# From code above
# the following prevents premature closing of main window if user
# hits 'x' multiple times.
# self.window.connect('delete-event', self.no_del_event)
# the following prevents reentering quit if user hits 'x' again
# self.window.disconnect(self.del_event)
# mark interface insenstitive to prevent unexpected events
self.uistate.set_sensitive(True)
# TODO
# For Abort of Quit: restore saved values for restore databese_changed
self.prev_has_changed = self.prev_has_changed1
self.dbstate.db.has_changed = self.dbstate.db.has_changed1
return
# ******************
# close the database # close the database
if self.dbstate.is_open(): if self.dbstate.is_open():
self.dbstate.db.close(user=self.user) #****************
# Need disk space:
# - for Linux 20 MB,
# - for sqllite and BsdDb - ? (PostgresDB?),
# - for backup - ('last_backup_file_size' + 5%) or (8% from sqllite and BsdDb file), PostgresDB?
diskspace = 20
if(get_avail_disk_size(config.get('database.path')) < diskspace):
self.uistate.push_message(self.dbstate, _("Disk space < {num} MB. Quit impossible.").format(num=diskspace))
WarningDialog(_("Low disk space:"),
_('You have less {num} MB on system disk:\n').format(num=diskspace) +
config.get('database.path') + '\n' +
_('Gramps need more space for close database before quit.\n') +
_('Clear your disk now and repeat quit again.'), parent=self.uistate.window)
# TODO Restore connections to window
# From code above
# the following prevents premature closing of main window if user
# hits 'x' multiple times.
# self.window.connect('delete-event', self.no_del_event)
# the following prevents reentering quit if user hits 'x' again
# self.window.disconnect(self.del_event)
# mark interface insenstitive to prevent unexpected events
self.uistate.set_sensitive(True)
return
else:
# ***********
self.dbstate.db.close(user=self.user)
# have each page save anything, if they need to: # have each page save anything, if they need to:
self.__delete_pages() self.__delete_pages()
@ -611,7 +659,30 @@ class ViewManager(CLIManager):
(horiz_position, vert_position) = self.window.get_position() (horiz_position, vert_position) = self.window.get_position()
config.set('interface.main-window-horiz-position', horiz_position) config.set('interface.main-window-horiz-position', horiz_position)
config.set('interface.main-window-vert-position', vert_position) config.set('interface.main-window-vert-position', vert_position)
config.save() #****************
# Need disk space for config file - 5 kB
diskspace = 1
if(get_avail_disk_size(config.get('database.path')) < diskspace):
self.uistate.push_message(self.dbstate, _("Disk space < {num} MB. Quit impossible.").format(num=diskspace))
WarningDialog(_("Low disk space:"),
_('You have less {num} MB on system disk:\n').format(num=diskspace) +
config.get('database.path') + '\n' +
_('Gramps need more space for save config file before quit.\n') +
_('Clear your disk now and repeat quit again.'), parent=self.uistate.window)
# TODO Restore connections to window
# From code above
# the following prevents premature closing of main window if user
# hits 'x' multiple times.
# self.window.connect('delete-event', self.no_del_event)
# the following prevents reentering quit if user hits 'x' again
# self.window.disconnect(self.del_event)
# mark interface insenstitive to prevent unexpected events
self.uistate.set_sensitive(True)
return
else:
# ***********
config.save()
self.app.quit() self.app.quit()
def abort(self, *obj): def abort(self, *obj):
@ -1203,13 +1274,19 @@ class ViewManager(CLIManager):
elif interval == 5: elif interval == 5:
seconds = 86400. # (24 hours) 1440min *60 seconds = 86400. # (24 hours) 1440min *60
now = time.time() now = time.time()
# TODO
# For Abort of Quit: save values for restore databese_changed
self.prev_has_changed1 = self.prev_has_changed
self.dbstate.db.has_changed1 = self.dbstate.db.has_changed
if interval and now > self.autobackup_time + seconds + 300.: if interval and now > self.autobackup_time + seconds + 300.:
# we have been delayed by more than 5 minutes # we have been delayed by more than 5 minutes
# so we have probably been awakened from sleep/hibernate # so we have probably been awakened from sleep/hibernate
# we should delay a bit more to let the system settle # we should delay a bit more to let the system settle
self.delay_timer = GLib.timeout_add_seconds(300, self.autobackup) self.delay_timer = GLib.timeout_add_seconds(300, self.autobackup)
self.autobackup_time = now self.autobackup_time = now
return return True
self.autobackup_time = now self.autobackup_time = now
# Only backup if more commits since last time # Only backup if more commits since last time
if(self.dbstate.db.is_open() and if(self.dbstate.db.is_open() and
@ -1217,15 +1294,45 @@ class ViewManager(CLIManager):
self.prev_has_changed = self.dbstate.db.has_changed self.prev_has_changed = self.dbstate.db.has_changed
self.uistate.set_busy_cursor(True) self.uistate.set_busy_cursor(True)
self.uistate.progress.show() self.uistate.progress.show()
self.uistate.push_message(self.dbstate, _("Autobackup...")) self.uistate.push_message(self.dbstate, _("Autobackup begin..."))
# TODO
# This 'try - exept' not work if disk full. Gramps freeze.
try: try:
self.__backup() backup_result = self.__backup()
except DbWriteFailure as msg: except DbWriteFailure as msg:
self.uistate.push_message(self.dbstate, self.uistate.push_message(self.dbstate,
_("Error saving backup data")) _("Error saving backup data"))
if backup_result > 0:
WarningDialog(_("Low backup disk space"),
_('You have not space on backup disk:\n') +
config.get('database.backup-path') + '\n' +
_('Gramps need least {num} MB for save backup file.\n').format(num=backup_result) +
_('What you can do now:\n') +
_('1. Clear your backup disk,\n') +
_('2. Check backup disk space,\n') +
_('3. Press button below.\n') +
_('Gramps try create backup file again.'), parent=self.uistate.window)
backup_result = self.__backup()
self.uistate.set_busy_cursor(False) self.uistate.set_busy_cursor(False)
self.uistate.progress.hide() self.uistate.progress.hide()
if backup_result > 0:
self.uistate.push_message(self.dbstate, _("Autobackup fail."))
# TODO
# Print this to log file
print('{0:%Y-%m-%d-%H-%M-%S}'.format(datetime.datetime.now()) + ' ' + _("Autobackup fail."))
return False
else:
self.uistate.push_message(self.dbstate, _("Autobackup successfully."))
# TODO
# Print this to log file
print('{0:%Y-%m-%d-%H-%M-%S}'.format(datetime.datetime.now()) + ' ' + _("Autobackup successfully."))
return True
else:
return True
def __backup(self): def __backup(self):
""" """
Backup database to a Gramps XML file. Backup database to a Gramps XML file.
@ -1239,7 +1346,62 @@ class ViewManager(CLIManager):
backup_name = "%s-%s.gramps" % (self.dbstate.db.get_dbname(), backup_name = "%s-%s.gramps" % (self.dbstate.db.get_dbname(),
timestamp) timestamp)
filename = os.path.join(backup_path, backup_name) filename = os.path.join(backup_path, backup_name)
writer.write(filename)
#**********
# TODO
# Need disk space - for sqllite and BsdDb - ? MB, for backup - ('last_backup_file_size' + 5%) or (8% from sqllite and BsdDb file)
# Backup for PostgresDB? I don't know aboit it size. It's must checked separelly.
# We search last time backup file.
bpath = sorted(os.listdir(backup_path), reverse=True)
blastfile = ""
for bfile in bpath:
if(os.path.isfile(os.path.join(backup_path, bfile)) and
bfile.startswith(self.dbstate.db.get_dbname()) and bfile.endswith('.gramps')):
blastfile = bfile
break
# Do we have last backup file for active database?
if not blastfile:
# TODO
# For me: bfile_size = active_database_size/100*8 (8% from active database Sqlite file).
# In this case we not need use last_backup_file_size. It's can be bettter solve.
# It's very important for big database (after create new and import data). We alllime use real size.
bfile_size = 5
# bfile_size = os.stat(active_database).st_size/(1024*1024)
# bfile_size = round(bfile_size/100*8)
else:
bfile_size = os.path.getsize(os.path.join(backup_path, blastfile))/(1024*1024)
bfile_new_size = round(bfile_size/100*105)
if bfile_new_size == 0: # If last backup file present but file size less 1 MB
bfile_new_size = 1
diskspace = bfile_new_size
if(get_avail_disk_size(backup_path) < diskspace):
self.uistate.push_message(self.dbstate, _("Low backup disk space (<{num} kB). Backup impossible.").format(num=diskspace))
return bfile_new_size
else:
#***********
# TODO Problem:
# 1. Gramps - begin writer.write(filename),
# 2. Thunar - copy big file,
# 3. My backup disk - full, Thunar - say me 'errror, disk full', Gramps me - 'nothing, silent, freeze'.
# I add 'try - except':
try:
writer.write(filename)
return 0
except:
WarningDialog(_("Low backup disk space"),
_('You have not space on backup disk:\n') +
config.get('database.backup-path') + '\n' +
_('Gramps need least {num} MB for save backup file.\n').format(num=bfile_new_size) +
_('Gramps break backup procedure.'), parent=self.uistate.window)
# TODO
# If backup create not full backup file - this file must be deleted as corrupted:
os.remove(filename)
return bfile_new_size
def reports_clicked(self, *obj): def reports_clicked(self, *obj):
""" """
@ -1674,7 +1836,7 @@ class QuickBackup(ManagedWindow): # TODO move this class into its own module
def __init__(self, dbstate, uistate, user): def __init__(self, dbstate, uistate, user):
""" """
Make a quick XML back with or without media. Make a quick XML backup with or without media.
""" """
self.dbstate = dbstate self.dbstate = dbstate
self.user = user self.user = user
@ -1773,7 +1935,7 @@ class QuickBackup(ManagedWindow): # TODO move this class into its own module
filename = os.path.join(path_entry.get_text(), basefile) filename = os.path.join(path_entry.get_text(), basefile)
if os.path.exists(filename): if os.path.exists(filename):
question = QuestionDialog2( question = QuestionDialog2(
_("Backup file already exists! Overwrite?"), _("XML Backup file already exists! Overwrite?"),
_("The file '%s' exists.") % filename, _("The file '%s' exists.") % filename,
_("Proceed and overwrite"), _("Proceed and overwrite"),
_("Cancel the backup"), _("Cancel the backup"),
@ -1791,7 +1953,7 @@ class QuickBackup(ManagedWindow): # TODO move this class into its own module
self.uistate.set_busy_cursor(True) self.uistate.set_busy_cursor(True)
self.uistate.pulse_progressbar(0) self.uistate.pulse_progressbar(0)
self.uistate.progress.show() self.uistate.progress.show()
self.uistate.push_message(self.dbstate, _("Making backup...")) self.uistate.push_message(self.dbstate, _("Making XML backup..."))
if include.get_active(): if include.get_active():
from gramps.plugins.export.exportpkg import PackageWriter from gramps.plugins.export.exportpkg import PackageWriter
writer = PackageWriter(self.dbstate.db, filename, self.user) writer = PackageWriter(self.dbstate.db, filename, self.user)
@ -1800,14 +1962,26 @@ class QuickBackup(ManagedWindow): # TODO move this class into its own module
from gramps.plugins.export.exportxml import XmlWriter from gramps.plugins.export.exportxml import XmlWriter
writer = XmlWriter(self.dbstate.db, self.user, writer = XmlWriter(self.dbstate.db, self.user,
strip_photos=0, compress=1) strip_photos=0, compress=1)
writer.write(filename) # TODO
# I add 'try - except'?
xml_backup_string = _("XML backup saved to '%s'") % filename
try:
writer.write(filename)
except:
xml_backup_string = _("XML backup aborted")
WarningDialog(_("Low XML backup disk space"),
_('You have not space on XML backup disk:\n') +
config.get('paths.quick-backup-directory') + '\n' +
_('Gramps break XML backup procedure.'), parent=self.uistate.window)
os.remove(filename)
self.uistate.set_busy_cursor(False) self.uistate.set_busy_cursor(False)
self.uistate.progress.hide() self.uistate.progress.hide()
self.uistate.push_message(self.dbstate, self.uistate.push_message(self.dbstate, xml_backup_string)
_("Backup saved to '%s'") % filename)
config.set('paths.quick-backup-directory', path_entry.get_text()) config.set('paths.quick-backup-directory', path_entry.get_text())
else: else:
self.uistate.push_message(self.dbstate, _("Backup aborted")) self.uistate.push_message(self.dbstate, _("XML backup aborted"))
if dbackup != Gtk.ResponseType.DELETE_EVENT: if dbackup != Gtk.ResponseType.DELETE_EVENT:
self.close() self.close()
@ -1848,3 +2022,4 @@ class QuickBackup(ManagedWindow): # TODO move this class into its own module
file_entry.set_text("%s.%s" % (base, extension)) file_entry.set_text("%s.%s" % (base, extension))
else: else:
file_entry.set_text("%s.%s" % (filename, extension)) file_entry.set_text("%s.%s" % (filename, extension))