From 14adce229b026bafa9725536aa45906f51d513af Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 8 May 2010 20:04:54 -0600 Subject: [PATCH] Clean up GUI initialization and add support for restoring corrupted databases automatically --- src/calibre/debug.py | 19 ++- src/calibre/gui2/__init__.py | 13 ++- src/calibre/gui2/main.py | 194 ++++++++++++++++++++++++++++--- src/calibre/gui2/ui.py | 63 +--------- src/calibre/library/database2.py | 2 +- src/calibre/library/sqlite.py | 2 +- 6 files changed, 208 insertions(+), 85 deletions(-) diff --git a/src/calibre/debug.py b/src/calibre/debug.py index 9945add7c8..c84ce3dfcc 100644 --- a/src/calibre/debug.py +++ b/src/calibre/debug.py @@ -40,7 +40,7 @@ Run an embedded python interpreter. return parser -def reinit_db(dbpath): +def reinit_db(dbpath, callback=None): if not os.path.exists(dbpath): raise ValueError(dbpath + ' does not exist') from calibre.library.sqlite import connect @@ -50,15 +50,26 @@ def reinit_db(dbpath): uv = conn.get('PRAGMA user_version;', all=False) conn.execute('PRAGMA writable_schema=ON') conn.commit() - sql = conn.dump() + sql_lines = conn.dump() conn.close() dest = dbpath + '.tmp' try: with closing(connect(dest, False)) as nconn: nconn.execute('create temporary table temp_sequence(id INTEGER PRIMARY KEY AUTOINCREMENT)') nconn.commit() - nconn.executescript(sql) - nconn.commit() + if callable(callback): + callback(len(sql_lines), True) + for i, line in enumerate(sql_lines): + try: + nconn.execute(line) + except: + import traceback + prints('SQL line %r failed with error:'%line) + prints(traceback.format_exc()) + continue + finally: + if callable(callback): + callback(i, False) nconn.execute('pragma user_version=%d'%int(uv)) nconn.commit() os.remove(dbpath) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 5aad257711..94f969d7e9 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -410,6 +410,7 @@ class FileDialog(QObject): modal = True, name = '', mode = QFileDialog.ExistingFiles, + default_dir='~' ): QObject.__init__(self) ftext = '' @@ -428,9 +429,10 @@ class FileDialog(QObject): self.selected_files = None self.fd = None - initial_dir = dynamic.get(self.dialog_name, os.path.expanduser('~')) + initial_dir = dynamic.get(self.dialog_name, + os.path.expanduser(default_dir)) if not isinstance(initial_dir, basestring): - initial_dir = os.path.expanduser('~') + initial_dir = os.path.expanduser(default_dir) self.selected_files = [] if mode == QFileDialog.AnyFile: f = unicode(QFileDialog.getSaveFileName(parent, title, initial_dir, ftext, "")) @@ -465,9 +467,10 @@ class FileDialog(QObject): return tuple(self.selected_files) -def choose_dir(window, name, title): - fd = FileDialog(title, [], False, window, name=name, - mode=QFileDialog.DirectoryOnly) +def choose_dir(window, name, title, default_dir='~'): + fd = FileDialog(title=title, filters=[], add_all_files_filter=False, + parent=window, name=name, mode=QFileDialog.DirectoryOnly, + default_dir=default_dir) dir = fd.get_files() if dir: return dir[0] diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index b71f6e6922..c261c38dcf 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -4,15 +4,18 @@ __copyright__ = '2008, Kovid Goyal ' import sys, os, time, socket, traceback from functools import partial -from PyQt4.Qt import QCoreApplication, QIcon, QMessageBox +from PyQt4.Qt import QCoreApplication, QIcon, QMessageBox, QObject, QTimer, \ + QThread, pyqtSignal, Qt, QProgressDialog, QString -from calibre import prints -from calibre.constants import iswindows, __appname__, isosx +from calibre import prints, plugins +from calibre.constants import iswindows, __appname__, isosx, filesystem_encoding from calibre.utils.ipc import ADDRESS, RC from calibre.gui2 import ORG_NAME, APP_UID, initialize_file_icon_provider, \ - Application + Application, choose_dir, error_dialog, question_dialog from calibre.gui2.main_window import option_parser as _option_parser from calibre.utils.config import prefs, dynamic +from calibre.library.database2 import LibraryDatabase2 +from calibre.library.sqlite import sqlite, DatabaseException def option_parser(): parser = _option_parser('''\ @@ -48,25 +51,186 @@ def init_qt(args): app.setWindowIcon(QIcon(I('library.png'))) return app, opts, args, actions +def get_library_path(): + library_path = prefs['library_path'] + if library_path is None: # Need to migrate to new database layout + base = os.path.expanduser('~') + if iswindows: + base = plugins['winutil'][0].special_folder_path( + plugins['winutil'][0].CSIDL_PERSONAL) + if not base or not os.path.exists(base): + from PyQt4.Qt import QDir + base = unicode(QDir.homePath()).replace('/', os.sep) + candidate = choose_dir(None, 'choose calibre library', + _('Choose a location for your calibre e-book library'), + default_dir=base) + if not candidate: + candidate = os.path.join(base, 'Calibre Library') + library_path = os.path.abspath(candidate) + if not os.path.exists(library_path): + try: + os.makedirs(library_path) + except: + error_dialog(None, _('Failed to create library'), + _('Failed to create calibre library at: %r. Aborting.')%library_path, + det_msg = traceback.print_exc(), show=True) + library_path = None + return library_path + +class DBRepair(QThread): + + repair_done = pyqtSignal(object, object) + progress = pyqtSignal(object, object) + + def __init__(self, library_path, parent, pd): + QThread.__init__(self, parent) + self.library_path = library_path + self.pd = pd + self.progress.connect(self._callback, type=Qt.QueuedConnection) + + def _callback(self, num, is_length): + if is_length: + self.pd.setRange(0, num-1) + num = 0 + self.pd.setValue(num) + + def callback(self, num, is_length): + self.progress.emit(num, is_length) + + def run(self): + from calibre.debug import reinit_db + try: + reinit_db(os.path.join(self.library_path, 'metadata.db'), + self.callback) + db = LibraryDatabase2(self.library_path) + tb = None + except: + db, tb = None, traceback.format_exc() + self.repair_done.emit(db, tb) + +class GuiRunner(QObject): + '''Make sure an event loop is running before starting the main work of + initialization''' + + def __init__(self, opts, args, actions, listener, app): + self.opts, self.args, self.listener, self.app = opts, args, listener, app + self.actions = actions + self.main = None + QObject.__init__(self) + self.timer = QTimer.singleShot(1, self.initialize) + + def start_gui(self): + from calibre.gui2.ui import Main + main = Main(self.library_path, self.db, self.listener, self.opts, self.actions) + add_filesystem_book = partial(main.add_filesystem_book, allow_device=False) + sys.excepthook = main.unhandled_exception + if len(self.args) > 1: + p = os.path.abspath(self.args[1]) + add_filesystem_book(p) + self.app.file_event_hook = add_filesystem_book + self.main = main + + def initialization_failed(self): + print 'Catastrophic failure initializing GUI, bailing out...' + QCoreApplication.exit(1) + raise SystemExit(1) + + def initialize_db_stage2(self, db, tb): + repair_pd = getattr(self, 'repair_pd', None) + if repair_pd is not None: + repair_pd.cancel() + + if db is None and tb is not None: + # DB Repair failed + error_dialog(None, _('Repairing failed'), + _('The database repair failed. Starting with ' + 'a new empty library.'), + det_msg=tb, show=True) + if db is None: + fname = _('Calibre Library') + if isinstance(fname, unicode): + try: + fname = fname.encode(filesystem_encoding) + except: + fname = 'Calibre Library' + x = os.path.expanduser('~'+os.sep+fname) + if not os.path.exists(x): + try: + os.makedirs(x) + except: + x = os.path.expanduser('~') + candidate = choose_dir(None, 'choose calibre library', + _('Choose a location for your new calibre e-book library'), + default_dir=x) + + if not candidate: + self.initialization_failed() + + try: + self.library_path = candidate + db = LibraryDatabase2(candidate) + except: + error_dialog(None, _('Bad database location'), + _('Bad database location %r. calibre will now quit.' + )%self.library_path, + det_msg=traceback.format_exc(), show=True) + self.initialization_failed() + + self.db = db + self.start_gui() + + def initialize_db(self): + db = None + try: + db = LibraryDatabase2(self.library_path) + except (sqlite.Error, DatabaseException): + repair = question_dialog(None, _('Corrupted database'), + _('Your calibre database appears to be corrupted. Do ' + 'you want calibre to try and repair it automatically? ' + 'If you say No, a new empty calibre library will be created.'), + det_msg=traceback.format_exc() + ) + if repair: + self.repair_pd = QProgressDialog(_('Repairing database. This ' + 'can take a very long time for a large collection'), QString(), + 0, 0) + self.repair_pd.setWindowModality(Qt.WindowModal) + self.repair_pd.show() + + self.repair = DBRepair(self.library_path, self, self.repair_pd) + self.repair.repair_done.connect(self.initialize_db_stage2, + type=Qt.QueuedConnection) + self.repair.start() + return + except: + error_dialog(None, _('Bad database location'), + _('Bad database location %r. Will start with ' + ' a new, empty calibre library')%self.library_path, + det_msg=traceback.format_exc(), show=True) + + self.initialize_db_stage2(db, None) + + def initialize(self, *args): + self.library_path = get_library_path() + if self.library_path is None: + self.initialization_failed() + + self.initialize_db() + + + def run_gui(opts, args, actions, listener, app): - from calibre.gui2.ui import Main initialize_file_icon_provider() if not dynamic.get('welcome_wizard_was_run', False): from calibre.gui2.wizard import wizard wizard().exec_() dynamic.set('welcome_wizard_was_run', True) - main = Main(listener, opts, actions) - add_filesystem_book = partial(main.add_filesystem_book, allow_device=False) - sys.excepthook = main.unhandled_exception - if len(args) > 1: - args[1] = os.path.abspath(args[1]) - add_filesystem_book(args[1]) - app.file_event_hook = add_filesystem_book + runner = GuiRunner(opts, args, actions, listener, app) ret = app.exec_() - if getattr(main, 'run_wizard_b4_shutdown', False): + if getattr(runner.main, 'run_wizard_b4_shutdown', False): from calibre.gui2.wizard import wizard wizard().exec_() - if getattr(main, 'restart_after_quit', False): + if getattr(runner.main, 'restart_after_quit', False): e = sys.executable if getattr(sys, 'frozen', False) else sys.argv[0] print 'Restarting with:', e, sys.argv if hasattr(sys, 'frameworks_dir'): @@ -78,7 +242,7 @@ def run_gui(opts, args, actions, listener, app): else: if iswindows: try: - main.system_tray_icon.hide() + runner.main.system_tray_icon.hide() except: pass return ret diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 62178c4fc4..bf520f62e7 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -14,9 +14,9 @@ from xml.parsers.expat import ExpatError from Queue import Queue, Empty from threading import Thread from functools import partial -from PyQt4.Qt import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer, \ +from PyQt4.Qt import Qt, SIGNAL, QObject, QUrl, QTimer, \ QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \ - QToolButton, QDialog, QDesktopServices, QFileDialog, \ + QToolButton, QDialog, QDesktopServices, \ QSystemTrayIcon, QApplication, QKeySequence, QAction, \ QMessageBox, QStackedLayout, QHelpEvent, QInputDialog,\ QThread, pyqtSignal @@ -125,8 +125,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.default_thumbnail = (pixmap.width(), pixmap.height(), pixmap_to_data(pixmap)) - def __init__(self, listener, opts, actions, parent=None): + def __init__(self, library_path, db, listener, opts, actions, parent=None): self.preferences_action, self.quit_action = actions + self.library_path = library_path self.spare_servers = [] MainWindow.__init__(self, opts, parent) # Initialize fontconfig in a separate thread as this can be a lengthy @@ -513,31 +514,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): if self.system_tray_icon.isVisible() and opts.start_in_tray: self.hide_windows() self.stack.setCurrentIndex(0) - try: - db = LibraryDatabase2(self.library_path) - except Exception: - import traceback - error_dialog(self, _('Bad database location'), - _('Bad database location')+':'+self.library_path, - det_msg=traceback.format_exc()).exec_() - fname = _('Calibre Library') - if isinstance(fname, unicode): - try: - fname = fname.encode(filesystem_encoding) - except: - fname = 'Calibre Library' - x = os.path.expanduser('~'+os.sep+fname) - if not os.path.exists(x): - os.makedirs(x) - dir = unicode(QFileDialog.getExistingDirectory(self, - _('Choose a location for your ebook library.'), - x)) - if not dir: - QCoreApplication.exit(1) - raise SystemExit(1) - else: - self.library_path = dir - db = LibraryDatabase2(self.library_path) self.library_view.set_database(db) prefs['library_path'] = self.library_path self.library_view.sortByColumn(*dynamic.get('sort_column', @@ -2330,38 +2306,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): d.show() self._modeless_dialogs.append(d) - - def initialize_database(self): - self.library_path = prefs['library_path'] - if self.library_path is None: # Need to migrate to new database layout - base = os.path.expanduser('~') - if iswindows: - from calibre import plugins - from PyQt4.Qt import QDir - base = plugins['winutil'][0].special_folder_path( - plugins['winutil'][0].CSIDL_PERSONAL) - if not base or not os.path.exists(base): - base = unicode(QDir.homePath()).replace('/', os.sep) - dir = unicode(QFileDialog.getExistingDirectory(self, - _('Choose a location for your ebook library.'), base)) - if not dir: - dir = os.path.expanduser('~/Library') - self.library_path = os.path.abspath(dir) - if not os.path.exists(self.library_path): - try: - os.makedirs(self.library_path) - except: - self.library_path = os.path.expanduser('~/CalibreLibrary') - error_dialog(self, _('Invalid library location'), - _('Could not access %s. Using %s as the library.')% - (repr(self.library_path), repr(self.library_path)) - ).exec_() - if not os.path.exists(self.library_path): - os.makedirs(self.library_path) - - def read_settings(self): - self.initialize_database() geometry = config['main_window_geometry'] if geometry is not None: self.restoreGeometry(geometry) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index f375b3345a..8606a91cca 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1399,7 +1399,7 @@ books_series_link feeds def check_integrity(self, callback): callback(0., _('Checking SQL integrity...')) user_version = self.user_version - sql = self.conn.dump() + sql = '\n'.join(self.conn.dump()) self.conn.close() dest = self.dbpath+'.tmp' if os.path.exists(dest): diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py index 9718cab872..1e937499fb 100644 --- a/src/calibre/library/sqlite.py +++ b/src/calibre/library/sqlite.py @@ -116,7 +116,7 @@ class DBThread(Thread): break if func == 'dump': try: - ok, res = True, '\n'.join(self.conn.iterdump()) + ok, res = True, tuple(self.conn.iterdump()) except Exception, err: ok, res = False, (err, traceback.format_exc()) else: