Add 'Library maintenance' to the library dropdown menu

1) move check integrity to here
2) move check library to here
3) move mark dirty to here
4) add a stub recover_database command
5) improve help message for cli calibredb restore_database, and add the --really-do-it option
This commit is contained in:
Charles Haley 2010-10-08 13:04:59 +01:00
parent 3ad46e0018
commit 76cd695669
5 changed files with 141 additions and 126 deletions

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
import os, shutil import os, shutil
from functools import partial from functools import partial
from PyQt4.Qt import QMenu, Qt, QInputDialog from PyQt4.Qt import QMenu, Qt, QInputDialog, QThread, pyqtSignal, QProgressDialog
from calibre import isbytestring from calibre import isbytestring
from calibre.constants import filesystem_encoding from calibre.constants import filesystem_encoding
@ -16,6 +16,7 @@ from calibre.utils.config import prefs
from calibre.gui2 import gprefs, warning_dialog, Dispatcher, error_dialog, \ from calibre.gui2 import gprefs, warning_dialog, Dispatcher, error_dialog, \
question_dialog, info_dialog question_dialog, info_dialog
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
from calibre.gui2.dialogs.check_library import CheckLibraryDialog
class LibraryUsageStats(object): # {{{ class LibraryUsageStats(object): # {{{
@ -75,6 +76,72 @@ class LibraryUsageStats(object): # {{{
self.write_stats() self.write_stats()
# }}} # }}}
# Check Integrity {{{
class VacThread(QThread):
check_done = pyqtSignal(object, object)
callback = pyqtSignal(object, object)
def __init__(self, parent, db):
QThread.__init__(self, parent)
self.db = db
self._parent = parent
def run(self):
err = bad = None
try:
bad = self.db.check_integrity(self.callbackf)
except:
import traceback
err = traceback.format_exc()
self.check_done.emit(bad, err)
def callbackf(self, progress, msg):
self.callback.emit(progress, msg)
class CheckIntegrity(QProgressDialog):
def __init__(self, db, parent=None):
QProgressDialog.__init__(self, parent)
self.db = db
self.setCancelButton(None)
self.setMinimum(0)
self.setMaximum(100)
self.setWindowTitle(_('Checking database integrity'))
self.setAutoReset(False)
self.setValue(0)
self.vthread = VacThread(self, db)
self.vthread.check_done.connect(self.check_done,
type=Qt.QueuedConnection)
self.vthread.callback.connect(self.callback, type=Qt.QueuedConnection)
self.vthread.start()
def callback(self, progress, msg):
self.setLabelText(msg)
self.setValue(int(100*progress))
def check_done(self, bad, err):
if err:
error_dialog(self, _('Error'),
_('Failed to check database integrity'),
det_msg=err, show=True)
elif bad:
titles = [self.db.title(x, index_is_id=True) for x in bad]
det_msg = '\n'.join(titles)
warning_dialog(self, _('Some inconsistencies found'),
_('The following books had formats listed in the '
'database that are not actually available. '
'The entries for the formats have been removed. '
'You should check them manually. This can '
'happen if you manipulate the files in the '
'library folder directly.'), det_msg=det_msg, show=True)
self.reset()
# }}}
class ChooseLibraryAction(InterfaceAction): class ChooseLibraryAction(InterfaceAction):
name = 'Choose Library' name = 'Choose Library'
@ -117,11 +184,28 @@ class ChooseLibraryAction(InterfaceAction):
self.rename_separator = self.choose_menu.addSeparator() self.rename_separator = self.choose_menu.addSeparator()
self.create_action(spec=(_('Library backup status...'), 'lt.png', None, self.maintenance_menu = QMenu(_('Library Maintenance'))
None), attr='action_backup_status') ac = self.create_action(spec=(_('Library metadata backup status'),
self.action_backup_status.triggered.connect(self.backup_status, 'lt.png', None, None), attr='action_backup_status')
type=Qt.QueuedConnection) ac.triggered.connect(self.backup_status, type=Qt.QueuedConnection)
self.choose_menu.addAction(self.action_backup_status) self.maintenance_menu.addAction(ac)
ac = self.create_action(spec=(_('Start backing up metadata of all books'),
'lt.png', None, None), attr='action_backup_metadata')
ac.triggered.connect(self.mark_dirty, type=Qt.QueuedConnection)
self.maintenance_menu.addAction(ac)
ac = self.create_action(spec=(_('Check library'), 'lt.png',
None, None), attr='action_check_library')
ac.triggered.connect(self.check_library, type=Qt.QueuedConnection)
self.maintenance_menu.addAction(ac)
ac = self.create_action(spec=(_('Check database integrity'), 'lt.png',
None, None), attr='action_check_database')
ac.triggered.connect(self.check_database, type=Qt.QueuedConnection)
self.maintenance_menu.addAction(ac)
ac = self.create_action(spec=(_('Recover database'), 'lt.png',
None, None), attr='action_restore_database')
ac.triggered.connect(self.restore_database, type=Qt.QueuedConnection)
self.maintenance_menu.addAction(ac)
self.choose_menu.addMenu(self.maintenance_menu)
def library_name(self): def library_name(self):
db = self.gui.library_view.model().db db = self.gui.library_view.model().db
@ -234,6 +318,37 @@ class ChooseLibraryAction(InterfaceAction):
_('Book metadata files remaining to be written: %s') % dirty_text, _('Book metadata files remaining to be written: %s') % dirty_text,
show=True) show=True)
def mark_dirty(self):
db = self.gui.library_view.model().db
db.dirtied(list(db.data.iterallids()))
info_dialog(self.gui, _('Backup metadata'),
_('Metadata will be backed up while calibre is running, at the '
'rate of approximately 1 book per second.'), show=True)
def check_library(self):
db = self.gui.library_view.model().db
d = CheckLibraryDialog(self.gui.parent(), db)
d.exec_()
def check_database(self, *args):
m = self.gui.library_view.model()
m.stop_metadata_backup()
try:
d = CheckIntegrity(m.db, self.gui)
d.exec_()
finally:
m.start_metadata_backup()
def restore_database(self):
info_dialog(self.gui, _('Recover database'),
_(
'This command rebuilds your calibre database from the information '
'stored by calibre in the OPF files.' + '<p>' +
'This function is not currently available in the GUI. You can '
'recover your database using the \'calibredb restore_database\' '
'command line function.'
), show=True)
def switch_requested(self, location): def switch_requested(self, location):
if not self.change_library_allowed(): if not self.change_library_allowed():
return return

