Bug 1885609 - implement backup method for PlacesBackupResource. r=backup-reviewers,places-reviewers,mak,mconley

Implements `PlacesBackupResource.backup` to store a copy of `places.sqlite` and `favicons.sqlite` in the staging folder.

If users don't want history remembered or use permanent private browsing mode, we will backup bookmarks instead to a file called `bookmarks.json`. Automatic backup is not yet implemented, so to test changes locally, go to `chrome://browser/content/backup/debug.html` to view where we store the staging folder and to manually run the backup methods for all available backup resources.

Backup files for `PlacesBackupResource` should be under the `places` subfolder in the staging folder.

Differential Revision: https://phabricator.services.mozilla.com/D206532
This commit is contained in:
kpatenio 2024-04-10 21:21:17 +00:00
parent c3a4b0e099
commit ccd602ba69
4 changed files with 291 additions and 56 deletions

View File

@ -3,6 +3,28 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"isBrowsingHistoryEnabled",
"places.history.enabled",
true
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"isSanitizeOnShutdownEnabled",
"privacy.sanitize.sanitizeOnShutdown",
false
);
/**
* Class representing Places database related files within a user profile.
@ -16,6 +38,47 @@ export class PlacesBackupResource extends BackupResource {
return false;
}
async backup(stagingPath, profilePath = PathUtils.profileDir) {
const sqliteDatabases = ["places.sqlite", "favicons.sqlite"];
let canBackupHistory =
!lazy.PrivateBrowsingUtils.permanentPrivateBrowsing &&
!lazy.isSanitizeOnShutdownEnabled &&
lazy.isBrowsingHistoryEnabled;
/**
* Do not backup places.sqlite and favicons.sqlite if users have history disabled, want history cleared on shutdown or are using permanent private browsing mode.
* Instead, export all existing bookmarks to a compressed JSON file that we can read when restoring the backup.
*/
if (!canBackupHistory) {
let bookmarksBackupFile = PathUtils.join(
stagingPath,
"bookmarks.jsonlz4"
);
await lazy.BookmarkJSONUtils.exportToFile(bookmarksBackupFile, {
compress: true,
});
return { bookmarksOnly: true };
}
for (let fileName of sqliteDatabases) {
let sourcePath = PathUtils.join(profilePath, fileName);
let destPath = PathUtils.join(stagingPath, fileName);
let connection;
try {
connection = await lazy.Sqlite.openConnection({
path: sourcePath,
readOnly: true,
});
await connection.backup(destPath);
} finally {
await connection.close();
}
}
return null;
}
async measure(profilePath = PathUtils.profileDir) {
let placesDBPath = PathUtils.join(profilePath, "places.sqlite");
let faviconsDBPath = PathUtils.join(profilePath, "favicons.sqlite");

View File

@ -0,0 +1,226 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { PlacesBackupResource } = ChromeUtils.importESModule(
"resource:///modules/backup/PlacesBackupResource.sys.mjs"
);
const { PrivateBrowsingUtils } = ChromeUtils.importESModule(
"resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
);
const HISTORY_ENABLED_PREF = "places.history.enabled";
const SANITIZE_ON_SHUTDOWN_PREF = "privacy.sanitize.sanitizeOnShutdown";
registerCleanupFunction(() => {
/**
* Even though test_backup_no_saved_history clears user prefs too,
* clear them here as well in case that test fails and we don't
* reach the end of the test, which handles the cleanup.
*/
Services.prefs.clearUserPref(HISTORY_ENABLED_PREF);
Services.prefs.clearUserPref(SANITIZE_ON_SHUTDOWN_PREF);
});
/**
* Tests that we can measure Places DB related files in the profile directory.
*/
add_task(async function test_measure() {
Services.fog.testResetFOG();
const EXPECTED_PLACES_DB_SIZE = 5240;
const EXPECTED_FAVICONS_DB_SIZE = 5240;
// Create resource files in temporary directory
const tempDir = PathUtils.tempDir;
let tempPlacesDBPath = PathUtils.join(tempDir, "places.sqlite");
let tempFaviconsDBPath = PathUtils.join(tempDir, "favicons.sqlite");
await createKilobyteSizedFile(tempPlacesDBPath, EXPECTED_PLACES_DB_SIZE);
await createKilobyteSizedFile(tempFaviconsDBPath, EXPECTED_FAVICONS_DB_SIZE);
let placesBackupResource = new PlacesBackupResource();
await placesBackupResource.measure(tempDir);
let placesMeasurement = Glean.browserBackup.placesSize.testGetValue();
let faviconsMeasurement = Glean.browserBackup.faviconsSize.testGetValue();
let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
// Compare glean vs telemetry measurements
TelemetryTestUtils.assertScalar(
scalars,
"browser.backup.places_size",
placesMeasurement,
"Glean and telemetry measurements for places.sqlite should be equal"
);
TelemetryTestUtils.assertScalar(
scalars,
"browser.backup.favicons_size",
faviconsMeasurement,
"Glean and telemetry measurements for favicons.sqlite should be equal"
);
// Compare glean measurements vs actual file sizes
Assert.equal(
placesMeasurement,
EXPECTED_PLACES_DB_SIZE,
"Should have collected the correct glean measurement for places.sqlite"
);
Assert.equal(
faviconsMeasurement,
EXPECTED_FAVICONS_DB_SIZE,
"Should have collected the correct glean measurement for favicons.sqlite"
);
await maybeRemovePath(tempPlacesDBPath);
await maybeRemovePath(tempFaviconsDBPath);
});
/**
* Tests that the backup method correctly copies places.sqlite and
* favicons.sqlite from the profile directory into the staging directory.
*/
add_task(async function test_backup() {
let sandbox = sinon.createSandbox();
let placesBackupResource = new PlacesBackupResource();
let sourcePath = await IOUtils.createUniqueDirectory(
PathUtils.tempDir,
"PlacesBackupResource-source-test"
);
let stagingPath = await IOUtils.createUniqueDirectory(
PathUtils.tempDir,
"PlacesBackupResource-staging-test"
);
let fakeConnection = {
backup: sandbox.stub().resolves(true),
close: sandbox.stub().resolves(true),
};
sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
await placesBackupResource.backup(stagingPath, sourcePath);
Assert.ok(
fakeConnection.backup.calledTwice,
"Backup should have been called twice"
);
Assert.ok(
fakeConnection.backup.firstCall.calledWith(
PathUtils.join(stagingPath, "places.sqlite")
),
"places.sqlite should have been backed up first"
);
Assert.ok(
fakeConnection.backup.secondCall.calledWith(
PathUtils.join(stagingPath, "favicons.sqlite")
),
"favicons.sqlite should have been backed up second"
);
await maybeRemovePath(stagingPath);
await maybeRemovePath(sourcePath);
sandbox.restore();
});
/**
* Tests that the backup method correctly creates a compressed bookmarks JSON file when users
* don't want history saved, even on shutdown.
*/
add_task(async function test_backup_no_saved_history() {
let sandbox = sinon.createSandbox();
let placesBackupResource = new PlacesBackupResource();
let sourcePath = await IOUtils.createUniqueDirectory(
PathUtils.tempDir,
"PlacesBackupResource-source-test"
);
let stagingPath = await IOUtils.createUniqueDirectory(
PathUtils.tempDir,
"PlacesBackupResource-staging-test"
);
let fakeConnection = {
backup: sandbox.stub().resolves(true),
close: sandbox.stub().resolves(true),
};
sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
/**
* First verify that remember history pref alone affects backup file type for places,
* despite sanitize on shutdown pref value.
*/
Services.prefs.setBoolPref(HISTORY_ENABLED_PREF, false);
Services.prefs.setBoolPref(SANITIZE_ON_SHUTDOWN_PREF, false);
await placesBackupResource.backup(stagingPath, sourcePath);
Assert.ok(
fakeConnection.backup.notCalled,
"No sqlite connections should have been made with remember history disabled"
);
await assertFilesExist(stagingPath, [{ path: "bookmarks.jsonlz4" }]);
await IOUtils.remove(PathUtils.join(stagingPath, "bookmarks.jsonlz4"));
/**
* Now verify that the sanitize shutdown pref alone affects backup file type for places,
* even if the user is okay with remembering history while browsing.
*/
Services.prefs.setBoolPref(HISTORY_ENABLED_PREF, true);
Services.prefs.setBoolPref(SANITIZE_ON_SHUTDOWN_PREF, true);
fakeConnection.backup.resetHistory();
await placesBackupResource.backup(stagingPath, sourcePath);
Assert.ok(
fakeConnection.backup.notCalled,
"No sqlite connections should have been made with sanitize shutdown enabled"
);
await assertFilesExist(stagingPath, [{ path: "bookmarks.jsonlz4" }]);
await maybeRemovePath(stagingPath);
await maybeRemovePath(sourcePath);
sandbox.restore();
Services.prefs.clearUserPref(HISTORY_ENABLED_PREF);
Services.prefs.clearUserPref(SANITIZE_ON_SHUTDOWN_PREF);
});
/**
* Tests that the backup method correctly creates a compressed bookmarks JSON file when
* permanent private browsing mode is enabled.
*/
add_task(async function test_backup_private_browsing() {
let sandbox = sinon.createSandbox();
let placesBackupResource = new PlacesBackupResource();
let sourcePath = await IOUtils.createUniqueDirectory(
PathUtils.tempDir,
"PlacesBackupResource-source-test"
);
let stagingPath = await IOUtils.createUniqueDirectory(
PathUtils.tempDir,
"PlacesBackupResource-staging-test"
);
let fakeConnection = {
backup: sandbox.stub().resolves(true),
close: sandbox.stub().resolves(true),
};
sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
sandbox.stub(PrivateBrowsingUtils, "permanentPrivateBrowsing").value(true);
await placesBackupResource.backup(stagingPath, sourcePath);
Assert.ok(
fakeConnection.backup.notCalled,
"No sqlite connections should have been made with permanent private browsing enabled"
);
await assertFilesExist(stagingPath, [{ path: "bookmarks.jsonlz4" }]);
await maybeRemovePath(stagingPath);
await maybeRemovePath(sourcePath);
sandbox.restore();
});

View File

@ -6,9 +6,6 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
const { CredentialsAndSecurityBackupResource } = ChromeUtils.importESModule(
"resource:///modules/backup/CredentialsAndSecurityBackupResource.sys.mjs"
);
const { PlacesBackupResource } = ChromeUtils.importESModule(
"resource:///modules/backup/PlacesBackupResource.sys.mjs"
);
const { AddonsBackupResource } = ChromeUtils.importESModule(
"resource:///modules/backup/AddonsBackupResource.sys.mjs"
);
@ -79,59 +76,6 @@ add_task(async function test_profDDiskSpace() {
);
});
/**
* Tests that we can measure Places DB related files in the profile directory.
*/
add_task(async function test_placesBackupResource() {
Services.fog.testResetFOG();
const EXPECTED_PLACES_DB_SIZE = 5240;
const EXPECTED_FAVICONS_DB_SIZE = 5240;
// Create resource files in temporary directory
const tempDir = PathUtils.tempDir;
let tempPlacesDBPath = PathUtils.join(tempDir, "places.sqlite");
let tempFaviconsDBPath = PathUtils.join(tempDir, "favicons.sqlite");
await createKilobyteSizedFile(tempPlacesDBPath, EXPECTED_PLACES_DB_SIZE);
await createKilobyteSizedFile(tempFaviconsDBPath, EXPECTED_FAVICONS_DB_SIZE);
let placesBackupResource = new PlacesBackupResource();
await placesBackupResource.measure(tempDir);
let placesMeasurement = Glean.browserBackup.placesSize.testGetValue();
let faviconsMeasurement = Glean.browserBackup.faviconsSize.testGetValue();
let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
// Compare glean vs telemetry measurements
TelemetryTestUtils.assertScalar(
scalars,
"browser.backup.places_size",
placesMeasurement,
"Glean and telemetry measurements for places.sqlite should be equal"
);
TelemetryTestUtils.assertScalar(
scalars,
"browser.backup.favicons_size",
faviconsMeasurement,
"Glean and telemetry measurements for favicons.sqlite should be equal"
);
// Compare glean measurements vs actual file sizes
Assert.equal(
placesMeasurement,
EXPECTED_PLACES_DB_SIZE,
"Should have collected the correct glean measurement for places.sqlite"
);
Assert.equal(
faviconsMeasurement,
EXPECTED_FAVICONS_DB_SIZE,
"Should have collected the correct glean measurement for favicons.sqlite"
);
await IOUtils.remove(tempPlacesDBPath);
await IOUtils.remove(tempFaviconsDBPath);
});
/**
* Tests that we can measure credentials related files in the profile directory.
*/

View File

@ -11,6 +11,8 @@ support-files = ["data/test_xulstore.json"]
["test_MiscDataBackupResource.js"]
["test_PlacesBackupResource.js"]
["test_PreferencesBackupResource.js"]
["test_createBackup.js"]