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:
Dan Stillman 2020-05-22 03:47:59 -04:00
parent a79d1c2114
commit 1e0ad3aba3
4 changed files with 207 additions and 139 deletions

View file

@ -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);
}
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;
await this._handleCorruptionMarker();
}
else {
// Some other error that we don't yet know how to deal with
throw e;
}
// Some other error that we don't yet know how to deal with
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);

View file

@ -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);

View file

@ -895,7 +895,6 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
}
} catch(e) {
Zotero.logError(e);
throw e;
}
});

View file

@ -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 youve 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.