View File

@ -32,7 +32,7 @@ class CheckLibraryDialog(QDialog):
self.copy = QPushButton(_('Copy to clipboard')) self.copy = QPushButton(_('Copy to clipboard'))
self.copy.setDefault(False) self.copy.setDefault(False)
self.copy.clicked.connect(self.copy_to_clipboard) self.copy.clicked.connect(self.copy_to_clipboard)
self.ok = QPushButton('&OK') self.ok = QPushButton('&Done')
self.ok.setDefault(True) self.ok.setDefault(True)
self.ok.clicked.connect(self.accept) self.ok.clicked.connect(self.accept)
self.cancel = QPushButton('&Cancel') self.cancel = QPushButton('&Cancel')

View File

@ -5,81 +5,14 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QProgressDialog, QThread, Qt, pyqtSignal
from calibre.gui2.dialogs.check_library import CheckLibraryDialog
from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.misc_ui import Ui_Form from calibre.gui2.preferences.misc_ui import Ui_Form
from calibre.gui2 import error_dialog, config, warning_dialog, \ from calibre.gui2 import error_dialog, config, open_local_file, info_dialog
open_local_file, info_dialog
from calibre.constants import isosx from calibre.constants import isosx
# Check Integrity {{{ # Check Integrity {{{
class VacThread(QThread):
check_done = pyqtSignal(object, object)
callback = pyqtSignal(object, object)
def __init__(self, parent, db):
QThread.__init__(self, parent)
self.db = db
self._parent = parent
def run(self):
err = bad = None
try:
bad = self.db.check_integrity(self.callbackf)
except:
import traceback
err = traceback.format_exc()
self.check_done.emit(bad, err)
def callbackf(self, progress, msg):
self.callback.emit(progress, msg)
class CheckIntegrity(QProgressDialog):
def __init__(self, db, parent=None):
QProgressDialog.__init__(self, parent)
self.db = db
self.setCancelButton(None)
self.setMinimum(0)
self.setMaximum(100)
self.setWindowTitle(_('Checking database integrity'))
self.setAutoReset(False)
self.setValue(0)
self.vthread = VacThread(self, db)
self.vthread.check_done.connect(self.check_done,
type=Qt.QueuedConnection)
self.vthread.callback.connect(self.callback, type=Qt.QueuedConnection)
self.vthread.start()
def callback(self, progress, msg):
self.setLabelText(msg)
self.setValue(int(100*progress))
def check_done(self, bad, err):
if err:
error_dialog(self, _('Error'),
_('Failed to check database integrity'),
det_msg=err, show=True)
elif bad:
titles = [self.db.title(x, index_is_id=True) for x in bad]
det_msg = '\n'.join(titles)
warning_dialog(self, _('Some inconsistencies found'),
_('The following books had formats listed in the '
'database that are not actually available. '
'The entries for the formats have been removed. '
'You should check them manually. This can '
'happen if you manipulate the files in the '
'library folder directly.'), det_msg=det_msg, show=True)
self.reset()
# }}}
class ConfigWidget(ConfigWidgetBase, Ui_Form): class ConfigWidget(ConfigWidgetBase, Ui_Form):
def genesis(self, gui): def genesis(self, gui):
@ -88,39 +21,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('worker_limit', config, restart_required=True) r('worker_limit', config, restart_required=True)
r('enforce_cpu_limit', config, restart_required=True) r('enforce_cpu_limit', config, restart_required=True)
self.device_detection_button.clicked.connect(self.debug_device_detection) self.device_detection_button.clicked.connect(self.debug_device_detection)
self.compact_button.clicked.connect(self.compact)
self.button_all_books_dirty.clicked.connect(self.mark_dirty)
self.button_check_library.clicked.connect(self.check_library)
self.button_open_config_dir.clicked.connect(self.open_config_dir) self.button_open_config_dir.clicked.connect(self.open_config_dir)
self.button_osx_symlinks.clicked.connect(self.create_symlinks) self.button_osx_symlinks.clicked.connect(self.create_symlinks)
self.button_osx_symlinks.setVisible(isosx) self.button_osx_symlinks.setVisible(isosx)
def mark_dirty(self):
db = self.gui.library_view.model().db
db.dirtied(list(db.data.iterallids()))
info_dialog(self, _('Backup metadata'),
_('Metadata will be backed up while calibre is running, at the '
'rate of 30 books per minute.'), show=True)
def check_library(self):
db = self.gui.library_view.model().db
d = CheckLibraryDialog(self.gui.parent(), db)
d.exec_()
def debug_device_detection(self, *args): def debug_device_detection(self, *args):
from calibre.gui2.preferences.device_debug import DebugDevice from calibre.gui2.preferences.device_debug import DebugDevice
d = DebugDevice(self) d = DebugDevice(self)
d.exec_() d.exec_()
def compact(self, *args):
m = self.gui.library_view.model()
m.stop_metadata_backup()
try:
d = CheckIntegrity(m.db, self)
d.exec_()
finally:
m.start_metadata_backup()
def open_config_dir(self, *args): def open_config_dir(self, *args):
from calibre.utils.config import config_dir from calibre.utils.config import config_dir
open_local_file(config_dir) open_local_file(config_dir)

