Clean up GUI initialization and add support for restoring corrupted databases automatically

This commit is contained in:
Kovid Goyal 2010-05-08 20:04:54 -06:00
parent 68a3256258
commit 14adce229b
6 changed files with 208 additions and 85 deletions

View File

@ -40,7 +40,7 @@ Run an embedded python interpreter.
return parser return parser
def reinit_db(dbpath): def reinit_db(dbpath, callback=None):
if not os.path.exists(dbpath): if not os.path.exists(dbpath):
raise ValueError(dbpath + ' does not exist') raise ValueError(dbpath + ' does not exist')
from calibre.library.sqlite import connect from calibre.library.sqlite import connect
@ -50,15 +50,26 @@ def reinit_db(dbpath):
uv = conn.get('PRAGMA user_version;', all=False) uv = conn.get('PRAGMA user_version;', all=False)
conn.execute('PRAGMA writable_schema=ON') conn.execute('PRAGMA writable_schema=ON')
conn.commit() conn.commit()
sql = conn.dump() sql_lines = conn.dump()
conn.close() conn.close()
dest = dbpath + '.tmp' dest = dbpath + '.tmp'
try: try:
with closing(connect(dest, False)) as nconn: with closing(connect(dest, False)) as nconn:
nconn.execute('create temporary table temp_sequence(id INTEGER PRIMARY KEY AUTOINCREMENT)') nconn.execute('create temporary table temp_sequence(id INTEGER PRIMARY KEY AUTOINCREMENT)')
nconn.commit() nconn.commit()
nconn.executescript(sql) if callable(callback):
nconn.commit() 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.execute('pragma user_version=%d'%int(uv))
nconn.commit() nconn.commit()
os.remove(dbpath) os.remove(dbpath)

View File

@ -410,6 +410,7 @@ class FileDialog(QObject):
modal = True, modal = True,
name = '', name = '',
mode = QFileDialog.ExistingFiles, mode = QFileDialog.ExistingFiles,
default_dir='~'
): ):
QObject.__init__(self) QObject.__init__(self)
ftext = '' ftext = ''
@ -428,9 +429,10 @@ class FileDialog(QObject):
self.selected_files = None self.selected_files = None
self.fd = 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): if not isinstance(initial_dir, basestring):
initial_dir = os.path.expanduser('~') initial_dir = os.path.expanduser(default_dir)
self.selected_files = [] self.selected_files = []
if mode == QFileDialog.AnyFile: if mode == QFileDialog.AnyFile:
f = unicode(QFileDialog.getSaveFileName(parent, title, initial_dir, ftext, "")) f = unicode(QFileDialog.getSaveFileName(parent, title, initial_dir, ftext, ""))
@ -465,9 +467,10 @@ class FileDialog(QObject):
return tuple(self.selected_files) return tuple(self.selected_files)
def choose_dir(window, name, title): def choose_dir(window, name, title, default_dir='~'):
fd = FileDialog(title, [], False, window, name=name, fd = FileDialog(title=title, filters=[], add_all_files_filter=False,
mode=QFileDialog.DirectoryOnly) parent=window, name=name, mode=QFileDialog.DirectoryOnly,
default_dir=default_dir)
dir = fd.get_files() dir = fd.get_files()
if dir: if dir:
return dir[0] return dir[0]

View File

