Restore corrupted-database detection during SQL queries
Show a popup if DB corruption is detected with a warning to avoid storing the data directory in cloud storage, a link to a support page with more information, and a button to restore from the last automatic backup. Also show the warning about cloud storage again after restoring from the last automatic backup.
This commit is contained in:
parent
a79d1c2114
commit
1e0ad3aba3
4 changed files with 207 additions and 139 deletions
|
@ -37,7 +37,7 @@ Zotero.DBConnection = function(dbNameOrPath) {
|
|||
}
|
||||
|
||||
this.MAX_BOUND_PARAMETERS = 999;
|
||||
this.DB_CORRUPTION_STRING = "2152857611";
|
||||
this.DB_CORRUPTION_STRING = "database disk image is malformed";
|
||||
|
||||
Components.utils.import("resource://gre/modules/Sqlite.jsm", this);
|
||||
|
||||
|
@ -658,6 +658,8 @@ Zotero.DBConnection.prototype.queryAsync = Zotero.Promise.coroutine(function* (s
|
|||
}
|
||||
}
|
||||
catch (e) {
|
||||
yield this._checkException(e);
|
||||
|
||||
if (e.errors && e.errors[0]) {
|
||||
var eStr = e + "";
|
||||
eStr = eStr.indexOf("Error: ") == 0 ? eStr.substr(7): e;
|
||||
|
@ -861,42 +863,9 @@ Zotero.DBConnection.prototype.integrityCheck = Zotero.Promise.coroutine(function
|
|||
});
|
||||
|
||||
|
||||
Zotero.DBConnection.prototype.checkException = function (e) {
|
||||
if (this._externalDB) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.message.includes(this.DB_CORRUPTION_STRING)) {
|
||||
// Write corrupt marker to data directory
|
||||
var file = Zotero.File.pathToFile(this._dbPath + '.is.corrupt');
|
||||
Zotero.File.putContents(file, '');
|
||||
|
||||
this._dbIsCorrupt = true;
|
||||
|
||||
var ps = Services.prompt;
|
||||
|
||||
var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
|
||||
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING);
|
||||
|
||||
var index = ps.confirmEx(null,
|
||||
Zotero.getString('general.error'),
|
||||
Zotero.getString('db.dbCorrupted', this._dbName) + '\n\n' + Zotero.getString('db.dbCorrupted.restart', Zotero.appName),
|
||||
buttonFlags,
|
||||
Zotero.getString('general.restartNow'),
|
||||
Zotero.getString('general.restartLater'),
|
||||
null, null, {});
|
||||
|
||||
if (index == 0) {
|
||||
var appStartup = Services.startup;
|
||||
appStartup.quit(Components.interfaces.nsIAppStartup.eRestart);
|
||||
appStartup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit);
|
||||
}
|
||||
|
||||
Zotero.skipLoading = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Zotero.DBConnection.prototype.isCorruptionError = function (e) {
|
||||
return e.message.includes(this.DB_CORRUPTION_STRING);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
|
@ -1141,11 +1110,9 @@ Zotero.DBConnection.prototype._getConnectionAsync = async function (options) {
|
|||
var store = Services.storage;
|
||||
|
||||
var file = this._dbPath;
|
||||
var backupFile = this._dbPath + '.bak';
|
||||
var fileName = OS.Path.basename(file);
|
||||
var corruptMarker = this._dbPath + '.is.corrupt';
|
||||
|
||||
catchBlock: try {
|
||||
try {
|
||||
if (await OS.File.exists(corruptMarker)) {
|
||||
throw new Error(this.DB_CORRUPTION_STRING);
|
||||
}
|
||||
|
@ -1162,100 +1129,12 @@ Zotero.DBConnection.prototype._getConnectionAsync = async function (options) {
|
|||
Zotero.logError(e);
|
||||
|
||||
if (e.message.includes(this.DB_CORRUPTION_STRING)) {
|
||||
this._debug(`Database file '${fileName}' corrupted`, 1);
|
||||
|
||||
// No backup file! Eek!
|
||||
if (!await OS.File.exists(backupFile)) {
|
||||
this._debug("No backup file for DB '" + this._dbName + "' exists", 1);
|
||||
|
||||
// Save damaged filed
|
||||
this._debug('Saving damaged DB file with .damaged extension', 1);
|
||||
let damagedFile = this._dbPath + '.damaged';
|
||||
await Zotero.File.moveToUnique(file, damagedFile);
|
||||
|
||||
// Create new main database
|
||||
this._connection = store.openDatabase(file);
|
||||
|
||||
if (await OS.File.exists(corruptMarker)) {
|
||||
await OS.File.remove(corruptMarker);
|
||||
await this._handleCorruptionMarker();
|
||||
}
|
||||
|
||||
Zotero.alert(
|
||||
null,
|
||||
Zotero.getString('startupError'),
|
||||
Zotero.getString('db.dbCorruptedNoBackup', fileName)
|
||||
);
|
||||
break catchBlock;
|
||||
}
|
||||
|
||||
// Save damaged file
|
||||
this._debug('Saving damaged DB file with .damaged extension', 1);
|
||||
let damagedFile = this._dbPath + '.damaged';
|
||||
await Zotero.File.moveToUnique(file, damagedFile);
|
||||
|
||||
// Test the backup file
|
||||
try {
|
||||
Zotero.debug("Asynchronously opening DB connection");
|
||||
this._connection = await Zotero.Promise.resolve(this.Sqlite.openConnection({
|
||||
path: backupFile
|
||||
}));
|
||||
}
|
||||
// Can't open backup either
|
||||
catch (e) {
|
||||
// Create new main database
|
||||
this._connection = await Zotero.Promise.resolve(this.Sqlite.openConnection({
|
||||
path: file
|
||||
}));
|
||||
|
||||
Zotero.alert(
|
||||
null,
|
||||
Zotero.getString('general.error'),
|
||||
Zotero.getString('db.dbRestoreFailed', fileName)
|
||||
);
|
||||
|
||||
if (await OS.File.exists(corruptMarker)) {
|
||||
await OS.File.remove(corruptMarker);
|
||||
}
|
||||
|
||||
break catchBlock;
|
||||
}
|
||||
|
||||
this._connection = undefined;
|
||||
|
||||
// Copy backup file to main DB file
|
||||
this._debug("Restoring database '" + this._dbName + "' from backup file", 1);
|
||||
try {
|
||||
await OS.File.copy(backupFile, file);
|
||||
}
|
||||
catch (e) {
|
||||
// TODO: deal with low disk space
|
||||
throw (e);
|
||||
}
|
||||
|
||||
// Open restored database
|
||||
this._connection = await Zotero.Promise.resolve(this.Sqlite.openConnection({
|
||||
path: file
|
||||
}));
|
||||
this._debug('Database restored', 1);
|
||||
Zotero.alert(
|
||||
null,
|
||||
Zotero.getString('general.warning'),
|
||||
Zotero.getString('db.dbRestored', [
|
||||
fileName,
|
||||
Zotero.Date.getFileDateString(Zotero.File.pathToFile(backupFile)),
|
||||
Zotero.Date.getFileTimeString(Zotero.File.pathToFile(backupFile))
|
||||
])
|
||||
);
|
||||
|
||||
if (await OS.File.exists(corruptMarker)) {
|
||||
await OS.File.remove(corruptMarker);
|
||||
}
|
||||
|
||||
break catchBlock;
|
||||
}
|
||||
|
||||
else {
|
||||
// Some other error that we don't yet know how to deal with
|
||||
throw (e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._externalDB) {
|
||||
|
@ -1287,6 +1166,185 @@ Zotero.DBConnection.prototype._getConnectionAsync = async function (options) {
|
|||
};
|
||||
|
||||
|
||||
Zotero.DBConnection.prototype._checkException = async function (e) {
|
||||
if (this._externalDB || !this.isCorruptionError(e)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const supportURL = 'https://zotero.org/support/kb/corrupted_database';
|
||||
|
||||
var filename = OS.Path.basename(this._dbPath);
|
||||
// Skip backups
|
||||
this._dbIsCorrupt = true;
|
||||
|
||||
var backupDate = null;
|
||||
var backupTime = null;
|
||||
try {
|
||||
let info = await OS.File.stat(this._dbPath + '.bak');
|
||||
backupDate = info.lastModificationDate.toLocaleDateString();
|
||||
backupTime = info.lastModificationDate.toLocaleTimeString();
|
||||
Zotero.debug(`Found ${this._dbPath} with date of ${backupDate}`);
|
||||
}
|
||||
catch (e) {}
|
||||
|
||||
var ps = Services.prompt;
|
||||
var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
|
||||
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL;
|
||||
|
||||
var index = ps.confirmEx(null,
|
||||
Zotero.getString('general.error'),
|
||||
Zotero.getString('db.dbCorrupted', [Zotero.appName, filename]) + '\n\n'
|
||||
+ Zotero.getString('db.dbCorrupted.cloudStorage', Zotero.appName) + '\n\n'
|
||||
+ (backupDate
|
||||
? Zotero.getString(
|
||||
'db.dbCorrupted.restoreFromLastAutomaticBackup',
|
||||
[Zotero.appName, backupDate, backupTime]
|
||||
) + '\n\n'
|
||||
+ Zotero.getString('db.dbCorrupted.viewMoreInformation', supportURL)
|
||||
: Zotero.getString('db.dbCorrupted.repairOrRestore', Zotero.appName)),
|
||||
buttonFlags,
|
||||
backupDate ? Zotero.getString('db.dbCorrupted.automaticBackup') : Zotero.getString('general.moreInformation'),
|
||||
null,
|
||||
null,
|
||||
null, {});
|
||||
|
||||
if (index == 0) {
|
||||
// Write corrupt marker to data directory
|
||||
let file = Zotero.File.pathToFile(this._dbPath + '.is.corrupt');
|
||||
Zotero.File.putContents(file, '');
|
||||
Zotero.skipLoading = true;
|
||||
Zotero.Utilities.Internal.quit(true);
|
||||
}
|
||||
else if (index == 1) {
|
||||
}
|
||||
else {
|
||||
Zotero.launchURL(supportURL);
|
||||
Zotero.Utilities.Internal.quit();
|
||||
Zotero.skipLoading = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {Boolean} - True if recovered, false if not
|
||||
*/
|
||||
Zotero.DBConnection.prototype._handleCorruptionMarker = async function () {
|
||||
var storage = Services.storage;
|
||||
var file = this._dbPath;
|
||||
var fileName = OS.Path.basename(file);
|
||||
var backupFile = this._dbPath + '.bak';
|
||||
var corruptMarker = this._dbPath + '.is.corrupt';
|
||||
|
||||
this._debug(`Database file '${fileName}' corrupted`, 1);
|
||||
|
||||
// No backup file! Eek!
|
||||
if (!await OS.File.exists(backupFile)) {
|
||||
this._debug("No backup file for DB '" + this._dbName + "' exists", 1);
|
||||
|
||||
// Save damaged filed
|
||||
this._debug('Saving damaged DB file with .damaged extension', 1);
|
||||
let damagedFile = this._dbPath + '.damaged';
|
||||
damagedFile = await Zotero.File.moveToUnique(file, damagedFile);
|
||||
|
||||
// Create new main database
|
||||
this._connection = storage.openDatabase(file);
|
||||
|
||||
if (await OS.File.exists(corruptMarker)) {
|
||||
await OS.File.remove(corruptMarker);
|
||||
}
|
||||
|
||||
Zotero.alert(
|
||||
null,
|
||||
Zotero.getString('startupError'),
|
||||
Zotero.getString(
|
||||
'db.dbCorruptedNoBackup',
|
||||
[Zotero.appName, fileName, OS.Path.basename(damagedFile)]
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save damaged file
|
||||
this._debug('Saving damaged DB file with .damaged extension', 1);
|
||||
var damagedFile = this._dbPath + '.damaged';
|
||||
damagedFile = await Zotero.File.moveToUnique(file, damagedFile);
|
||||
|
||||
// Test the backup file
|
||||
try {
|
||||
Zotero.debug("Asynchronously opening DB connection");
|
||||
this._connection = await Zotero.Promise.resolve(this.Sqlite.openConnection({
|
||||
path: backupFile
|
||||
}));
|
||||
await this.closeDatabase();
|
||||
}
|
||||
// Can't open backup either
|
||||
catch (e) {
|
||||
// Create new main database
|
||||
this._connection = await Zotero.Promise.resolve(this.Sqlite.openConnection({
|
||||
path: file
|
||||
}));
|
||||
|
||||
Zotero.alert(
|
||||
null,
|
||||
Zotero.getString('general.error'),
|
||||
Zotero.getString(
|
||||
'db.dbRestoreFailed',
|
||||
[Zotero.appName, fileName, OS.Path.basename(damagedFile)]
|
||||
)
|
||||
);
|
||||
|
||||
if (await OS.File.exists(corruptMarker)) {
|
||||
await OS.File.remove(corruptMarker);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._connection = undefined;
|
||||
|
||||
// Copy backup file to main DB file
|
||||
this._debug("Restoring database '" + this._dbName + "' from backup file", 1);
|
||||
try {
|
||||
await OS.File.copy(backupFile, file);
|
||||
}
|
||||
catch (e) {
|
||||
// TODO: deal with low disk space
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Open restored database
|
||||
this._connection = await Zotero.Promise.resolve(this.Sqlite.openConnection({
|
||||
path: file
|
||||
}));
|
||||
this._debug('Database restored', 1);
|
||||
let backupDate = '';
|
||||
let backupTime = '';
|
||||
try {
|
||||
let info = await OS.File.stat(backupFile);
|
||||
backupDate = info.lastModificationDate.toLocaleDateString();
|
||||
backupTime = info.lastModificationDate.toLocaleTimeString();
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
Zotero.alert(
|
||||
null,
|
||||
Zotero.getString('general.warning'),
|
||||
Zotero.getString(
|
||||
'db.dbRestored',
|
||||
[Zotero.appName, fileName, backupDate, backupTime, OS.Path.basename(damagedFile)]
|
||||
) + '\n\n'
|
||||
+ Zotero.getString('db.dbRestored.cloudStorage')
|
||||
);
|
||||
|
||||
if (await OS.File.exists(corruptMarker)) {
|
||||
await OS.File.remove(corruptMarker);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Zotero.DBConnection.prototype._debug = function (str, level) {
|
||||
var prefix = this._dbName == 'zotero' ? '' : '[' + this._dbName + '] ';
|
||||
Zotero.debug(prefix + str, level);
|
||||
|
|
|
@ -263,6 +263,12 @@ Zotero.Schema = new function(){
|
|||
_schemaUpdateDeferred.resolve(true);
|
||||
}
|
||||
catch (e) {
|
||||
// DB corruption already shows an alert
|
||||
if (Zotero.DB.isCorruptionError(e)) {
|
||||
_schemaUpdateDeferred.reject(e);
|
||||
return;
|
||||
}
|
||||
|
||||
let kbURL = 'https://www.zotero.org/support/kb/unable_to_load_translators_and_styles';
|
||||
let msg = Zotero.getString('startupError.bundledFileUpdateError', Zotero.clientName);
|
||||
|
||||
|
|
|
@ -895,7 +895,6 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
|||
}
|
||||
} catch(e) {
|
||||
Zotero.logError(e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -622,11 +622,16 @@ attachment.convertToStored.title = Convert to Stored File;Convert to Stored File
|
|||
attachment.convertToStored.text = %1$S attachment will be converted from a linked file to a stored file.;%1$S attachments will be converted from linked files to stored files.
|
||||
attachment.convertToStored.deleteOriginal = Delete original file after storing;Delete original files after storing
|
||||
|
||||
db.dbCorrupted = The Zotero database '%S' appears to have become corrupted.
|
||||
db.dbCorrupted.restart = Please restart %S to attempt an automatic restore from the last backup.
|
||||
db.dbCorruptedNoBackup = The Zotero database '%S' appears to have become corrupted, and no automatic backup is available.\n\nA new database has been created. The damaged file was saved to your Zotero data directory.
|
||||
db.dbRestored = The Zotero database '%1$S' appears to have become corrupted.\n\nYour data was restored from the last automatic backup made on %2$S at %3$S. The damaged file was saved to your Zotero data directory.
|
||||
db.dbRestoreFailed = The Zotero database '%S' appears to have become corrupted, and an attempt to restore from the last automatic backup failed.\n\nA new database has been created. The damaged file was saved to your Zotero data directory.
|
||||
db.dbCorrupted = The %1$S database “%2$S” appears to have become corrupted.
|
||||
db.dbCorrupted.cloudStorage = This generally occurs when the %S data directory is stored in a cloud storage folder or on a network drive. If you’ve moved your data directory to one of those places, you should move it back to the default location.
|
||||
db.dbCorrupted.repairOrRestore = You will need to repair the database or restore it from a backup or from your online library.
|
||||
db.dbCorrupted.restoreFromLastAutomaticBackup = %S can attempt to restore from the last automatic backup from %S at %S.
|
||||
db.dbCorrupted.viewMoreInformation = For more information on repairing or restoring the database and preventing further corruption, see %S
|
||||
db.dbCorruptedNoBackup = The %1$S database “%2$S” appears to have become corrupted, and no automatic backup is available.\n\nA new database has been created. The damaged file was saved to your %1$S data directory as “%3$S”.
|
||||
db.dbRestored = The %1$S database “%2$S” appears to have become corrupted.\n\nYour data was restored from the last automatic backup from %3$S at %4$S. The damaged file was saved to your %1$S data directory as “%5$S”.
|
||||
db.dbRestored.cloudStorage = If your data directory is stored in a cloud storage folder or on a network drive, you should move it back to the default location immediately.
|
||||
db.dbRestoreFailed = The %1$S database “%2$S” appears to have become corrupted, and an attempt to restore from the last automatic backup failed.\n\nA new database has been created. The damaged file was saved to your %1$S data directory as “%3$S”.
|
||||
db.dbCorrupted.automaticBackup = Restore from Automatic Backup
|
||||
|
||||
db.integrityCheck.passed = No errors were found in the database.
|
||||
db.integrityCheck.failed = Errors were found in your Zotero database.
|
||||
|
|
Loading…
Reference in a new issue