View File

@ -77,13 +77,6 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item row="5" column="0" colspan="2">
<widget class="QPushButton" name="compact_button">
<property name="text">
<string>&amp;Check database integrity</string>
</property>
</widget>
</item>
<item row="6" column="0"> <item row="6" column="0">
<spacer name="verticalSpacer_7"> <spacer name="verticalSpacer_7">
<property name="orientation"> <property name="orientation">
@ -124,20 +117,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="10" column="0" colspan="2">
<widget class="QPushButton" name="button_all_books_dirty">
<property name="text">
<string>Back up metadata of all books</string>
</property>
</widget>
</item>
<item row="11" column="0" colspan="2">
<widget class="QPushButton" name="button_check_library">
<property name="text">
<string>Check the library folders for potential problems</string>
</property>
</widget>
</item>
<item row="20" column="0"> <item row="20" column="0">
<spacer name="verticalSpacer_9"> <spacer name="verticalSpacer_9">
<property name="orientation"> <property name="orientation">

View File

@ -961,13 +961,19 @@ def restore_database_option_parser():
''' '''
%prog restore_database [options] %prog restore_database [options]
Restore this database from the metadata stored in OPF Restore this database from the metadata stored in OPF files in each
files in each directory of the calibre library. This is directory of the calibre library. This is useful if your metadata.db file
useful if your metadata.db file has been corrupted. has been corrupted.
WARNING: This completely regenerates your database. You will WARNING: This command completely regenerates your database. You will lose
lose stored per-book conversion settings and custom recipes. all saved searches, user categories, plugboards, stored per-book conversion
settings, and custom recipes. Restored metadata will only be as accurate as
what is found in the OPF files.
''')) '''))
parser.add_option('-r', '--really-do-it', default=False, action='store_true',
help=_('Really do the recovery. The command will not run '
'unless this option is specified.'))
return parser return parser
def command_restore_database(args, dbpath): def command_restore_database(args, dbpath):
@ -978,6 +984,12 @@ def command_restore_database(args, dbpath):
parser.print_help() parser.print_help()
return 1 return 1
if not opts.really_do_it:
print _('You must provide the --really-do-it option to do a recovery\n')
parser.print_help()
return 1
return
if opts.library_path is not None: if opts.library_path is not None:
dbpath = opts.library_path dbpath = opts.library_path
@ -1028,7 +1040,7 @@ information is the equivalent of what is shown in the tags pane.
parser.add_option('-r', '--categories', default='', dest='report', parser.add_option('-r', '--categories', default='', dest='report',
help=_("Comma-separated list of category lookup names.\n" help=_("Comma-separated list of category lookup names.\n"
"Default: all")) "Default: all"))
parser.add_option('-w', '--line-width', default=-1, type=int, parser.add_option('-w', '--idth', default=-1, type=int,
help=_('The maximum width of a single line in the output. ' help=_('The maximum width of a single line in the output. '
'Defaults to detecting screen size.')) 'Defaults to detecting screen size.'))
parser.add_option('-s', '--separator', default=',', parser.add_option('-s', '--separator', default=',',