diff --git a/browser/components/migration/FirefoxProfileMigrator.js b/browser/components/migration/FirefoxProfileMigrator.js index e1f1cb22f9f..89c9c7256a1 100644 --- a/browser/components/migration/FirefoxProfileMigrator.js +++ b/browser/components/migration/FirefoxProfileMigrator.js @@ -20,8 +20,15 @@ XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups", "resource://gre/modules/PlacesBackups.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "SessionMigration", "resource:///modules/sessionstore/SessionMigration.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); -function FirefoxProfileMigrator() { } + +function FirefoxProfileMigrator() { + this.wrappedJSObject = this; // for testing... +} FirefoxProfileMigrator.prototype = Object.create(MigratorPrototype); @@ -58,6 +65,10 @@ FirefoxProfileMigrator.prototype.getResources = function() { if (sourceProfileDir.equals(currentProfileDir)) return null; + return this._getResourcesInternal(sourceProfileDir, currentProfileDir); +} + +FirefoxProfileMigrator.prototype._getResourcesInternal = function(sourceProfileDir, currentProfileDir) { let getFileResource = function(aMigrationType, aFileNames) { let files = []; for (let fileName of aFileNames) { @@ -120,8 +131,72 @@ FirefoxProfileMigrator.prototype.getResources = function() { } } + // FHR related migrations. + let times = getFileResource(types.OTHERDATA, ["times.json"]); + let healthReporter = { + name: "healthreporter", // name is used only by tests... + type: types.OTHERDATA, + migrate: aCallback => { + // the health-reporter can't have been initialized yet so it's safe to + // copy the SQL file. + + // We only support the default database name - copied from healthreporter.jsm + const DEFAULT_DATABASE_NAME = "healthreport.sqlite"; + let path = OS.Path.join(sourceProfileDir.path, DEFAULT_DATABASE_NAME); + let sqliteFile = FileUtils.File(path); + if (sqliteFile.exists()) { + sqliteFile.copyTo(currentProfileDir, ""); + } + // In unusual cases there may be 2 additional files - a "write ahead log" + // (-wal) file and a "shared memory file" (-shm). The wal file contains + // data that will be replayed when the DB is next opened, while the shm + // file is ignored in that case - the replay happens using only the wal. + // So we *do* copy a wal if it exists, but not a shm. + // See https://www.sqlite.org/tempfiles.html for more. + // (Note also we attempt these copies even if we can't find the DB, and + // rely on FHR itself to do the right thing if it can) + path = OS.Path.join(sourceProfileDir.path, DEFAULT_DATABASE_NAME + "-wal"); + let sqliteWal = FileUtils.File(path); + if (sqliteWal.exists()) { + sqliteWal.copyTo(currentProfileDir, ""); + } + + // If the 'healthreport' directory exists we copy everything from it. + let subdir = this._getFileObject(sourceProfileDir, "healthreport"); + if (subdir && subdir.isDirectory()) { + // Copy all regular files. + let dest = currentProfileDir.clone(); + dest.append("healthreport"); + dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, + FileUtils.PERMS_DIRECTORY); + let enumerator = subdir.directoryEntries; + while (enumerator.hasMoreElements()) { + let file = enumerator.getNext().QueryInterface(Components.interfaces.nsIFile); + if (file.isDirectory()) { + continue; + } + file.copyTo(dest, ""); + } + } + // If the 'datareporting' directory exists we copy just state.json + subdir = this._getFileObject(sourceProfileDir, "datareporting"); + if (subdir && subdir.isDirectory()) { + let stateFile = this._getFileObject(subdir, "state.json"); + if (stateFile) { + let dest = currentProfileDir.clone(); + dest.append("datareporting"); + dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, + FileUtils.PERMS_DIRECTORY); + stateFile.copyTo(dest, ""); + } + } + aCallback(true); + } + } + return [r for each (r in [places, cookies, passwords, formData, - dictionary, bookmarksBackups, session]) if (r)]; + dictionary, bookmarksBackups, session, + times, healthReporter]) if (r)]; } Object.defineProperty(FirefoxProfileMigrator.prototype, "startupOnlyMigrator", { diff --git a/browser/components/migration/tests/unit/test_fx_fhr.js b/browser/components/migration/tests/unit/test_fx_fhr.js new file mode 100644 index 00000000000..3cdf4284569 --- /dev/null +++ b/browser/components/migration/tests/unit/test_fx_fhr.js @@ -0,0 +1,228 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +let {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {}); + +function run_test() { + run_next_test(); +} + +function checkDirectoryContains(dir, files) { + print("checking " + dir.path + " - should contain " + Object.keys(files)); + let seen = new Set(); + let enumerator = dir.directoryEntries; + while (enumerator.hasMoreElements()) { + let file = enumerator.getNext().QueryInterface(Ci.nsIFile); + print("found file: " + file.path); + Assert.ok(file.leafName in files, file.leafName + " exists, but shouldn't"); + + let expectedContents = files[file.leafName]; + if (typeof expectedContents != "string") { + // it's a subdir - recurse! + Assert.ok(file.isDirectory(), "should be a subdir"); + let newDir = dir.clone(); + newDir.append(file.leafName); + checkDirectoryContains(newDir, expectedContents); + } else { + Assert.ok(!file.isDirectory(), "should be a regular file"); + let stream = Cc['@mozilla.org/network/file-input-stream;1'] + .createInstance(Ci.nsIFileInputStream); + stream.init(file, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF); + + let sis = Cc["@mozilla.org/scriptableinputstream;1"] + .createInstance(Ci.nsIScriptableInputStream); + sis.init(stream); + let contents = sis.read(file.fileSize); + sis.close(); + + Assert.equal(contents, expectedContents); + } + seen.add(file.leafName); + } + let missing = [x for (x in files) if (!seen.has(x))]; + Assert.deepEqual(missing, [], "no missing files in " + dir.path); +} + +function getTestDirs() { + // we make a directory structure in a temp dir which mirrors what we are + // testing. + let tempDir = do_get_tempdir(); + let srcDir = tempDir.clone(); + srcDir.append("test_source_dir"); + srcDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + + let targetDir = tempDir.clone(); + targetDir.append("test_target_dir"); + targetDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + + // no need to cleanup these dirs - the xpcshell harness will do it for us. + return [srcDir, targetDir]; +} + +function writeToFile(dir, leafName, contents) { + let file = dir.clone(); + file.append(leafName); + + let outputStream = FileUtils.openFileOutputStream(file); + outputStream.write(contents, contents.length); + outputStream.close(); +} + +function promiseFHRMigrator(srcDir, targetDir) { + let migrator = Cc["@mozilla.org/profile/migrator;1?app=browser&type=firefox"] + .createInstance(Ci.nsISupports) + .wrappedJSObject; + let migrators = migrator._getResourcesInternal(srcDir, targetDir); + for (let m of migrators) { + if (m.name == "healthreporter") { + return new Promise((resolve, reject) => { + m.migrate(resolve); + }); + } + } + throw new Error("failed to find the fhr migrator"); +} + +add_task(function* test_empty() { + let [srcDir, targetDir] = getTestDirs(); + let ok = yield promiseFHRMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true with empty directories"); + // check both are empty + checkDirectoryContains(srcDir, {}); + checkDirectoryContains(targetDir, {}); +}); + +add_task(function* test_just_sqlite() { + let [srcDir, targetDir] = getTestDirs(); + + let contents = "hello there\n\n"; + writeToFile(srcDir, "healthreport.sqlite", contents); + + let ok = yield promiseFHRMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true with sqlite file copied"); + + checkDirectoryContains(targetDir, { + "healthreport.sqlite": contents, + }); +}); + +add_task(function* test_sqlite_extras() { + let [srcDir, targetDir] = getTestDirs(); + + let contents_sqlite = "hello there\n\n"; + writeToFile(srcDir, "healthreport.sqlite", contents_sqlite); + + let contents_wal = "this is the wal\n\n"; + writeToFile(srcDir, "healthreport.sqlite-wal", contents_wal); + + // and the -shm - this should *not* be copied. + writeToFile(srcDir, "healthreport.sqlite-shm", "whatever"); + + let ok = yield promiseFHRMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true with sqlite file copied"); + + checkDirectoryContains(targetDir, { + "healthreport.sqlite": contents_sqlite, + "healthreport.sqlite-wal": contents_wal, + }); +}); + +add_task(function* test_sqlite_healthreport_not_dir() { + let [srcDir, targetDir] = getTestDirs(); + + let contents = "hello there\n\n"; + writeToFile(srcDir, "healthreport.sqlite", contents); + writeToFile(srcDir, "healthreport", "I'm a file but should be a directory"); + + let ok = yield promiseFHRMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true even though the directory was a file"); + // We should have only the sqlite file + checkDirectoryContains(targetDir, { + "healthreport.sqlite": contents, + }); +}); + +add_task(function* test_sqlite_healthreport_empty() { + let [srcDir, targetDir] = getTestDirs(); + + let contents = "hello there\n\n"; + writeToFile(srcDir, "healthreport.sqlite", contents); + + // create an empty 'healthreport' subdir. + let subDir = srcDir.clone(); + subDir.append("healthreport"); + subDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + + let ok = yield promiseFHRMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true"); + + // we should end up with the .sqlite file and an empty subdir in the target. + checkDirectoryContains(targetDir, { + "healthreport.sqlite": contents, + "healthreport": {}, + }); +}); + +add_task(function* test_sqlite_healthreport_contents() { + let [srcDir, targetDir] = getTestDirs(); + + let contents = "hello there\n\n"; + writeToFile(srcDir, "healthreport.sqlite", contents); + + // create an empty 'healthreport' subdir. + let subDir = srcDir.clone(); + subDir.append("healthreport"); + subDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + + writeToFile(subDir, "file1", "this is file 1"); + writeToFile(subDir, "file2", "this is file 2"); + + let ok = yield promiseFHRMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true"); + + // we should end up with the .sqlite file and an empty subdir in the target. + checkDirectoryContains(targetDir, { + "healthreport.sqlite": contents, + "healthreport": { + "file1": "this is file 1", + "file2": "this is file 2", + }, + }); +}); + +add_task(function* test_datareporting_empty() { + let [srcDir, targetDir] = getTestDirs(); + + // create an empty 'datareporting' subdir. + let subDir = srcDir.clone(); + subDir.append("datareporting"); + subDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + + let ok = yield promiseFHRMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true"); + + // we should end up with nothing at all in the destination - state.json was + // missing so we didn't even create the target dir. + checkDirectoryContains(targetDir, {}); +}); + +add_task(function* test_datareporting_many() { + let [srcDir, targetDir] = getTestDirs(); + + // create an empty 'datareporting' subdir. + let subDir = srcDir.clone(); + subDir.append("datareporting"); + subDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + + writeToFile(subDir, "state.json", "should be copied"); + writeToFile(subDir, "something.else", "should not"); + + let ok = yield promiseFHRMigrator(srcDir, targetDir); + Assert.ok(ok, "callback should have been true"); + + checkDirectoryContains(targetDir, { + "datareporting" : { + "state.json": "should be copied", + } + }); +}); diff --git a/browser/components/migration/tests/unit/xpcshell.ini b/browser/components/migration/tests/unit/xpcshell.ini index e5c9190bd17..d3000b28408 100644 --- a/browser/components/migration/tests/unit/xpcshell.ini +++ b/browser/components/migration/tests/unit/xpcshell.ini @@ -6,3 +6,5 @@ skip-if = toolkit == 'android' || toolkit == 'gonk' [test_IE_bookmarks.js] skip-if = os != "win" + +[test_fx_fhr.js]