summaryrefslogtreecommitdiffstats
path: root/browser/components/backup
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/backup')
-rw-r--r--browser/components/backup/.eslintrc.js9
-rw-r--r--browser/components/backup/BackupResources.sys.mjs24
-rw-r--r--browser/components/backup/BackupService.sys.mjs129
-rw-r--r--browser/components/backup/content/debug.html46
-rw-r--r--browser/components/backup/content/debug.js59
-rw-r--r--browser/components/backup/docs/backup-resources.rst18
-rw-r--r--browser/components/backup/docs/index.rst1
-rw-r--r--browser/components/backup/jar.mn9
-rw-r--r--browser/components/backup/metrics.yaml276
-rw-r--r--browser/components/backup/moz.build10
-rw-r--r--browser/components/backup/resources/AddonsBackupResource.sys.mjs100
-rw-r--r--browser/components/backup/resources/BackupResource.sys.mjs83
-rw-r--r--browser/components/backup/resources/CookiesBackupResource.sys.mjs25
-rw-r--r--browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs53
-rw-r--r--browser/components/backup/resources/FormHistoryBackupResource.sys.mjs25
-rw-r--r--browser/components/backup/resources/MiscDataBackupResource.sys.mjs101
-rw-r--r--browser/components/backup/resources/PlacesBackupResource.sys.mjs91
-rw-r--r--browser/components/backup/resources/PreferencesBackupResource.sys.mjs98
-rw-r--r--browser/components/backup/resources/SessionStoreBackupResource.sys.mjs53
-rw-r--r--browser/components/backup/tests/xpcshell/head.js167
-rw-r--r--browser/components/backup/tests/xpcshell/test_BackupResource.js (renamed from browser/components/backup/tests/xpcshell/test_BrowserResource.js)26
-rw-r--r--browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js113
-rw-r--r--browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js226
-rw-r--r--browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js132
-rw-r--r--browser/components/backup/tests/xpcshell/test_createBackup.js74
-rw-r--r--browser/components/backup/tests/xpcshell/test_measurements.js547
-rw-r--r--browser/components/backup/tests/xpcshell/xpcshell.toml14
27 files changed, 2466 insertions, 43 deletions
diff --git a/browser/components/backup/.eslintrc.js b/browser/components/backup/.eslintrc.js
deleted file mode 100644
index 9aafb4a214..0000000000
--- a/browser/components/backup/.eslintrc.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-module.exports = {
- extends: ["plugin:mozilla/require-jsdoc"],
-};
diff --git a/browser/components/backup/BackupResources.sys.mjs b/browser/components/backup/BackupResources.sys.mjs
index 276fabefdf..ce7f53b10d 100644
--- a/browser/components/backup/BackupResources.sys.mjs
+++ b/browser/components/backup/BackupResources.sys.mjs
@@ -2,14 +2,28 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
-// Remove this import after BackupResource is referenced elsewhere.
-// eslint-disable-next-line no-unused-vars
-import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
-
/**
* Classes exported here are registered as a resource that can be
* backed up and restored in the BackupService.
*
* They must extend the BackupResource base class.
*/
-export {};
+import { AddonsBackupResource } from "resource:///modules/backup/AddonsBackupResource.sys.mjs";
+import { CookiesBackupResource } from "resource:///modules/backup/CookiesBackupResource.sys.mjs";
+import { CredentialsAndSecurityBackupResource } from "resource:///modules/backup/CredentialsAndSecurityBackupResource.sys.mjs";
+import { FormHistoryBackupResource } from "resource:///modules/backup/FormHistoryBackupResource.sys.mjs";
+import { MiscDataBackupResource } from "resource:///modules/backup/MiscDataBackupResource.sys.mjs";
+import { PlacesBackupResource } from "resource:///modules/backup/PlacesBackupResource.sys.mjs";
+import { PreferencesBackupResource } from "resource:///modules/backup/PreferencesBackupResource.sys.mjs";
+import { SessionStoreBackupResource } from "resource:///modules/backup/SessionStoreBackupResource.sys.mjs";
+
+export {
+ AddonsBackupResource,
+ CookiesBackupResource,
+ CredentialsAndSecurityBackupResource,
+ FormHistoryBackupResource,
+ MiscDataBackupResource,
+ PlacesBackupResource,
+ PreferencesBackupResource,
+ SessionStoreBackupResource,
+};
diff --git a/browser/components/backup/BackupService.sys.mjs b/browser/components/backup/BackupService.sys.mjs
index 853f4768ce..3521f315fd 100644
--- a/browser/components/backup/BackupService.sys.mjs
+++ b/browser/components/backup/BackupService.sys.mjs
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-import * as BackupResources from "resource:///modules/backup/BackupResources.sys.mjs";
+import * as DefaultBackupResources from "resource:///modules/backup/BackupResources.sys.mjs";
const lazy = {};
@@ -37,6 +37,13 @@ export class BackupService {
#resources = new Map();
/**
+ * True if a backup is currently in progress.
+ *
+ * @type {boolean}
+ */
+ #backupInProgress = false;
+
+ /**
* Returns a reference to a BackupService singleton. If this is the first time
* that this getter is accessed, this causes the BackupService singleton to be
* be instantiated.
@@ -48,27 +55,130 @@ export class BackupService {
if (this.#instance) {
return this.#instance;
}
- this.#instance = new BackupService(BackupResources);
+ this.#instance = new BackupService(DefaultBackupResources);
this.#instance.takeMeasurements();
return this.#instance;
}
/**
+ * Returns a reference to the BackupService singleton. If the singleton has
+ * not been initialized, an error is thrown.
+ *
+ * @static
+ * @returns {BackupService}
+ */
+ static get() {
+ if (!this.#instance) {
+ throw new Error("BackupService not initialized");
+ }
+ return this.#instance;
+ }
+
+ /**
* Create a BackupService instance.
*
- * @param {object} [backupResources=BackupResources] - Object containing BackupResource classes to associate with this service.
+ * @param {object} [backupResources=DefaultBackupResources] - Object containing BackupResource classes to associate with this service.
*/
- constructor(backupResources = BackupResources) {
+ constructor(backupResources = DefaultBackupResources) {
lazy.logConsole.debug("Instantiated");
for (const resourceName in backupResources) {
- let resource = BackupResources[resourceName];
+ let resource = backupResources[resourceName];
this.#resources.set(resource.key, resource);
}
}
/**
+ * Create a backup of the user's profile.
+ *
+ * @param {object} [options]
+ * Options for the backup.
+ * @param {string} [options.profilePath=PathUtils.profileDir]
+ * The path to the profile to backup. By default, this is the current
+ * profile.
+ * @returns {Promise<undefined>}
+ */
+ async createBackup({ profilePath = PathUtils.profileDir } = {}) {
+ // createBackup does not allow re-entry or concurrent backups.
+ if (this.#backupInProgress) {
+ lazy.logConsole.warn("Backup attempt already in progress");
+ return;
+ }
+
+ this.#backupInProgress = true;
+
+ try {
+ lazy.logConsole.debug(`Creating backup for profile at ${profilePath}`);
+
+ // First, check to see if a `backups` directory already exists in the
+ // profile.
+ let backupDirPath = PathUtils.join(profilePath, "backups");
+ lazy.logConsole.debug("Creating backups folder");
+
+ // ignoreExisting: true is the default, but we're being explicit that it's
+ // okay if this folder already exists.
+ await IOUtils.makeDirectory(backupDirPath, { ignoreExisting: true });
+
+ let stagingPath = await this.#prepareStagingFolder(backupDirPath);
+
+ // Perform the backup for each resource.
+ for (let resourceClass of this.#resources.values()) {
+ try {
+ lazy.logConsole.debug(
+ `Backing up resource with key ${resourceClass.key}. ` +
+ `Requires encryption: ${resourceClass.requiresEncryption}`
+ );
+ let resourcePath = PathUtils.join(stagingPath, resourceClass.key);
+ await IOUtils.makeDirectory(resourcePath);
+
+ // `backup` on each BackupResource should return us a ManifestEntry
+ // that we eventually write to a JSON manifest file, but for now,
+ // we're just going to log it.
+ let manifestEntry = await new resourceClass().backup(
+ resourcePath,
+ profilePath
+ );
+ lazy.logConsole.debug(
+ `Backup of resource with key ${resourceClass.key} completed`,
+ manifestEntry
+ );
+ } catch (e) {
+ lazy.logConsole.error(
+ `Failed to backup resource: ${resourceClass.key}`,
+ e
+ );
+ }
+ }
+ } finally {
+ this.#backupInProgress = false;
+ }
+ }
+
+ /**
+ * Constructs the staging folder for the backup in the passed in backup
+ * folder. If a pre-existing staging folder exists, it will be cleared out.
+ *
+ * @param {string} backupDirPath
+ * The path to the backup folder.
+ * @returns {Promise<string>}
+ * The path to the empty staging folder.
+ */
+ async #prepareStagingFolder(backupDirPath) {
+ let stagingPath = PathUtils.join(backupDirPath, "staging");
+ lazy.logConsole.debug("Checking for pre-existing staging folder");
+ if (await IOUtils.exists(stagingPath)) {
+ // A pre-existing staging folder exists. A previous backup attempt must
+ // have failed or been interrupted. We'll clear it out.
+ lazy.logConsole.warn("A pre-existing staging folder exists. Clearing.");
+ await IOUtils.remove(stagingPath, { recursive: true });
+ }
+ await IOUtils.makeDirectory(stagingPath);
+
+ return stagingPath;
+ }
+
+ /**
* Take measurements of the current profile state for Telemetry.
*
* @returns {Promise<undefined>}
@@ -97,7 +207,14 @@ export class BackupService {
// Measure the size of each file we are going to backup.
for (let resourceClass of this.#resources.values()) {
- await new resourceClass().measure(PathUtils.profileDir);
+ try {
+ await new resourceClass().measure(PathUtils.profileDir);
+ } catch (e) {
+ lazy.logConsole.error(
+ `Failed to measure for resource: ${resourceClass.key}`,
+ e
+ );
+ }
}
}
}
diff --git a/browser/components/backup/content/debug.html b/browser/components/backup/content/debug.html
new file mode 100644
index 0000000000..5d6517cf2a
--- /dev/null
+++ b/browser/components/backup/content/debug.html
@@ -0,0 +1,46 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Profile backup debug tool</title>
+
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ </head>
+ <body>
+ <header>
+ <h1>Profile backup debug tool</h1>
+ </header>
+
+ <main>
+ <section>
+ <h2>State</h2>
+ <ol>
+ <li>
+ <input
+ type="checkbox"
+ preference="browser.backup.enabled"
+ />BackupService component enabled
+ </li>
+ <li>
+ <input
+ type="checkbox"
+ preference="browser.backup.log"
+ />BackupService debug logging enabled
+ </li>
+ </ol>
+ </section>
+ <section id="controls">
+ <h2>Controls</h2>
+ <button id="create-backup">Create backup</button>
+ <button id="open-backup-folder">Open backups folder</button>
+ </section>
+ </main>
+
+ <script src="chrome://global/content/preferencesBindings.js"></script>
+ <script src="chrome://browser/content/backup/debug.js"></script>
+ </body>
+</html>
diff --git a/browser/components/backup/content/debug.js b/browser/components/backup/content/debug.js
new file mode 100644
index 0000000000..fd673818c0
--- /dev/null
+++ b/browser/components/backup/content/debug.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from /toolkit/content/preferencesBindings.js */
+
+Preferences.addAll([
+ { id: "browser.backup.enabled", type: "bool" },
+ { id: "browser.backup.log", type: "bool" },
+]);
+
+const { BackupService } = ChromeUtils.importESModule(
+ "resource:///modules/backup/BackupService.sys.mjs"
+);
+
+let DebugUI = {
+ init() {
+ let controls = document.querySelector("#controls");
+ controls.addEventListener("click", this);
+ },
+
+ handleEvent(event) {
+ let target = event.target;
+ if (HTMLButtonElement.isInstance(event.target)) {
+ this.onButtonClick(target);
+ }
+ },
+
+ async onButtonClick(button) {
+ switch (button.id) {
+ case "create-backup": {
+ let service = BackupService.get();
+ button.disabled = true;
+ await service.createBackup();
+ button.disabled = false;
+ break;
+ }
+ case "open-backup-folder": {
+ let backupsDir = PathUtils.join(PathUtils.profileDir, "backups");
+
+ let nsLocalFile = Components.Constructor(
+ "@mozilla.org/file/local;1",
+ "nsIFile",
+ "initWithPath"
+ );
+
+ if (await IOUtils.exists(backupsDir)) {
+ new nsLocalFile(backupsDir).reveal();
+ } else {
+ alert("backups folder doesn't exist yet");
+ }
+
+ break;
+ }
+ }
+ },
+};
+
+DebugUI.init();
diff --git a/browser/components/backup/docs/backup-resources.rst b/browser/components/backup/docs/backup-resources.rst
new file mode 100644
index 0000000000..4ead0d316d
--- /dev/null
+++ b/browser/components/backup/docs/backup-resources.rst
@@ -0,0 +1,18 @@
+================================
+Backup Resources Reference
+================================
+
+A ``BackupResource`` is the base class used to represent a group of data within
+a user profile that is logical to backup together. For example, the
+``PlacesBackupResource`` represents both the ``places.sqlite`` SQLite database,
+as well as the ``favicons.sqlite`` database. The ``AddonsBackupResource``
+represents not only the preferences for various addons, but also the XPI files
+that those addons are defined in.
+
+Each ``BackupResource`` subclass is registered for use by the
+``BackupService`` by adding it to the default set of exported classes in the
+``BackupResources`` module in ``BackupResources.sys.mjs``.
+
+.. js:autoclass:: BackupResource
+ :members:
+ :private-members:
diff --git a/browser/components/backup/docs/index.rst b/browser/components/backup/docs/index.rst
index 1e201f8f1c..db9995dad2 100644
--- a/browser/components/backup/docs/index.rst
+++ b/browser/components/backup/docs/index.rst
@@ -11,3 +11,4 @@ into a single file that can be easily restored from.
:maxdepth: 3
backup-service
+ backup-resources
diff --git a/browser/components/backup/jar.mn b/browser/components/backup/jar.mn
new file mode 100644
index 0000000000..7800962486
--- /dev/null
+++ b/browser/components/backup/jar.mn
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+browser.jar:
+#ifdef NIGHTLY_BUILD
+ content/browser/backup/debug.html (content/debug.html)
+ content/browser/backup/debug.js (content/debug.js)
+#endif
diff --git a/browser/components/backup/metrics.yaml b/browser/components/backup/metrics.yaml
index 6d6a16a178..cf6f95ee75 100644
--- a/browser/components/backup/metrics.yaml
+++ b/browser/components/backup/metrics.yaml
@@ -28,3 +28,279 @@ browser.backup:
- mconley@mozilla.com
expires: never
telemetry_mirror: BROWSER_BACKUP_PROF_D_DISK_SPACE
+
+ places_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total file size of the places.sqlite db located in the current profile
+ directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883642
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883642
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_PLACES_SIZE
+
+ favicons_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total file size of the favicons.sqlite db located in the current profile
+ directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883642
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883642
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_FAVICONS_SIZE
+
+ credentials_data_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of logins, payment method, and form autofill related files
+ in the current profile directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883736
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883736
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_CREDENTIALS_DATA_SIZE
+
+ security_data_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of files needed for NSS initialization parameters and security
+ certificate settings in the current profile directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883736
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883736
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_SECURITY_DATA_SIZE
+
+ preferences_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of files relating to user preferences and permissions in the current profile
+ directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883739
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883739
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_PREFERENCES_SIZE
+
+ misc_data_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of files for telemetry, site storage, media device origin mapping,
+ chrome privileged IndexedDB databases, and Mozilla Accounts in the current profile directory,
+ rounded to the nearest tenth kilobyte.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883747
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1887746
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883747
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1887746
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_MISC_DATA_SIZE
+
+ cookies_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total file size of the cookies.sqlite db located in the current profile
+ directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_COOKIES_SIZE
+
+ form_history_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The file size of the formhistory.sqlite db located in the current profile
+ directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_FORM_HISTORY_SIZE
+
+ session_store_backups_directory_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of the session store backups directory, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_SESSION_STORE_BACKUPS_DIRECTORY_SIZE
+
+ session_store_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The size of uncompressed session store json, in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_SESSION_STORE_SIZE
+
+ extensions_json_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total file size of the current profiles extensions metadata files,
+ rounded to the nearest 10 kilobytes.
+ Files included are:
+ - extensions.json
+ - extension-settings.json
+ - extension-preferences.json
+ - addonStartup.json.lz4
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_EXTENSIONS_JSON_SIZE
+
+ extension_store_permissions_data_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The file size of the current profiles extension-store-permissions/data.safe.bin
+ file, rounded to the nearest 10 kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_EXTENSION_STORE_PERMISSIONS_DATA_SIZE
+
+ storage_sync_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The file size of the current profiles storage-sync-v2.sqlite db,
+ rounded to the nearest 10 kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_STORAGE_SYNC_SIZE
+
+ browser_extension_data_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of the current profiles storage.local legacy JSON backend
+ in the browser-extension-data directory, rounded to the nearest 10 kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_BROWSER_EXTENSION_DATA_SIZE
+
+ extensions_xpi_directory_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of the current profiles extensions directory,
+ rounded to the nearest 10 kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_EXTENSIONS_XPI_DIRECTORY_SIZE
+
+ extensions_storage_size:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total size of all extensions storage directories,
+ rounded to the nearest 10 kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_EXTENSIONS_STORAGE_SIZE
diff --git a/browser/components/backup/moz.build b/browser/components/backup/moz.build
index 0ea7d66b7d..be548ce81f 100644
--- a/browser/components/backup/moz.build
+++ b/browser/components/backup/moz.build
@@ -7,6 +7,8 @@
with Files("**"):
BUG_COMPONENT = ("Firefox", "Profiles")
+JAR_MANIFESTS += ["jar.mn"]
+
SPHINX_TREES["docs"] = "docs"
XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
@@ -14,5 +16,13 @@ XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
EXTRA_JS_MODULES.backup += [
"BackupResources.sys.mjs",
"BackupService.sys.mjs",
+ "resources/AddonsBackupResource.sys.mjs",
"resources/BackupResource.sys.mjs",
+ "resources/CookiesBackupResource.sys.mjs",
+ "resources/CredentialsAndSecurityBackupResource.sys.mjs",
+ "resources/FormHistoryBackupResource.sys.mjs",
+ "resources/MiscDataBackupResource.sys.mjs",
+ "resources/PlacesBackupResource.sys.mjs",
+ "resources/PreferencesBackupResource.sys.mjs",
+ "resources/SessionStoreBackupResource.sys.mjs",
]
diff --git a/browser/components/backup/resources/AddonsBackupResource.sys.mjs b/browser/components/backup/resources/AddonsBackupResource.sys.mjs
new file mode 100644
index 0000000000..83b97ed2f2
--- /dev/null
+++ b/browser/components/backup/resources/AddonsBackupResource.sys.mjs
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
+
+/**
+ * Backup for addons and extensions files and data.
+ */
+export class AddonsBackupResource extends BackupResource {
+ static get key() {
+ return "addons";
+ }
+
+ static get requiresEncryption() {
+ return false;
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ // Report the total size of the extension json files.
+ const jsonFiles = [
+ "extensions.json",
+ "extension-settings.json",
+ "extension-preferences.json",
+ "addonStartup.json.lz4",
+ ];
+ let extensionsJsonSize = 0;
+ for (const filePath of jsonFiles) {
+ let resourcePath = PathUtils.join(profilePath, filePath);
+ let resourceSize = await BackupResource.getFileSize(resourcePath);
+ if (Number.isInteger(resourceSize)) {
+ extensionsJsonSize += resourceSize;
+ }
+ }
+ Glean.browserBackup.extensionsJsonSize.set(extensionsJsonSize);
+
+ // Report the size of permissions store data, if present.
+ let extensionStorePermissionsDataPath = PathUtils.join(
+ profilePath,
+ "extension-store-permissions",
+ "data.safe.bin"
+ );
+ let extensionStorePermissionsDataSize = await BackupResource.getFileSize(
+ extensionStorePermissionsDataPath
+ );
+ if (Number.isInteger(extensionStorePermissionsDataSize)) {
+ Glean.browserBackup.extensionStorePermissionsDataSize.set(
+ extensionStorePermissionsDataSize
+ );
+ }
+
+ // Report the size of extensions storage sync database.
+ let storageSyncPath = PathUtils.join(profilePath, "storage-sync-v2.sqlite");
+ let storageSyncSize = await BackupResource.getFileSize(storageSyncPath);
+ Glean.browserBackup.storageSyncSize.set(storageSyncSize);
+
+ // Report the total size of XPI files in the extensions directory.
+ let extensionsXpiDirectoryPath = PathUtils.join(profilePath, "extensions");
+ let extensionsXpiDirectorySize = await BackupResource.getDirectorySize(
+ extensionsXpiDirectoryPath,
+ {
+ shouldExclude: (filePath, fileType) =>
+ fileType !== "regular" || !filePath.endsWith(".xpi"),
+ }
+ );
+ Glean.browserBackup.extensionsXpiDirectorySize.set(
+ extensionsXpiDirectorySize
+ );
+
+ // Report the total size of the browser extension data.
+ let browserExtensionDataPath = PathUtils.join(
+ profilePath,
+ "browser-extension-data"
+ );
+ let browserExtensionDataSize = await BackupResource.getDirectorySize(
+ browserExtensionDataPath
+ );
+ Glean.browserBackup.browserExtensionDataSize.set(browserExtensionDataSize);
+
+ // Report the size of all moz-extension IndexedDB databases.
+ let defaultStoragePath = PathUtils.join(profilePath, "storage", "default");
+ let extensionsStorageSize = await BackupResource.getDirectorySize(
+ defaultStoragePath,
+ {
+ shouldExclude: (filePath, _fileType, parentPath) => {
+ if (
+ parentPath == defaultStoragePath &&
+ !PathUtils.filename(filePath).startsWith("moz-extension")
+ ) {
+ return true;
+ }
+ return false;
+ },
+ }
+ );
+ if (Number.isInteger(extensionsStorageSize)) {
+ Glean.browserBackup.extensionsStorageSize.set(extensionsStorageSize);
+ }
+ }
+}
diff --git a/browser/components/backup/resources/BackupResource.sys.mjs b/browser/components/backup/resources/BackupResource.sys.mjs
index bde3f0669c..d851eb5199 100644
--- a/browser/components/backup/resources/BackupResource.sys.mjs
+++ b/browser/components/backup/resources/BackupResource.sys.mjs
@@ -3,7 +3,19 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// Convert from bytes to kilobytes (not kibibytes).
-const BYTES_IN_KB = 1000;
+export const BYTES_IN_KB = 1000;
+
+/**
+ * Convert bytes to the nearest 10th kilobyte to make the measurements fuzzier.
+ *
+ * @param {number} bytes - size in bytes.
+ * @returns {number} - size in kilobytes rounded to the nearest 10th kilobyte.
+ */
+export function bytesToFuzzyKilobytes(bytes) {
+ let sizeInKb = Math.ceil(bytes / BYTES_IN_KB);
+ let nearestTenthKb = Math.round(sizeInKb / 10) * 10;
+ return Math.max(nearestTenthKb, 1);
+}
/**
* An abstract class representing a set of data within a user profile
@@ -23,6 +35,21 @@ export class BackupResource {
}
/**
+ * This must be overridden to return a boolean indicating whether the
+ * resource requires encryption when being backed up. Encryption should be
+ * required for particularly sensitive data, such as passwords / credentials,
+ * cookies, or payment methods. If you're not sure, talk to someone from the
+ * Privacy team.
+ *
+ * @type {boolean}
+ */
+ static get requiresEncryption() {
+ throw new Error(
+ "BackupResource::requiresEncryption needs to be overridden."
+ );
+ }
+
+ /**
* Get the size of a file.
*
* @param {string} filePath - path to a file.
@@ -40,21 +67,25 @@ export class BackupResource {
return null;
}
- let sizeInKb = Math.ceil(size / BYTES_IN_KB);
- // Make the measurement fuzzier by rounding to the nearest 10kb.
- let nearestTenthKb = Math.round(sizeInKb / 10) * 10;
+ let nearestTenthKb = bytesToFuzzyKilobytes(size);
- return Math.max(nearestTenthKb, 1);
+ return nearestTenthKb;
}
/**
* Get the total size of a directory.
*
* @param {string} directoryPath - path to a directory.
+ * @param {object} options - A set of additional optional parameters.
+ * @param {Function} [options.shouldExclude] - an optional callback which based on file path and file type should return true
+ * if the file should be excluded from the computed directory size.
* @returns {Promise<number|null>} - the size of all descendants of the directory in kilobytes, or null if the
* directory does not exist, the path is not a directory or the size is unknown.
*/
- static async getDirectorySize(directoryPath) {
+ static async getDirectorySize(
+ directoryPath,
+ { shouldExclude = () => false } = {}
+ ) {
if (!(await IOUtils.exists(directoryPath))) {
return null;
}
@@ -75,15 +106,20 @@ export class BackupResource {
childFilePath
);
+ if (shouldExclude(childFilePath, childType, directoryPath)) {
+ continue;
+ }
+
if (childSize >= 0) {
- let sizeInKb = Math.ceil(childSize / BYTES_IN_KB);
- // Make the measurement fuzzier by rounding to the nearest 10kb.
- let nearestTenthKb = Math.round(sizeInKb / 10) * 10;
- size += Math.max(nearestTenthKb, 1);
+ let nearestTenthKb = bytesToFuzzyKilobytes(childSize);
+
+ size += nearestTenthKb;
}
if (childType == "directory") {
- let childDirectorySize = await this.getDirectorySize(childFilePath);
+ let childDirectorySize = await this.getDirectorySize(childFilePath, {
+ shouldExclude,
+ });
if (Number.isInteger(childDirectorySize)) {
size += childDirectorySize;
}
@@ -106,4 +142,29 @@ export class BackupResource {
async measure(profilePath) {
throw new Error("BackupResource::measure needs to be overridden.");
}
+
+ /**
+ * Perform a safe copy of the resource(s) and write them into the backup
+ * database. The Promise should resolve with an object that can be serialized
+ * to JSON, as it will be written to the manifest file. This same object will
+ * be deserialized and passed to restore() when restoring the backup. This
+ * object can be null if no additional information is needed to restore the
+ * backup.
+ *
+ * @param {string} stagingPath
+ * The path to the staging folder where copies of the datastores for this
+ * BackupResource should be written to.
+ * @param {string} [profilePath=null]
+ * This is null if the backup is being run on the currently running user
+ * profile. If, however, the backup is being run on a different user profile
+ * (for example, it's being run from a BackgroundTask on a user profile that
+ * just shut down, or during test), then this is a string set to that user
+ * profile path.
+ *
+ * @returns {Promise<object|null>}
+ */
+ // eslint-disable-next-line no-unused-vars
+ async backup(stagingPath, profilePath = null) {
+ throw new Error("BackupResource::backup must be overridden");
+ }
}
diff --git a/browser/components/backup/resources/CookiesBackupResource.sys.mjs b/browser/components/backup/resources/CookiesBackupResource.sys.mjs
new file mode 100644
index 0000000000..8b988fd532
--- /dev/null
+++ b/browser/components/backup/resources/CookiesBackupResource.sys.mjs
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
+
+/**
+ * Class representing Cookies database within a user profile.
+ */
+export class CookiesBackupResource extends BackupResource {
+ static get key() {
+ return "cookies";
+ }
+
+ static get requiresEncryption() {
+ return true;
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ let cookiesDBPath = PathUtils.join(profilePath, "cookies.sqlite");
+ let cookiesSize = await BackupResource.getFileSize(cookiesDBPath);
+
+ Glean.browserBackup.cookiesSize.set(cookiesSize);
+ }
+}
diff --git a/browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs b/browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs
new file mode 100644
index 0000000000..89069de826
--- /dev/null
+++ b/browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
+
+/**
+ * Class representing files needed for logins, payment methods and form autofill within a user profile.
+ */
+export class CredentialsAndSecurityBackupResource extends BackupResource {
+ static get key() {
+ return "credentials_and_security";
+ }
+
+ static get requiresEncryption() {
+ return true;
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ const securityFiles = ["cert9.db", "pkcs11.txt"];
+ let securitySize = 0;
+
+ for (let filePath of securityFiles) {
+ let resourcePath = PathUtils.join(profilePath, filePath);
+ let resourceSize = await BackupResource.getFileSize(resourcePath);
+ if (Number.isInteger(resourceSize)) {
+ securitySize += resourceSize;
+ }
+ }
+
+ Glean.browserBackup.securityDataSize.set(securitySize);
+
+ const credentialsFiles = [
+ "key4.db",
+ "logins.json",
+ "logins-backup.json",
+ "autofill-profiles.json",
+ "credentialstate.sqlite",
+ "signedInUser.json",
+ ];
+ let credentialsSize = 0;
+
+ for (let filePath of credentialsFiles) {
+ let resourcePath = PathUtils.join(profilePath, filePath);
+ let resourceSize = await BackupResource.getFileSize(resourcePath);
+ if (Number.isInteger(resourceSize)) {
+ credentialsSize += resourceSize;
+ }
+ }
+
+ Glean.browserBackup.credentialsDataSize.set(credentialsSize);
+ }
+}
diff --git a/browser/components/backup/resources/FormHistoryBackupResource.sys.mjs b/browser/components/backup/resources/FormHistoryBackupResource.sys.mjs
new file mode 100644
index 0000000000..cb314eb34d
--- /dev/null
+++ b/browser/components/backup/resources/FormHistoryBackupResource.sys.mjs
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
+
+/**
+ * Class representing Form history database within a user profile.
+ */
+export class FormHistoryBackupResource extends BackupResource {
+ static get key() {
+ return "formhistory";
+ }
+
+ static get requiresEncryption() {
+ return false;
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ let formHistoryDBPath = PathUtils.join(profilePath, "formhistory.sqlite");
+ let formHistorySize = await BackupResource.getFileSize(formHistoryDBPath);
+
+ Glean.browserBackup.formHistorySize.set(formHistorySize);
+ }
+}
diff --git a/browser/components/backup/resources/MiscDataBackupResource.sys.mjs b/browser/components/backup/resources/MiscDataBackupResource.sys.mjs
new file mode 100644
index 0000000000..97224f0e31
--- /dev/null
+++ b/browser/components/backup/resources/MiscDataBackupResource.sys.mjs
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
+});
+
+/**
+ * Class representing miscellaneous files for telemetry, site storage,
+ * media device origin mapping, chrome privileged IndexedDB databases,
+ * and Mozilla Accounts within a user profile.
+ */
+export class MiscDataBackupResource extends BackupResource {
+ static get key() {
+ return "miscellaneous";
+ }
+
+ static get requiresEncryption() {
+ return false;
+ }
+
+ async backup(stagingPath, profilePath = PathUtils.profileDir) {
+ const files = [
+ "times.json",
+ "enumerate_devices.txt",
+ "SiteSecurityServiceState.bin",
+ ];
+
+ for (let fileName of files) {
+ let sourcePath = PathUtils.join(profilePath, fileName);
+ let destPath = PathUtils.join(stagingPath, fileName);
+ if (await IOUtils.exists(sourcePath)) {
+ await IOUtils.copy(sourcePath, destPath, { recursive: true });
+ }
+ }
+
+ const sqliteDatabases = ["protections.sqlite"];
+
+ 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();
+ }
+ }
+
+ // Bug 1890585 - we don't currently have the ability to copy the
+ // chrome-privileged IndexedDB databases under storage/permanent/chrome, so
+ // we'll just skip that for now.
+
+ return null;
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ const files = [
+ "times.json",
+ "enumerate_devices.txt",
+ "protections.sqlite",
+ "SiteSecurityServiceState.bin",
+ ];
+
+ let fullSize = 0;
+
+ for (let filePath of files) {
+ let resourcePath = PathUtils.join(profilePath, filePath);
+ let resourceSize = await BackupResource.getFileSize(resourcePath);
+ if (Number.isInteger(resourceSize)) {
+ fullSize += resourceSize;
+ }
+ }
+
+ let chromeIndexedDBDirPath = PathUtils.join(
+ profilePath,
+ "storage",
+ "permanent",
+ "chrome"
+ );
+ let chromeIndexedDBDirSize = await BackupResource.getDirectorySize(
+ chromeIndexedDBDirPath
+ );
+ if (Number.isInteger(chromeIndexedDBDirSize)) {
+ fullSize += chromeIndexedDBDirSize;
+ }
+
+ Glean.browserBackup.miscDataSize.set(fullSize);
+ }
+}
diff --git a/browser/components/backup/resources/PlacesBackupResource.sys.mjs b/browser/components/backup/resources/PlacesBackupResource.sys.mjs
new file mode 100644
index 0000000000..1955406f51
--- /dev/null
+++ b/browser/components/backup/resources/PlacesBackupResource.sys.mjs
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * 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.
+ */
+export class PlacesBackupResource extends BackupResource {
+ static get key() {
+ return "places";
+ }
+
+ static get requiresEncryption() {
+ 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");
+ let placesDBSize = await BackupResource.getFileSize(placesDBPath);
+ let faviconsDBSize = await BackupResource.getFileSize(faviconsDBPath);
+
+ Glean.browserBackup.placesSize.set(placesDBSize);
+ Glean.browserBackup.faviconsSize.set(faviconsDBSize);
+ }
+}
diff --git a/browser/components/backup/resources/PreferencesBackupResource.sys.mjs b/browser/components/backup/resources/PreferencesBackupResource.sys.mjs
new file mode 100644
index 0000000000..012c0bf91e
--- /dev/null
+++ b/browser/components/backup/resources/PreferencesBackupResource.sys.mjs
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
+import { Sqlite } from "resource://gre/modules/Sqlite.sys.mjs";
+
+/**
+ * Class representing files that modify preferences and permissions within a user profile.
+ */
+export class PreferencesBackupResource extends BackupResource {
+ static get key() {
+ return "preferences";
+ }
+
+ static get requiresEncryption() {
+ return false;
+ }
+
+ async backup(stagingPath, profilePath = PathUtils.profileDir) {
+ // These are files that can be simply copied into the staging folder using
+ // IOUtils.copy.
+ const simpleCopyFiles = [
+ "xulstore.json",
+ "containers.json",
+ "handlers.json",
+ "search.json.mozlz4",
+ "user.js",
+ "chrome",
+ ];
+
+ for (let fileName of simpleCopyFiles) {
+ let sourcePath = PathUtils.join(profilePath, fileName);
+ let destPath = PathUtils.join(stagingPath, fileName);
+ if (await IOUtils.exists(sourcePath)) {
+ await IOUtils.copy(sourcePath, destPath, { recursive: true });
+ }
+ }
+
+ const sqliteDatabases = ["permissions.sqlite", "content-prefs.sqlite"];
+
+ for (let fileName of sqliteDatabases) {
+ let sourcePath = PathUtils.join(profilePath, fileName);
+ let destPath = PathUtils.join(stagingPath, fileName);
+ let connection;
+
+ try {
+ connection = await Sqlite.openConnection({
+ path: sourcePath,
+ });
+
+ await connection.backup(destPath);
+ } finally {
+ await connection.close();
+ }
+ }
+
+ // prefs.js is a special case - we have a helper function to flush the
+ // current prefs state to disk off of the main thread.
+ let prefsDestPath = PathUtils.join(stagingPath, "prefs.js");
+ let prefsDestFile = await IOUtils.getFile(prefsDestPath);
+ await Services.prefs.backupPrefFile(prefsDestFile);
+
+ return null;
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ const files = [
+ "prefs.js",
+ "xulstore.json",
+ "permissions.sqlite",
+ "content-prefs.sqlite",
+ "containers.json",
+ "handlers.json",
+ "search.json.mozlz4",
+ "user.js",
+ ];
+ let fullSize = 0;
+
+ for (let filePath of files) {
+ let resourcePath = PathUtils.join(profilePath, filePath);
+ let resourceSize = await BackupResource.getFileSize(resourcePath);
+ if (Number.isInteger(resourceSize)) {
+ fullSize += resourceSize;
+ }
+ }
+
+ const chromeDirectoryPath = PathUtils.join(profilePath, "chrome");
+ let chromeDirectorySize = await BackupResource.getDirectorySize(
+ chromeDirectoryPath
+ );
+ if (Number.isInteger(chromeDirectorySize)) {
+ fullSize += chromeDirectorySize;
+ }
+
+ Glean.browserBackup.preferencesSize.set(fullSize);
+ }
+}
diff --git a/browser/components/backup/resources/SessionStoreBackupResource.sys.mjs b/browser/components/backup/resources/SessionStoreBackupResource.sys.mjs
new file mode 100644
index 0000000000..fa5dcca848
--- /dev/null
+++ b/browser/components/backup/resources/SessionStoreBackupResource.sys.mjs
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+import {
+ BackupResource,
+ bytesToFuzzyKilobytes,
+} from "resource:///modules/backup/BackupResource.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+});
+
+/**
+ * Class representing Session store related files within a user profile.
+ */
+export class SessionStoreBackupResource extends BackupResource {
+ static get key() {
+ return "sessionstore";
+ }
+
+ static get requiresEncryption() {
+ // Session store data does not require encryption, but if encryption is
+ // disabled, then session cookies will be cleared from the backup before
+ // writing it to the disk.
+ return false;
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ // Get the current state of the session store JSON and
+ // measure it's uncompressed size.
+ let sessionStoreJson = lazy.SessionStore.getCurrentState(true);
+ let sessionStoreSize = new TextEncoder().encode(
+ JSON.stringify(sessionStoreJson)
+ ).byteLength;
+ let sessionStoreNearestTenthKb = bytesToFuzzyKilobytes(sessionStoreSize);
+
+ Glean.browserBackup.sessionStoreSize.set(sessionStoreNearestTenthKb);
+
+ let sessionStoreBackupsDirectoryPath = PathUtils.join(
+ profilePath,
+ "sessionstore-backups"
+ );
+ let sessionStoreBackupsDirectorySize =
+ await BackupResource.getDirectorySize(sessionStoreBackupsDirectoryPath);
+
+ Glean.browserBackup.sessionStoreBackupsDirectorySize.set(
+ sessionStoreBackupsDirectorySize
+ );
+ }
+}
diff --git a/browser/components/backup/tests/xpcshell/head.js b/browser/components/backup/tests/xpcshell/head.js
new file mode 100644
index 0000000000..2402870a13
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/head.js
@@ -0,0 +1,167 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { BackupService } = ChromeUtils.importESModule(
+ "resource:///modules/backup/BackupService.sys.mjs"
+);
+
+const { BackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/BackupResource.sys.mjs"
+);
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const { Sqlite } = ChromeUtils.importESModule(
+ "resource://gre/modules/Sqlite.sys.mjs"
+);
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const BYTES_IN_KB = 1000;
+
+do_get_profile();
+
+/**
+ * Some fake backup resource classes to test with.
+ */
+class FakeBackupResource1 extends BackupResource {
+ static get key() {
+ return "fake1";
+ }
+ static get requiresEncryption() {
+ return false;
+ }
+}
+
+/**
+ * Another fake backup resource class to test with.
+ */
+class FakeBackupResource2 extends BackupResource {
+ static get key() {
+ return "fake2";
+ }
+ static get requiresEncryption() {
+ return true;
+ }
+}
+
+/**
+ * Yet another fake backup resource class to test with.
+ */
+class FakeBackupResource3 extends BackupResource {
+ static get key() {
+ return "fake3";
+ }
+ static get requiresEncryption() {
+ return false;
+ }
+}
+
+/**
+ * Create a file of a given size in kilobytes.
+ *
+ * @param {string} path the path where the file will be created.
+ * @param {number} sizeInKB size file in Kilobytes.
+ * @returns {Promise<undefined>}
+ */
+async function createKilobyteSizedFile(path, sizeInKB) {
+ let bytes = new Uint8Array(sizeInKB * BYTES_IN_KB);
+ await IOUtils.write(path, bytes);
+}
+
+/**
+ * @typedef {object} TestFileObject
+ * @property {(string|Array.<string>)} path
+ * The relative path of the file. It can be a string or an array of strings
+ * in the event that directories need to be created. For example, this is
+ * an array of valid TestFileObjects.
+ *
+ * [
+ * { path: "file1.txt" },
+ * { path: ["dir1", "file2.txt"] },
+ * { path: ["dir2", "dir3", "file3.txt"], sizeInKB: 25 },
+ * { path: "file4.txt" },
+ * ]
+ *
+ * @property {number} [sizeInKB=10]
+ * The size of the created file in kilobytes. Defaults to 10.
+ */
+
+/**
+ * Easily creates a series of test files and directories under parentPath.
+ *
+ * @param {string} parentPath
+ * The path to the parent directory where the files will be created.
+ * @param {TestFileObject[]} testFilesArray
+ * An array of TestFileObjects describing what test files to create within
+ * the parentPath.
+ * @see TestFileObject
+ * @returns {Promise<undefined>}
+ */
+async function createTestFiles(parentPath, testFilesArray) {
+ for (let { path, sizeInKB } of testFilesArray) {
+ if (Array.isArray(path)) {
+ // Make a copy of the array of path elements, chopping off the last one.
+ // We'll assume the unchopped items are directories, and make sure they
+ // exist first.
+ let folders = path.slice(0, -1);
+ await IOUtils.getDirectory(PathUtils.join(parentPath, ...folders));
+ }
+
+ if (sizeInKB === undefined) {
+ sizeInKB = 10;
+ }
+
+ // This little piece of cleverness coerces a string into an array of one
+ // if path is a string, or just leaves it alone if it's already an array.
+ let filePath = PathUtils.join(parentPath, ...[].concat(path));
+ await createKilobyteSizedFile(filePath, sizeInKB);
+ }
+}
+
+/**
+ * Checks that files exist within a particular folder. The filesize is not
+ * checked.
+ *
+ * @param {string} parentPath
+ * The path to the parent directory where the files should exist.
+ * @param {TestFileObject[]} testFilesArray
+ * An array of TestFileObjects describing what test files to search for within
+ * parentPath.
+ * @see TestFileObject
+ * @returns {Promise<undefined>}
+ */
+async function assertFilesExist(parentPath, testFilesArray) {
+ for (let { path } of testFilesArray) {
+ let copiedFileName = PathUtils.join(parentPath, ...[].concat(path));
+ Assert.ok(
+ await IOUtils.exists(copiedFileName),
+ `${copiedFileName} should exist in the staging folder`
+ );
+ }
+}
+
+/**
+ * Remove a file or directory at a path if it exists and files are unlocked.
+ *
+ * @param {string} path path to remove.
+ */
+async function maybeRemovePath(path) {
+ try {
+ await IOUtils.remove(path, { ignoreAbsent: true, recursive: true });
+ } catch (error) {
+ // Sometimes remove() throws when the file is not unlocked soon
+ // enough.
+ if (error.name != "NS_ERROR_FILE_IS_LOCKED") {
+ // Ignoring any errors, as the temp folder will be cleaned up.
+ console.error(error);
+ }
+ }
+}
diff --git a/browser/components/backup/tests/xpcshell/test_BrowserResource.js b/browser/components/backup/tests/xpcshell/test_BackupResource.js
index 23c8e077a5..6623f4cd77 100644
--- a/browser/components/backup/tests/xpcshell/test_BrowserResource.js
+++ b/browser/components/backup/tests/xpcshell/test_BackupResource.js
@@ -3,16 +3,12 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
-const { BackupResource } = ChromeUtils.importESModule(
+const { bytesToFuzzyKilobytes } = ChromeUtils.importESModule(
"resource:///modules/backup/BackupResource.sys.mjs"
);
const EXPECTED_KILOBYTES_FOR_XULSTORE = 1;
-add_setup(() => {
- do_get_profile();
-});
-
/**
* Tests that BackupService.getFileSize will get the size of a file in kilobytes.
*/
@@ -35,7 +31,7 @@ add_task(async function test_getFileSize() {
});
/**
- * Tests that BackupService.getFileSize will get the total size of all the files in a directory and it's children in kilobytes.
+ * Tests that BackupService.getDirectorySize will get the total size of all the files in a directory and it's children in kilobytes.
*/
add_task(async function test_getDirectorySize() {
let file = do_get_file("data/test_xulstore.json");
@@ -61,3 +57,21 @@ add_task(async function test_getDirectorySize() {
await IOUtils.remove(testDir, { recursive: true });
});
+
+/**
+ * Tests that bytesToFuzzyKilobytes will convert bytes to kilobytes
+ * and round up to the nearest tenth kilobyte.
+ */
+add_task(async function test_bytesToFuzzyKilobytes() {
+ let largeSize = bytesToFuzzyKilobytes(1234000);
+
+ Assert.equal(
+ largeSize,
+ 1230,
+ "1234 bytes is rounded up to the nearest tenth kilobyte, 1230"
+ );
+
+ let smallSize = bytesToFuzzyKilobytes(3);
+
+ Assert.equal(smallSize, 1, "Sizes under 10 kilobytes return 1 kilobyte");
+});
diff --git a/browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js b/browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js
new file mode 100644
index 0000000000..e57dd50cd3
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { MiscDataBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/MiscDataBackupResource.sys.mjs"
+);
+
+/**
+ * Tests that we can measure miscellaneous files in the profile directory.
+ */
+add_task(async function test_measure() {
+ Services.fog.testResetFOG();
+
+ const EXPECTED_MISC_KILOBYTES_SIZE = 241;
+ const tempDir = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "MiscDataBackupResource-measurement-test"
+ );
+
+ const mockFiles = [
+ { path: "times.json", sizeInKB: 5 },
+ { path: "enumerate_devices.txt", sizeInKB: 1 },
+ { path: "protections.sqlite", sizeInKB: 100 },
+ { path: "SiteSecurityServiceState.bin", sizeInKB: 10 },
+ { path: ["storage", "permanent", "chrome", "123ABC.sqlite"], sizeInKB: 40 },
+ { path: ["storage", "permanent", "chrome", "456DEF.sqlite"], sizeInKB: 40 },
+ {
+ path: ["storage", "permanent", "chrome", "mockIDBDir", "890HIJ.sqlite"],
+ sizeInKB: 40,
+ },
+ ];
+
+ await createTestFiles(tempDir, mockFiles);
+
+ let miscDataBackupResource = new MiscDataBackupResource();
+ await miscDataBackupResource.measure(tempDir);
+
+ let measurement = Glean.browserBackup.miscDataSize.testGetValue();
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.misc_data_size",
+ measurement,
+ "Glean and telemetry measurements for misc data should be equal"
+ );
+ Assert.equal(
+ measurement,
+ EXPECTED_MISC_KILOBYTES_SIZE,
+ "Should have collected the correct glean measurement for misc files"
+ );
+
+ await maybeRemovePath(tempDir);
+});
+
+add_task(async function test_backup() {
+ let sandbox = sinon.createSandbox();
+
+ let miscDataBackupResource = new MiscDataBackupResource();
+ let sourcePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "MiscDataBackupResource-source-test"
+ );
+ let stagingPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "MiscDataBackupResource-staging-test"
+ );
+
+ const simpleCopyFiles = [
+ { path: "times.json" },
+ { path: "enumerate_devices.txt" },
+ { path: "SiteSecurityServiceState.bin" },
+ ];
+ await createTestFiles(sourcePath, simpleCopyFiles);
+
+ // We have no need to test that Sqlite.sys.mjs's backup method is working -
+ // this is something that is tested in Sqlite's own tests. We can just make
+ // sure that it's being called using sinon. Unfortunately, we cannot do the
+ // same thing with IOUtils.copy, as its methods are not stubbable.
+ let fakeConnection = {
+ backup: sandbox.stub().resolves(true),
+ close: sandbox.stub().resolves(true),
+ };
+ sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
+
+ await miscDataBackupResource.backup(stagingPath, sourcePath);
+
+ await assertFilesExist(stagingPath, simpleCopyFiles);
+
+ // Next, we'll make sure that the Sqlite connection had `backup` called on it
+ // with the right arguments.
+ Assert.ok(
+ fakeConnection.backup.calledOnce,
+ "Called backup the expected number of times for all connections"
+ );
+ Assert.ok(
+ fakeConnection.backup.firstCall.calledWith(
+ PathUtils.join(stagingPath, "protections.sqlite")
+ ),
+ "Called backup on the protections.sqlite Sqlite connection"
+ );
+
+ // Bug 1890585 - we don't currently have the ability to copy the
+ // chrome-privileged IndexedDB databases under storage/permanent/chrome, so
+ // we'll just skip testing that for now.
+
+ await maybeRemovePath(stagingPath);
+ await maybeRemovePath(sourcePath);
+
+ sandbox.restore();
+});
diff --git a/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js b/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js
new file mode 100644
index 0000000000..de97281372
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js
@@ -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();
+});
diff --git a/browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js b/browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js
new file mode 100644
index 0000000000..6845431bb8
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PreferencesBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/PreferencesBackupResource.sys.mjs"
+);
+
+/**
+ * Test that the measure method correctly collects the disk-sizes of things that
+ * the PreferencesBackupResource is meant to back up.
+ */
+add_task(async function test_measure() {
+ Services.fog.testResetFOG();
+
+ const EXPECTED_PREFERENCES_KILOBYTES_SIZE = 415;
+ const tempDir = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PreferencesBackupResource-measure-test"
+ );
+ const mockFiles = [
+ { path: "prefs.js", sizeInKB: 20 },
+ { path: "xulstore.json", sizeInKB: 1 },
+ { path: "permissions.sqlite", sizeInKB: 100 },
+ { path: "content-prefs.sqlite", sizeInKB: 260 },
+ { path: "containers.json", sizeInKB: 1 },
+ { path: "handlers.json", sizeInKB: 1 },
+ { path: "search.json.mozlz4", sizeInKB: 1 },
+ { path: "user.js", sizeInKB: 2 },
+ { path: ["chrome", "userChrome.css"], sizeInKB: 5 },
+ { path: ["chrome", "userContent.css"], sizeInKB: 5 },
+ { path: ["chrome", "css", "mockStyles.css"], sizeInKB: 5 },
+ ];
+
+ await createTestFiles(tempDir, mockFiles);
+
+ let preferencesBackupResource = new PreferencesBackupResource();
+
+ await preferencesBackupResource.measure(tempDir);
+
+ let measurement = Glean.browserBackup.preferencesSize.testGetValue();
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.preferences_size",
+ measurement,
+ "Glean and telemetry measurements for preferences data should be equal"
+ );
+ Assert.equal(
+ measurement,
+ EXPECTED_PREFERENCES_KILOBYTES_SIZE,
+ "Should have collected the correct glean measurement for preferences files"
+ );
+
+ await maybeRemovePath(tempDir);
+});
+
+/**
+ * Test that the backup method correctly copies items from the profile directory
+ * into the staging directory.
+ */
+add_task(async function test_backup() {
+ let sandbox = sinon.createSandbox();
+
+ let preferencesBackupResource = new PreferencesBackupResource();
+ let sourcePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PreferencesBackupResource-source-test"
+ );
+ let stagingPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PreferencesBackupResource-staging-test"
+ );
+
+ const simpleCopyFiles = [
+ { path: "xulstore.json" },
+ { path: "containers.json" },
+ { path: "handlers.json" },
+ { path: "search.json.mozlz4" },
+ { path: "user.js" },
+ { path: ["chrome", "userChrome.css"] },
+ { path: ["chrome", "userContent.css"] },
+ { path: ["chrome", "childFolder", "someOtherStylesheet.css"] },
+ ];
+ await createTestFiles(sourcePath, simpleCopyFiles);
+
+ // We have no need to test that Sqlite.sys.mjs's backup method is working -
+ // this is something that is tested in Sqlite's own tests. We can just make
+ // sure that it's being called using sinon. Unfortunately, we cannot do the
+ // same thing with IOUtils.copy, as its methods are not stubbable.
+ let fakeConnection = {
+ backup: sandbox.stub().resolves(true),
+ close: sandbox.stub().resolves(true),
+ };
+ sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
+
+ await preferencesBackupResource.backup(stagingPath, sourcePath);
+
+ await assertFilesExist(stagingPath, simpleCopyFiles);
+
+ // Next, we'll make sure that the Sqlite connection had `backup` called on it
+ // with the right arguments.
+ Assert.ok(
+ fakeConnection.backup.calledTwice,
+ "Called backup the expected number of times for all connections"
+ );
+ Assert.ok(
+ fakeConnection.backup.firstCall.calledWith(
+ PathUtils.join(stagingPath, "permissions.sqlite")
+ ),
+ "Called backup on the permissions.sqlite Sqlite connection"
+ );
+ Assert.ok(
+ fakeConnection.backup.secondCall.calledWith(
+ PathUtils.join(stagingPath, "content-prefs.sqlite")
+ ),
+ "Called backup on the content-prefs.sqlite Sqlite connection"
+ );
+
+ // And we'll make sure that preferences were properly written out.
+ Assert.ok(
+ await IOUtils.exists(PathUtils.join(stagingPath, "prefs.js")),
+ "prefs.js should exist in the staging folder"
+ );
+
+ await maybeRemovePath(stagingPath);
+ await maybeRemovePath(sourcePath);
+
+ sandbox.restore();
+});
diff --git a/browser/components/backup/tests/xpcshell/test_createBackup.js b/browser/components/backup/tests/xpcshell/test_createBackup.js
new file mode 100644
index 0000000000..fcace695ef
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_createBackup.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that calling BackupService.createBackup will call backup on each
+ * registered BackupResource, and that each BackupResource will have a folder
+ * created for them to write into.
+ */
+add_task(async function test_createBackup() {
+ let sandbox = sinon.createSandbox();
+ sandbox
+ .stub(FakeBackupResource1.prototype, "backup")
+ .resolves({ fake1: "hello from 1" });
+ sandbox
+ .stub(FakeBackupResource2.prototype, "backup")
+ .rejects(new Error("Some failure to backup"));
+ sandbox
+ .stub(FakeBackupResource3.prototype, "backup")
+ .resolves({ fake3: "hello from 3" });
+
+ let bs = new BackupService({
+ FakeBackupResource1,
+ FakeBackupResource2,
+ FakeBackupResource3,
+ });
+
+ let fakeProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "createBackupTest"
+ );
+
+ await bs.createBackup({ profilePath: fakeProfilePath });
+
+ // For now, we expect a staging folder to exist under the fakeProfilePath,
+ // and we should find a folder for each fake BackupResource.
+ let stagingPath = PathUtils.join(fakeProfilePath, "backups", "staging");
+ Assert.ok(await IOUtils.exists(stagingPath), "Staging folder exists");
+
+ for (let backupResourceClass of [
+ FakeBackupResource1,
+ FakeBackupResource2,
+ FakeBackupResource3,
+ ]) {
+ let expectedResourceFolder = PathUtils.join(
+ stagingPath,
+ backupResourceClass.key
+ );
+ Assert.ok(
+ await IOUtils.exists(expectedResourceFolder),
+ `BackupResource staging folder exists for ${backupResourceClass.key}`
+ );
+ Assert.ok(
+ backupResourceClass.prototype.backup.calledOnce,
+ `Backup was called for ${backupResourceClass.key}`
+ );
+ Assert.ok(
+ backupResourceClass.prototype.backup.calledWith(
+ expectedResourceFolder,
+ fakeProfilePath
+ ),
+ `Backup was passed the right paths for ${backupResourceClass.key}`
+ );
+ }
+
+ // After createBackup is more fleshed out, we're going to want to make sure
+ // that we're writing the manifest file and that it contains the expected
+ // ManifestEntry objects, and that the staging folder was successfully
+ // renamed with the current date.
+ await IOUtils.remove(fakeProfilePath, { recursive: true });
+
+ sandbox.restore();
+});
diff --git a/browser/components/backup/tests/xpcshell/test_measurements.js b/browser/components/backup/tests/xpcshell/test_measurements.js
index e5726126b2..0dece6b370 100644
--- a/browser/components/backup/tests/xpcshell/test_measurements.js
+++ b/browser/components/backup/tests/xpcshell/test_measurements.js
@@ -3,22 +3,59 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
-const { BackupService } = ChromeUtils.importESModule(
- "resource:///modules/backup/BackupService.sys.mjs"
+const { CredentialsAndSecurityBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/CredentialsAndSecurityBackupResource.sys.mjs"
+);
+const { AddonsBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/AddonsBackupResource.sys.mjs"
+);
+const { CookiesBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/CookiesBackupResource.sys.mjs"
+);
+
+const { FormHistoryBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/FormHistoryBackupResource.sys.mjs"
);
-const { TelemetryTestUtils } = ChromeUtils.importESModule(
- "resource://testing-common/TelemetryTestUtils.sys.mjs"
+const { SessionStoreBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/SessionStoreBackupResource.sys.mjs"
);
add_setup(() => {
- do_get_profile();
// FOG needs to be initialized in order for data to flow.
Services.fog.initializeFOG();
Services.telemetry.clearScalars();
});
/**
+ * Tests that calling `BackupService.takeMeasurements` will call the measure
+ * method of all registered BackupResource classes.
+ */
+add_task(async function test_takeMeasurements() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(FakeBackupResource1.prototype, "measure").resolves();
+ sandbox
+ .stub(FakeBackupResource2.prototype, "measure")
+ .rejects(new Error("Some failure to measure"));
+
+ let bs = new BackupService({ FakeBackupResource1, FakeBackupResource2 });
+ await bs.takeMeasurements();
+
+ for (let backupResourceClass of [FakeBackupResource1, FakeBackupResource2]) {
+ Assert.ok(
+ backupResourceClass.prototype.measure.calledOnce,
+ "Measure was called"
+ );
+ Assert.ok(
+ backupResourceClass.prototype.measure.calledWith(PathUtils.profileDir),
+ "Measure was called with the profile directory argument"
+ );
+ }
+
+ sandbox.restore();
+});
+
+/**
* Tests that we can measure the disk space available in the profile directory.
*/
add_task(async function test_profDDiskSpace() {
@@ -38,3 +75,503 @@ add_task(async function test_profDDiskSpace() {
"device"
);
});
+
+/**
+ * Tests that we can measure credentials related files in the profile directory.
+ */
+add_task(async function test_credentialsAndSecurityBackupResource() {
+ Services.fog.testResetFOG();
+
+ const EXPECTED_CREDENTIALS_KILOBYTES_SIZE = 413;
+ const EXPECTED_SECURITY_KILOBYTES_SIZE = 231;
+
+ // Create resource files in temporary directory
+ const tempDir = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "CredentialsAndSecurityBackupResource-measurement-test"
+ );
+
+ const mockFiles = [
+ // Set up credentials files
+ { path: "key4.db", sizeInKB: 300 },
+ { path: "logins.json", sizeInKB: 1 },
+ { path: "logins-backup.json", sizeInKB: 1 },
+ { path: "autofill-profiles.json", sizeInKB: 1 },
+ { path: "credentialstate.sqlite", sizeInKB: 100 },
+ { path: "signedInUser.json", sizeInKB: 5 },
+ // Set up security files
+ { path: "cert9.db", sizeInKB: 230 },
+ { path: "pkcs11.txt", sizeInKB: 1 },
+ ];
+
+ await createTestFiles(tempDir, mockFiles);
+
+ let credentialsAndSecurityBackupResource =
+ new CredentialsAndSecurityBackupResource();
+ await credentialsAndSecurityBackupResource.measure(tempDir);
+
+ let credentialsMeasurement =
+ Glean.browserBackup.credentialsDataSize.testGetValue();
+ let securityMeasurement = Glean.browserBackup.securityDataSize.testGetValue();
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+
+ // Credentials measurements
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.credentials_data_size",
+ credentialsMeasurement,
+ "Glean and telemetry measurements for credentials data should be equal"
+ );
+
+ Assert.equal(
+ credentialsMeasurement,
+ EXPECTED_CREDENTIALS_KILOBYTES_SIZE,
+ "Should have collected the correct glean measurement for credentials files"
+ );
+
+ // Security measurements
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.security_data_size",
+ securityMeasurement,
+ "Glean and telemetry measurements for security data should be equal"
+ );
+ Assert.equal(
+ securityMeasurement,
+ EXPECTED_SECURITY_KILOBYTES_SIZE,
+ "Should have collected the correct glean measurement for security files"
+ );
+
+ // Cleanup
+ await maybeRemovePath(tempDir);
+});
+
+/**
+ * Tests that we can measure the Cookies db in a profile directory.
+ */
+add_task(async function test_cookiesBackupResource() {
+ const EXPECTED_COOKIES_DB_SIZE = 1230;
+
+ Services.fog.testResetFOG();
+
+ // Create resource files in temporary directory
+ let tempDir = PathUtils.tempDir;
+ let tempCookiesDBPath = PathUtils.join(tempDir, "cookies.sqlite");
+ await createKilobyteSizedFile(tempCookiesDBPath, EXPECTED_COOKIES_DB_SIZE);
+
+ let cookiesBackupResource = new CookiesBackupResource();
+ await cookiesBackupResource.measure(tempDir);
+
+ let cookiesMeasurement = Glean.browserBackup.cookiesSize.testGetValue();
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+
+ // Compare glean vs telemetry measurements
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.cookies_size",
+ cookiesMeasurement,
+ "Glean and telemetry measurements for cookies.sqlite should be equal"
+ );
+
+ // Compare glean measurements vs actual file sizes
+ Assert.equal(
+ cookiesMeasurement,
+ EXPECTED_COOKIES_DB_SIZE,
+ "Should have collected the correct glean measurement for cookies.sqlite"
+ );
+
+ await maybeRemovePath(tempCookiesDBPath);
+});
+
+/**
+ * Tests that we can measure the Form History db in a profile directory.
+ */
+add_task(async function test_formHistoryBackupResource() {
+ const EXPECTED_FORM_HISTORY_DB_SIZE = 500;
+
+ Services.fog.testResetFOG();
+
+ // Create resource files in temporary directory
+ let tempDir = PathUtils.tempDir;
+ let tempFormHistoryDBPath = PathUtils.join(tempDir, "formhistory.sqlite");
+ await createKilobyteSizedFile(
+ tempFormHistoryDBPath,
+ EXPECTED_FORM_HISTORY_DB_SIZE
+ );
+
+ let formHistoryBackupResource = new FormHistoryBackupResource();
+ await formHistoryBackupResource.measure(tempDir);
+
+ let formHistoryMeasurement =
+ Glean.browserBackup.formHistorySize.testGetValue();
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+
+ // Compare glean vs telemetry measurements
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.form_history_size",
+ formHistoryMeasurement,
+ "Glean and telemetry measurements for formhistory.sqlite should be equal"
+ );
+
+ // Compare glean measurements vs actual file sizes
+ Assert.equal(
+ formHistoryMeasurement,
+ EXPECTED_FORM_HISTORY_DB_SIZE,
+ "Should have collected the correct glean measurement for formhistory.sqlite"
+ );
+
+ await IOUtils.remove(tempFormHistoryDBPath);
+});
+
+/**
+ * Tests that we can measure the Session Store JSON and backups directory.
+ */
+add_task(async function test_sessionStoreBackupResource() {
+ const EXPECTED_KILOBYTES_FOR_BACKUPS_DIR = 1000;
+ Services.fog.testResetFOG();
+
+ // Create the sessionstore-backups directory.
+ let tempDir = PathUtils.tempDir;
+ let sessionStoreBackupsPath = PathUtils.join(
+ tempDir,
+ "sessionstore-backups",
+ "restore.jsonlz4"
+ );
+ await createKilobyteSizedFile(
+ sessionStoreBackupsPath,
+ EXPECTED_KILOBYTES_FOR_BACKUPS_DIR
+ );
+
+ let sessionStoreBackupResource = new SessionStoreBackupResource();
+ await sessionStoreBackupResource.measure(tempDir);
+
+ let sessionStoreBackupsDirectoryMeasurement =
+ Glean.browserBackup.sessionStoreBackupsDirectorySize.testGetValue();
+ let sessionStoreMeasurement =
+ Glean.browserBackup.sessionStoreSize.testGetValue();
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+
+ // Compare glean vs telemetry measurements
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.session_store_backups_directory_size",
+ sessionStoreBackupsDirectoryMeasurement,
+ "Glean and telemetry measurements for session store backups directory should be equal"
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.session_store_size",
+ sessionStoreMeasurement,
+ "Glean and telemetry measurements for session store should be equal"
+ );
+
+ // Compare glean measurements vs actual file sizes
+ Assert.equal(
+ sessionStoreBackupsDirectoryMeasurement,
+ EXPECTED_KILOBYTES_FOR_BACKUPS_DIR,
+ "Should have collected the correct glean measurement for the sessionstore-backups directory"
+ );
+
+ // Session store measurement is from `getCurrentState`, so exact size is unknown.
+ Assert.greater(
+ sessionStoreMeasurement,
+ 0,
+ "Should have collected a measurement for the session store"
+ );
+
+ await IOUtils.remove(sessionStoreBackupsPath);
+});
+
+/**
+ * Tests that we can measure the size of all the addons & extensions data.
+ */
+add_task(async function test_AddonsBackupResource() {
+ Services.fog.testResetFOG();
+ Services.telemetry.clearScalars();
+
+ const EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON = 250;
+ const EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORE = 500;
+ const EXPECTED_KILOBYTES_FOR_STORAGE_SYNC = 50;
+ const EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_A = 600;
+ const EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_B = 400;
+ const EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C = 150;
+ const EXPECTED_KILOBYTES_FOR_EXTENSIONS_DIRECTORY = 1000;
+ const EXPECTED_KILOBYTES_FOR_EXTENSION_DATA = 100;
+ const EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE = 200;
+
+ let tempDir = PathUtils.tempDir;
+
+ // Create extensions json files (all the same size).
+ const extensionsFilePath = PathUtils.join(tempDir, "extensions.json");
+ await createKilobyteSizedFile(
+ extensionsFilePath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON
+ );
+ const extensionSettingsFilePath = PathUtils.join(
+ tempDir,
+ "extension-settings.json"
+ );
+ await createKilobyteSizedFile(
+ extensionSettingsFilePath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON
+ );
+ const extensionsPrefsFilePath = PathUtils.join(
+ tempDir,
+ "extension-preferences.json"
+ );
+ await createKilobyteSizedFile(
+ extensionsPrefsFilePath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON
+ );
+ const addonStartupFilePath = PathUtils.join(tempDir, "addonStartup.json.lz4");
+ await createKilobyteSizedFile(
+ addonStartupFilePath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON
+ );
+
+ // Create the extension store permissions data file.
+ let extensionStorePermissionsDataSize = PathUtils.join(
+ tempDir,
+ "extension-store-permissions",
+ "data.safe.bin"
+ );
+ await createKilobyteSizedFile(
+ extensionStorePermissionsDataSize,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORE
+ );
+
+ // Create the storage sync database file.
+ let storageSyncPath = PathUtils.join(tempDir, "storage-sync-v2.sqlite");
+ await createKilobyteSizedFile(
+ storageSyncPath,
+ EXPECTED_KILOBYTES_FOR_STORAGE_SYNC
+ );
+
+ // Create the extensions directory with XPI files.
+ let extensionsXpiAPath = PathUtils.join(
+ tempDir,
+ "extensions",
+ "extension-b.xpi"
+ );
+ let extensionsXpiBPath = PathUtils.join(
+ tempDir,
+ "extensions",
+ "extension-a.xpi"
+ );
+ await createKilobyteSizedFile(
+ extensionsXpiAPath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_A
+ );
+ await createKilobyteSizedFile(
+ extensionsXpiBPath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_B
+ );
+ // Should be ignored.
+ let extensionsXpiStagedPath = PathUtils.join(
+ tempDir,
+ "extensions",
+ "staged",
+ "staged-test-extension.xpi"
+ );
+ let extensionsXpiTrashPath = PathUtils.join(
+ tempDir,
+ "extensions",
+ "trash",
+ "trashed-test-extension.xpi"
+ );
+ let extensionsXpiUnpackedPath = PathUtils.join(
+ tempDir,
+ "extensions",
+ "unpacked-extension.xpi",
+ "manifest.json"
+ );
+ await createKilobyteSizedFile(
+ extensionsXpiStagedPath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C
+ );
+ await createKilobyteSizedFile(
+ extensionsXpiTrashPath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C
+ );
+ await createKilobyteSizedFile(
+ extensionsXpiUnpackedPath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C
+ );
+
+ // Create the browser extension data directory.
+ let browserExtensionDataPath = PathUtils.join(
+ tempDir,
+ "browser-extension-data",
+ "test-file"
+ );
+ await createKilobyteSizedFile(
+ browserExtensionDataPath,
+ EXPECTED_KILOBYTES_FOR_EXTENSION_DATA
+ );
+
+ // Create the extensions storage directory.
+ let extensionsStoragePath = PathUtils.join(
+ tempDir,
+ "storage",
+ "default",
+ "moz-extension+++test-extension-id",
+ "idb",
+ "data.sqlite"
+ );
+ // Other storage files that should not be counted.
+ let otherStoragePath = PathUtils.join(
+ tempDir,
+ "storage",
+ "default",
+ "https+++accounts.firefox.com",
+ "ls",
+ "data.sqlite"
+ );
+
+ await createKilobyteSizedFile(
+ extensionsStoragePath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE
+ );
+ await createKilobyteSizedFile(
+ otherStoragePath,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE
+ );
+
+ // Measure all the extensions data.
+ let extensionsBackupResource = new AddonsBackupResource();
+ await extensionsBackupResource.measure(tempDir);
+
+ let extensionsJsonSizeMeasurement =
+ Glean.browserBackup.extensionsJsonSize.testGetValue();
+ Assert.equal(
+ extensionsJsonSizeMeasurement,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON * 4, // There are 4 equally sized files.
+ "Should have collected the correct measurement of the total size of all extensions JSON files"
+ );
+
+ let extensionStorePermissionsDataSizeMeasurement =
+ Glean.browserBackup.extensionStorePermissionsDataSize.testGetValue();
+ Assert.equal(
+ extensionStorePermissionsDataSizeMeasurement,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORE,
+ "Should have collected the correct measurement of the size of the extension store permissions data"
+ );
+
+ let storageSyncSizeMeasurement =
+ Glean.browserBackup.storageSyncSize.testGetValue();
+ Assert.equal(
+ storageSyncSizeMeasurement,
+ EXPECTED_KILOBYTES_FOR_STORAGE_SYNC,
+ "Should have collected the correct measurement of the size of the storage sync database"
+ );
+
+ let extensionsXpiDirectorySizeMeasurement =
+ Glean.browserBackup.extensionsXpiDirectorySize.testGetValue();
+ Assert.equal(
+ extensionsXpiDirectorySizeMeasurement,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_DIRECTORY,
+ "Should have collected the correct measurement of the size 2 equally sized XPI files in the extensions directory"
+ );
+
+ let browserExtensionDataSizeMeasurement =
+ Glean.browserBackup.browserExtensionDataSize.testGetValue();
+ Assert.equal(
+ browserExtensionDataSizeMeasurement,
+ EXPECTED_KILOBYTES_FOR_EXTENSION_DATA,
+ "Should have collected the correct measurement of the size of the browser extension data directory"
+ );
+
+ let extensionsStorageSizeMeasurement =
+ Glean.browserBackup.extensionsStorageSize.testGetValue();
+ Assert.equal(
+ extensionsStorageSizeMeasurement,
+ EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE,
+ "Should have collected the correct measurement of all the extensions storage"
+ );
+
+ // Compare glean vs telemetry measurements
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.extensions_json_size",
+ extensionsJsonSizeMeasurement,
+ "Glean and telemetry measurements for extensions JSON should be equal"
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.extension_store_permissions_data_size",
+ extensionStorePermissionsDataSizeMeasurement,
+ "Glean and telemetry measurements for extension store permissions data should be equal"
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.storage_sync_size",
+ storageSyncSizeMeasurement,
+ "Glean and telemetry measurements for storage sync database should be equal"
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.extensions_xpi_directory_size",
+ extensionsXpiDirectorySizeMeasurement,
+ "Glean and telemetry measurements for extensions directory should be equal"
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.browser_extension_data_size",
+ browserExtensionDataSizeMeasurement,
+ "Glean and telemetry measurements for browser extension data should be equal"
+ );
+ TelemetryTestUtils.assertScalar(
+ scalars,
+ "browser.backup.extensions_storage_size",
+ extensionsStorageSizeMeasurement,
+ "Glean and telemetry measurements for extensions storage should be equal"
+ );
+
+ await maybeRemovePath(tempDir);
+});
+
+/**
+ * Tests that we can handle the extension store permissions data not existing.
+ */
+add_task(
+ async function test_AddonsBackupResource_no_extension_store_permissions_data() {
+ Services.fog.testResetFOG();
+
+ let tempDir = PathUtils.tempDir;
+
+ let extensionsBackupResource = new AddonsBackupResource();
+ await extensionsBackupResource.measure(tempDir);
+
+ let extensionStorePermissionsDataSizeMeasurement =
+ Glean.browserBackup.extensionStorePermissionsDataSize.testGetValue();
+ Assert.equal(
+ extensionStorePermissionsDataSizeMeasurement,
+ null,
+ "Should NOT have collected a measurement for the missing data"
+ );
+ }
+);
+
+/**
+ * Tests that we can handle a profile with no moz-extension IndexedDB databases.
+ */
+add_task(
+ async function test_AddonsBackupResource_no_extension_storage_databases() {
+ Services.fog.testResetFOG();
+
+ let tempDir = PathUtils.tempDir;
+
+ let extensionsBackupResource = new AddonsBackupResource();
+ await extensionsBackupResource.measure(tempDir);
+
+ let extensionsStorageSizeMeasurement =
+ Glean.browserBackup.extensionsStorageSize.testGetValue();
+ Assert.equal(
+ extensionsStorageSizeMeasurement,
+ null,
+ "Should NOT have collected a measurement for the missing data"
+ );
+ }
+);
diff --git a/browser/components/backup/tests/xpcshell/xpcshell.toml b/browser/components/backup/tests/xpcshell/xpcshell.toml
index fb6dcd6846..07e517f1f2 100644
--- a/browser/components/backup/tests/xpcshell/xpcshell.toml
+++ b/browser/components/backup/tests/xpcshell/xpcshell.toml
@@ -1,8 +1,20 @@
[DEFAULT]
+head = "head.js"
firefox-appdir = "browser"
skip-if = ["os == 'android'"]
+prefs = [
+ "browser.backup.log=true",
+]
-["test_BrowserResource.js"]
+["test_BackupResource.js"]
support-files = ["data/test_xulstore.json"]
+["test_MiscDataBackupResource.js"]
+
+["test_PlacesBackupResource.js"]
+
+["test_PreferencesBackupResource.js"]
+
+["test_createBackup.js"]
+
["test_measurements.js"]