зеркало из https://github.com/mozilla/gecko-dev.git
Bug 888784 - Add a new Sqlite.jsm based database connector to FormHistory.jsm. r=mak
MozReview-Commit-ID: JADYzdAokVJ --HG-- extra : rebase_source : 226f858c2a85ac7e57a9289925136d8e1788f127
This commit is contained in:
Родитель
e449275782
Коммит
d2c296a580
|
@ -95,11 +95,19 @@ Cu.import("resource://gre/modules/AppConstants.jsm");
|
|||
XPCOMUtils.defineLazyServiceGetter(this, "uuidService",
|
||||
"@mozilla.org/uuid-generator;1",
|
||||
"nsIUUIDGenerator");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
|
||||
"resource://gre/modules/AsyncShutdown.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
"resource://gre/modules/osfile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
|
||||
"resource://gre/modules/Sqlite.jsm");
|
||||
|
||||
|
||||
const DB_SCHEMA_VERSION = 4;
|
||||
const DAY_IN_MS = 86400000; // 1 day in milliseconds
|
||||
const MAX_SEARCH_TOKENS = 10;
|
||||
const NOOP = function noop() {};
|
||||
const DB_FILENAME = "formhistory.sqlite";
|
||||
|
||||
var supportsDeletedTable = AppConstants.platform == "android";
|
||||
|
||||
|
@ -381,17 +389,17 @@ XPCOMUtils.defineLazyGetter(this, "dbConnection", function() {
|
|||
|
||||
try {
|
||||
dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile).clone();
|
||||
dbFile.append("formhistory.sqlite");
|
||||
dbFile.append(DB_FILENAME);
|
||||
log("Opening database at " + dbFile.path);
|
||||
|
||||
_dbConnection = Services.storage.openUnsharedDatabase(dbFile);
|
||||
_dbConnection = Services.storage.openDatabase(dbFile);
|
||||
dbInit();
|
||||
} catch (e) {
|
||||
if (e.result != Cr.NS_ERROR_FILE_CORRUPTED) {
|
||||
throw e;
|
||||
}
|
||||
dbCleanup(dbFile);
|
||||
_dbConnection = Services.storage.openUnsharedDatabase(dbFile);
|
||||
_dbConnection = Services.storage.openDatabase(dbFile);
|
||||
dbInit();
|
||||
}
|
||||
|
||||
|
@ -479,6 +487,16 @@ var Migrators = {
|
|||
_dbConnection.createTable("moz_deleted_formhistory", tSQL);
|
||||
}
|
||||
},
|
||||
|
||||
async dbAsyncMigrateToVersion4(conn) {
|
||||
const TABLE_NAME = "moz_deleted_formhistory";
|
||||
let tableExists = await conn.tableExists(TABLE_NAME);
|
||||
if (!tableExists) {
|
||||
let table = dbSchema.tables[TABLE_NAME];
|
||||
let tSQL = Object.keys(table).map(col => [col, table[col]].join(" ")).join(", ");
|
||||
await conn.execute(`CREATE TABLE ${TABLE_NAME} (${tSQL})`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function dbCreate() {
|
||||
|
@ -806,7 +824,251 @@ function expireOldEntriesVacuum(aExpireTime, aBeginningCount) {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Database creation and access. Used by FormHistory and some of the
|
||||
* utility functions, but is not exposed to the outside world.
|
||||
* @class
|
||||
*/
|
||||
this.DB = {
|
||||
// Once we establish a database connection, we have to hold a reference
|
||||
// to it so that it won't get GC'd.
|
||||
_instance: null,
|
||||
// MAX_ATTEMPTS is how many times we'll try to establish a connection
|
||||
// or migrate a database before giving up.
|
||||
MAX_ATTEMPTS: 2,
|
||||
|
||||
/** String representing where the FormHistory database is on the filesystem */
|
||||
get path() {
|
||||
return OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets up and returns a connection to the FormHistory database. The
|
||||
* connection also registers itself with AsyncShutdown so that the
|
||||
* connection is closed on when the profile-before-change observer
|
||||
* notification is fired.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* @resolves An Sqlite.jsm connection to the database.
|
||||
* @rejects If connecting to the database, or migrating the database
|
||||
* failed after MAX_ATTEMPTS attempts (where each attempt
|
||||
* backs up and deletes the old database), this will reject
|
||||
* with the Sqlite.jsm error.
|
||||
*/
|
||||
get conn() {
|
||||
delete this.conn;
|
||||
let conn = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
this._instance = await this._establishConn();
|
||||
} catch (e) {
|
||||
log("Failed to establish database connection.");
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncShutdown.profileBeforeChange.addBlocker(
|
||||
"Closing FormHistory database.", () => this._instance.close());
|
||||
|
||||
resolve(this._instance);
|
||||
});
|
||||
|
||||
return this.conn = conn;
|
||||
},
|
||||
|
||||
// Private functions
|
||||
|
||||
/**
|
||||
* Tries to connect to the Sqlite database at this.path, and then
|
||||
* migrates the database as necessary. If any of the steps to do this
|
||||
* fail, this function should re-enter itself with an incremented
|
||||
* attemptNum so that another attempt can be made after backing up
|
||||
* and deleting the old database.
|
||||
*
|
||||
* @async
|
||||
* @param {number} attemptNum
|
||||
* The optional number of the attempt that is being made to connect
|
||||
* to the database. Defaults to 0.
|
||||
* @returns {Promise}
|
||||
* @resolves An Sqlite.jsm connection to the database.
|
||||
* @rejects After MAX_ATTEMPTS, this will reject with the Sqlite.jsm
|
||||
* error.
|
||||
*/
|
||||
async _establishConn(attemptNum = 0) {
|
||||
log(`Establishing database connection - attempt # ${attemptNum}`);
|
||||
let conn;
|
||||
try {
|
||||
conn = await Sqlite.openConnection({ path: this.path });
|
||||
} catch (e) {
|
||||
// Bug 1423729 - We should check the reason for the connection failure,
|
||||
// in case this is due to the disk being full or the database file being
|
||||
// inaccessible due to third-party software (like anti-virus software).
|
||||
// In that case, we should probably fail right away.
|
||||
if (attemptNum < this.MAX_ATTEMPTS) {
|
||||
log("Establishing connection failed.");
|
||||
await this._failover(conn);
|
||||
return this._establishConn(++attemptNum);
|
||||
}
|
||||
|
||||
if (conn) {
|
||||
await conn.close();
|
||||
}
|
||||
log("Establishing connection failed too many times. Giving up.");
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
let dbVersion = parseInt(await conn.getSchemaVersion(), 10);
|
||||
|
||||
// Case 1: Database is up to date and we're ready to go.
|
||||
if (dbVersion == DB_SCHEMA_VERSION) {
|
||||
return conn;
|
||||
}
|
||||
|
||||
// Case 2: Downgrade
|
||||
if (dbVersion > DB_SCHEMA_VERSION) {
|
||||
log("Downgrading to version " + DB_SCHEMA_VERSION);
|
||||
// User's DB is newer. Sanity check that our expected columns are
|
||||
// present, and if so mark the lower version and merrily continue
|
||||
// on. If the columns are borked, something is wrong so blow away
|
||||
// the DB and start from scratch. [Future incompatible upgrades
|
||||
// should switch to a different table or file.]
|
||||
if (!(await this._expectedColumnsPresent(conn))) {
|
||||
throw Components.Exception("DB is missing expected columns",
|
||||
Cr.NS_ERROR_FILE_CORRUPTED);
|
||||
}
|
||||
|
||||
// Change the stored version to the current version. If the user
|
||||
// runs the newer code again, it will see the lower version number
|
||||
// and re-upgrade (to fixup any entries the old code added).
|
||||
await conn.setSchemaVersion(DB_SCHEMA_VERSION);
|
||||
return conn;
|
||||
}
|
||||
|
||||
// Case 3: Very old database that cannot be migrated.
|
||||
//
|
||||
// When FormHistory is released, we will no longer support the various
|
||||
// schema versions prior to this release that nsIFormHistory2 once did.
|
||||
// We'll throw an NS_ERROR_FILE_CORRUPTED, which should cause us to wipe
|
||||
// out this DB and create a new one (unless this is our MAX_ATTEMPTS
|
||||
// attempt).
|
||||
if (dbVersion > 0 && dbVersion < 3) {
|
||||
throw Components.Exception("DB version is unsupported.",
|
||||
Cr.NS_ERROR_FILE_CORRUPTED);
|
||||
}
|
||||
|
||||
if (dbVersion == 0) {
|
||||
// Case 4: New database
|
||||
await conn.executeTransaction(async () => {
|
||||
log("Creating DB -- tables");
|
||||
for (let name in dbSchema.tables) {
|
||||
let table = dbSchema.tables[name];
|
||||
let tSQL = Object.keys(table).map(col => [col, table[col]].join(" ")).join(", ");
|
||||
log("Creating table " + name + " with " + tSQL);
|
||||
await conn.execute(`CREATE TABLE ${name} (${tSQL})`);
|
||||
}
|
||||
|
||||
log("Creating DB -- indices");
|
||||
for (let name in dbSchema.indices) {
|
||||
let index = dbSchema.indices[name];
|
||||
let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
|
||||
"(" + index.columns.join(", ") + ")";
|
||||
await conn.execute(statement);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Case 5: Old database requiring a migration
|
||||
await conn.executeTransaction(async () => {
|
||||
for (let v = dbVersion + 1; v <= DB_SCHEMA_VERSION; v++) {
|
||||
log("Upgrading to version " + v + "...");
|
||||
await Migrators["dbAsyncMigrateToVersion" + v](conn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await conn.setSchemaVersion(DB_SCHEMA_VERSION);
|
||||
|
||||
return conn;
|
||||
} catch (e) {
|
||||
if (e.result != Cr.NS_ERROR_FILE_CORRUPTED) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (attemptNum < this.MAX_ATTEMPTS) {
|
||||
log("Setting up database failed.");
|
||||
await this._failover(conn);
|
||||
return this._establishConn(++attemptNum);
|
||||
}
|
||||
|
||||
if (conn) {
|
||||
await conn.close();
|
||||
}
|
||||
|
||||
log("Setting up database failed too many times. Giving up.");
|
||||
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Closes a connection to the database, then backs up the database before
|
||||
* deleting it.
|
||||
*
|
||||
* @async
|
||||
* @param {SqliteConnection | null} conn
|
||||
* The connection to the database that we failed to establish or
|
||||
* migrate.
|
||||
* @throws If any file operations fail.
|
||||
*/
|
||||
async _failover(conn) {
|
||||
log("Cleaning up DB file - close & remove & backup.");
|
||||
if (conn) {
|
||||
await conn.close();
|
||||
}
|
||||
let backupFile = this.path + ".corrupt";
|
||||
let { file, path: uniquePath } =
|
||||
await OS.File.openUnique(backupFile, { humanReadable: true });
|
||||
await file.close();
|
||||
await OS.File.copy(this.path, uniquePath);
|
||||
await OS.File.remove(this.path);
|
||||
log("Completed DB cleanup.");
|
||||
},
|
||||
|
||||
/**
|
||||
* Tests that a database connection contains the tables that we expect.
|
||||
*
|
||||
* @async
|
||||
* @param {SqliteConnection | null} conn
|
||||
* The connection to the database that we're testing.
|
||||
* @returns {Promise}
|
||||
* @resolves true if all expected columns are present.
|
||||
*/
|
||||
async _expectedColumnsPresent(conn) {
|
||||
for (let name in dbSchema.tables) {
|
||||
let table = dbSchema.tables[name];
|
||||
let query = "SELECT " +
|
||||
Object.keys(table).join(", ") +
|
||||
" FROM " + name;
|
||||
try {
|
||||
await conn.execute(query, null, (row, cancel) => {
|
||||
// One row is enough to let us know this worked.
|
||||
cancel();
|
||||
});
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
log("Verified that expected columns are present in DB.");
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
this.FormHistory = {
|
||||
get db() {
|
||||
return DB.conn;
|
||||
},
|
||||
|
||||
get enabled() {
|
||||
return Prefs.enabled;
|
||||
},
|
||||
|
|
Загрузка…
Ссылка в новой задаче