@ -4,15 +4,18 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, os, time, socket, traceback import sys, os, time, socket, traceback
from functools import partial 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 import prints, plugins
from calibre.constants import iswindows, __appname__, isosx from calibre.constants import iswindows, __appname__, isosx, filesystem_encoding
from calibre.utils.ipc import ADDRESS, RC from calibre.utils.ipc import ADDRESS, RC
from calibre.gui2 import ORG_NAME, APP_UID, initialize_file_icon_provider, \ 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.gui2.main_window import option_parser as _option_parser
from calibre.utils.config import prefs, dynamic from calibre.utils.config import prefs, dynamic
from calibre.library.database2 import LibraryDatabase2
from calibre.library.sqlite import sqlite, DatabaseException
def option_parser(): def option_parser():
parser = _option_parser('''\ parser = _option_parser('''\
@ -48,25 +51,186 @@ def init_qt(args):
app.setWindowIcon(QIcon(I('library.png'))) app.setWindowIcon(QIcon(I('library.png')))
return app, opts, args, actions 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): def run_gui(opts, args, actions, listener, app):
from calibre.gui2.ui import Main
initialize_file_icon_provider() initialize_file_icon_provider()
if not dynamic.get('welcome_wizard_was_run', False): if not dynamic.get('welcome_wizard_was_run', False):
from calibre.gui2.wizard import wizard from calibre.gui2.wizard import wizard
wizard().exec_() wizard().exec_()
dynamic.set('welcome_wizard_was_run', True) dynamic.set('welcome_wizard_was_run', True)
main = Main(listener, opts, actions) runner = GuiRunner(opts, args, actions, listener, app)
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
ret = app.exec_() 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 from calibre.gui2.wizard import wizard
wizard().exec_() 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] e = sys.executable if getattr(sys, 'frozen', False) else sys.argv[0]
print 'Restarting with:', e, sys.argv print 'Restarting with:', e, sys.argv
if hasattr(sys, 'frameworks_dir'): if hasattr(sys, 'frameworks_dir'):
@ -78,7 +242,7 @@ def run_gui(opts, args, actions, listener, app):
else: else:
if iswindows: if iswindows:
try: try:
main.system_tray_icon.hide() runner.main.system_tray_icon.hide()
except: except:
pass pass
return ret return ret

View File

@ -14,9 +14,9 @@ from xml.parsers.expat import ExpatError
from Queue import Queue, Empty from Queue import Queue, Empty
from threading import Thread from threading import Thread
from functools import partial 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, \ QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \
QToolButton, QDialog, QDesktopServices, QFileDialog, \ QToolButton, QDialog, QDesktopServices, \
QSystemTrayIcon, QApplication, QKeySequence, QAction, \ QSystemTrayIcon, QApplication, QKeySequence, QAction, \
QMessageBox, QStackedLayout, QHelpEvent, QInputDialog,\ QMessageBox, QStackedLayout, QHelpEvent, QInputDialog,\
QThread, pyqtSignal QThread, pyqtSignal
@ -125,8 +125,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.default_thumbnail = (pixmap.width(), pixmap.height(), self.default_thumbnail = (pixmap.width(), pixmap.height(),
pixmap_to_data(pixmap)) 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.preferences_action, self.quit_action = actions
self.library_path = library_path
self.spare_servers = [] self.spare_servers = []
MainWindow.__init__(self, opts, parent) MainWindow.__init__(self, opts, parent)
# Initialize fontconfig in a separate thread as this can be a lengthy # 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: if self.system_tray_icon.isVisible() and opts.start_in_tray:
self.hide_windows() self.hide_windows()
self.stack.setCurrentIndex(0) 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) self.library_view.set_database(db)
prefs['library_path'] = self.library_path prefs['library_path'] = self.library_path
self.library_view.sortByColumn(*dynamic.get('sort_column', self.library_view.sortByColumn(*dynamic.get('sort_column',
@ -2330,38 +2306,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
d.show() d.show()
self._modeless_dialogs.append(d) 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): def read_settings(self):
self.initialize_database()
geometry = config['main_window_geometry'] geometry = config['main_window_geometry']
if geometry is not None: if geometry is not None:
self.restoreGeometry(geometry) self.restoreGeometry(geometry)

View File

@ -1399,7 +1399,7 @@ books_series_link feeds
def check_integrity(self, callback): def check_integrity(self, callback):
callback(0., _('Checking SQL integrity...')) callback(0., _('Checking SQL integrity...'))
user_version = self.user_version user_version = self.user_version
sql = self.conn.dump() sql = '\n'.join(self.conn.dump())
self.conn.close() self.conn.close()
dest = self.dbpath+'.tmp' dest = self.dbpath+'.tmp'
if os.path.exists(dest): if os.path.exists(dest):

View File

@ -116,7 +116,7 @@ class DBThread(Thread):
break break
if func == 'dump': if func == 'dump':
try: try:
ok, res = True, '\n'.join(self.conn.iterdump()) ok, res = True, tuple(self.conn.iterdump())
except Exception, err: except Exception, err:
ok, res = False, (err, traceback.format_exc()) ok, res = False, (err, traceback.format_exc())
else: else: