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.mjs15
-rw-r--r--browser/components/backup/BackupService.sys.mjs103
-rw-r--r--browser/components/backup/docs/backup-service.rst8
-rw-r--r--browser/components/backup/docs/index.rst13
-rw-r--r--browser/components/backup/metrics.yaml30
-rw-r--r--browser/components/backup/moz.build18
-rw-r--r--browser/components/backup/resources/BackupResource.sys.mjs109
-rw-r--r--browser/components/backup/tests/xpcshell/data/test_xulstore.json18
-rw-r--r--browser/components/backup/tests/xpcshell/test_BrowserResource.js63
-rw-r--r--browser/components/backup/tests/xpcshell/test_measurements.js40
-rw-r--r--browser/components/backup/tests/xpcshell/xpcshell.toml8
12 files changed, 434 insertions, 0 deletions
diff --git a/browser/components/backup/.eslintrc.js b/browser/components/backup/.eslintrc.js
new file mode 100644
index 0000000000..9aafb4a214
--- /dev/null
+++ b/browser/components/backup/.eslintrc.js
@@ -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/. */
+
+"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
new file mode 100644
index 0000000000..276fabefdf
--- /dev/null
+++ b/browser/components/backup/BackupResources.sys.mjs
@@ -0,0 +1,15 @@
+/* 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/. */
+
+// 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 {};
diff --git a/browser/components/backup/BackupService.sys.mjs b/browser/components/backup/BackupService.sys.mjs
new file mode 100644
index 0000000000..853f4768ce
--- /dev/null
+++ b/browser/components/backup/BackupService.sys.mjs
@@ -0,0 +1,103 @@
+/* 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/. */
+
+import * as BackupResources from "resource:///modules/backup/BackupResources.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
+ return console.createInstance({
+ prefix: "BackupService",
+ maxLogLevel: Services.prefs.getBoolPref("browser.backup.log", false)
+ ? "Debug"
+ : "Warn",
+ });
+});
+
+/**
+ * The BackupService class orchestrates the scheduling and creation of profile
+ * backups. It also does most of the heavy lifting for the restoration of a
+ * profile backup.
+ */
+export class BackupService {
+ /**
+ * The BackupService singleton instance.
+ *
+ * @static
+ * @type {BackupService|null}
+ */
+ static #instance = null;
+
+ /**
+ * Map of instantiated BackupResource classes.
+ *
+ * @type {Map<string, BackupResource>}
+ */
+ #resources = new Map();
+
+ /**
+ * 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.
+ *
+ * @static
+ * @type {BackupService}
+ */
+ static init() {
+ if (this.#instance) {
+ return this.#instance;
+ }
+ this.#instance = new BackupService(BackupResources);
+ this.#instance.takeMeasurements();
+
+ return this.#instance;
+ }
+
+ /**
+ * Create a BackupService instance.
+ *
+ * @param {object} [backupResources=BackupResources] - Object containing BackupResource classes to associate with this service.
+ */
+ constructor(backupResources = BackupResources) {
+ lazy.logConsole.debug("Instantiated");
+
+ for (const resourceName in backupResources) {
+ let resource = BackupResources[resourceName];
+ this.#resources.set(resource.key, resource);
+ }
+ }
+
+ /**
+ * Take measurements of the current profile state for Telemetry.
+ *
+ * @returns {Promise<undefined>}
+ */
+ async takeMeasurements() {
+ lazy.logConsole.debug("Taking Telemetry measurements");
+
+ // Note: We're talking about kilobytes here, not kibibytes. That means
+ // 1000 bytes, and not 1024 bytes.
+ const BYTES_IN_KB = 1000;
+ const BYTES_IN_MB = 1000000;
+
+ // We'll start by measuring the available disk space on the storage
+ // device that the profile directory is on.
+ let profileDir = await IOUtils.getFile(PathUtils.profileDir);
+
+ let profDDiskSpaceBytes = profileDir.diskSpaceAvailable;
+
+ // Make the measurement fuzzier by rounding to the nearest 10MB.
+ let profDDiskSpaceMB =
+ Math.round(profDDiskSpaceBytes / BYTES_IN_MB / 100) * 100;
+
+ // And then record the value in kilobytes, since that's what everything
+ // else is going to be measured in.
+ Glean.browserBackup.profDDiskSpace.set(profDDiskSpaceMB * BYTES_IN_KB);
+
+ // Measure the size of each file we are going to backup.
+ for (let resourceClass of this.#resources.values()) {
+ await new resourceClass().measure(PathUtils.profileDir);
+ }
+ }
+}
diff --git a/browser/components/backup/docs/backup-service.rst b/browser/components/backup/docs/backup-service.rst
new file mode 100644
index 0000000000..fe363b46b4
--- /dev/null
+++ b/browser/components/backup/docs/backup-service.rst
@@ -0,0 +1,8 @@
+========================
+Backup Service Reference
+========================
+
+.. js::autoattribute:: BackupService#instance
+.. js:autoclass:: BackupService
+ :members:
+ :private-members:
diff --git a/browser/components/backup/docs/index.rst b/browser/components/backup/docs/index.rst
new file mode 100644
index 0000000000..1e201f8f1c
--- /dev/null
+++ b/browser/components/backup/docs/index.rst
@@ -0,0 +1,13 @@
+.. _components/backup:
+
+================
+Backup Component
+================
+
+The Backup Component is responsible for creating backups of user profile data
+into a single file that can be easily restored from.
+
+.. toctree::
+ :maxdepth: 3
+
+ backup-service
diff --git a/browser/components/backup/metrics.yaml b/browser/components/backup/metrics.yaml
new file mode 100644
index 0000000000..6d6a16a178
--- /dev/null
+++ b/browser/components/backup/metrics.yaml
@@ -0,0 +1,30 @@
+# 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/.
+
+# Adding a new metric? We have docs for that!
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Firefox :: Profiles'
+
+browser.backup:
+ prof_d_disk_space:
+ type: quantity
+ unit: kilobyte
+ description: >
+ The total disk space available on the storage device that the profile
+ directory is stored on. To reduce fingerprintability, we round to the
+ nearest 10 megabytes and return the result in kilobytes.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1884407
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1884407
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - mconley@mozilla.com
+ expires: never
+ telemetry_mirror: BROWSER_BACKUP_PROF_D_DISK_SPACE
diff --git a/browser/components/backup/moz.build b/browser/components/backup/moz.build
new file mode 100644
index 0000000000..0ea7d66b7d
--- /dev/null
+++ b/browser/components/backup/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Profiles")
+
+SPHINX_TREES["docs"] = "docs"
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
+
+EXTRA_JS_MODULES.backup += [
+ "BackupResources.sys.mjs",
+ "BackupService.sys.mjs",
+ "resources/BackupResource.sys.mjs",
+]
diff --git a/browser/components/backup/resources/BackupResource.sys.mjs b/browser/components/backup/resources/BackupResource.sys.mjs
new file mode 100644
index 0000000000..bde3f0669c
--- /dev/null
+++ b/browser/components/backup/resources/BackupResource.sys.mjs
@@ -0,0 +1,109 @@
+/* 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/. */
+
+// Convert from bytes to kilobytes (not kibibytes).
+const BYTES_IN_KB = 1000;
+
+/**
+ * An abstract class representing a set of data within a user profile
+ * that can be persisted to a separate backup archive file, and restored
+ * to a new user profile from that backup archive file.
+ */
+export class BackupResource {
+ /**
+ * This must be overridden to return a simple string identifier for the
+ * resource, for example "places" or "extensions". This key is used as
+ * a unique identifier for the resource.
+ *
+ * @type {string}
+ */
+ static get key() {
+ throw new Error("BackupResource::key needs to be overridden.");
+ }
+
+ /**
+ * Get the size of a file.
+ *
+ * @param {string} filePath - path to a file.
+ * @returns {Promise<number|null>} - the size of the file in kilobytes, or null if the
+ * file does not exist, the path is a directory or the size is unknown.
+ */
+ static async getFileSize(filePath) {
+ if (!(await IOUtils.exists(filePath))) {
+ return null;
+ }
+
+ let { size } = await IOUtils.stat(filePath);
+
+ if (size < 0) {
+ 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;
+
+ return Math.max(nearestTenthKb, 1);
+ }
+
+ /**
+ * Get the total size of a directory.
+ *
+ * @param {string} directoryPath - path to a directory.
+ * @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) {
+ if (!(await IOUtils.exists(directoryPath))) {
+ return null;
+ }
+
+ let { type } = await IOUtils.stat(directoryPath);
+
+ if (type != "directory") {
+ return null;
+ }
+
+ let children = await IOUtils.getChildren(directoryPath, {
+ ignoreAbsent: true,
+ });
+
+ let size = 0;
+ for (const childFilePath of children) {
+ let { size: childSize, type: childType } = await IOUtils.stat(
+ childFilePath
+ );
+
+ 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);
+ }
+
+ if (childType == "directory") {
+ let childDirectorySize = await this.getDirectorySize(childFilePath);
+ if (Number.isInteger(childDirectorySize)) {
+ size += childDirectorySize;
+ }
+ }
+ }
+
+ return size;
+ }
+
+ constructor() {}
+
+ /**
+ * This must be overridden to record telemetry on the size of any
+ * data associated with this BackupResource.
+ *
+ * @param {string} profilePath - path to a profile directory.
+ * @returns {Promise<undefined>}
+ */
+ // eslint-disable-next-line no-unused-vars
+ async measure(profilePath) {
+ throw new Error("BackupResource::measure needs to be overridden.");
+ }
+}
diff --git a/browser/components/backup/tests/xpcshell/data/test_xulstore.json b/browser/components/backup/tests/xpcshell/data/test_xulstore.json
new file mode 100644
index 0000000000..0d0890ab16
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/data/test_xulstore.json
@@ -0,0 +1,18 @@
+{
+ "chrome://browser/content/browser.xhtml": {
+ "PersonalToolbar": { "collapsed": "false" },
+ "main-window": {
+ "screenX": "852",
+ "screenY": "125",
+ "width": "1484",
+ "height": "1256",
+ "sizemode": "normal"
+ },
+ "sidebar-box": {
+ "sidebarcommand": "viewBookmarksSidebar",
+ "width": "323",
+ "style": "width: 323px;"
+ },
+ "sidebar-title": { "value": "Bookmarks" }
+ }
+}
diff --git a/browser/components/backup/tests/xpcshell/test_BrowserResource.js b/browser/components/backup/tests/xpcshell/test_BrowserResource.js
new file mode 100644
index 0000000000..23c8e077a5
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_BrowserResource.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { BackupResource } = 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.
+ */
+add_task(async function test_getFileSize() {
+ let file = do_get_file("data/test_xulstore.json");
+
+ let testFilePath = PathUtils.join(PathUtils.profileDir, "test_xulstore.json");
+
+ await IOUtils.copy(file.path, PathUtils.profileDir);
+
+ let size = await BackupResource.getFileSize(testFilePath);
+
+ Assert.equal(
+ size,
+ EXPECTED_KILOBYTES_FOR_XULSTORE,
+ "Size of the test_xulstore.json is rounded up to the nearest kilobyte."
+ );
+
+ await IOUtils.remove(testFilePath);
+});
+
+/**
+ * Tests that BackupService.getFileSize 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");
+
+ // Create a test directory with the test json file in it.
+ let testDir = PathUtils.join(PathUtils.profileDir, "testDir");
+ await IOUtils.makeDirectory(testDir);
+ await IOUtils.copy(file.path, testDir);
+
+ // Create another test directory inside of that one.
+ let nestedTestDir = PathUtils.join(testDir, "testDir");
+ await IOUtils.makeDirectory(nestedTestDir);
+ await IOUtils.copy(file.path, nestedTestDir);
+
+ let size = await BackupResource.getDirectorySize(testDir);
+
+ Assert.equal(
+ size,
+ EXPECTED_KILOBYTES_FOR_XULSTORE * 2,
+ `Total size of the directory is rounded up to the nearest kilobyte
+ and is equal to twice the size of the test_xulstore.json file`
+ );
+
+ await IOUtils.remove(testDir, { recursive: true });
+});
diff --git a/browser/components/backup/tests/xpcshell/test_measurements.js b/browser/components/backup/tests/xpcshell/test_measurements.js
new file mode 100644
index 0000000000..e5726126b2
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_measurements.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { BackupService } = ChromeUtils.importESModule(
+ "resource:///modules/backup/BackupService.sys.mjs"
+);
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.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 we can measure the disk space available in the profile directory.
+ */
+add_task(async function test_profDDiskSpace() {
+ let bs = new BackupService();
+ await bs.takeMeasurements();
+ let measurement = Glean.browserBackup.profDDiskSpace.testGetValue();
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent", false, true),
+ "browser.backup.prof_d_disk_space",
+ measurement
+ );
+
+ Assert.greater(
+ measurement,
+ 0,
+ "Should have collected a measurement for the profile directory storage " +
+ "device"
+ );
+});
diff --git a/browser/components/backup/tests/xpcshell/xpcshell.toml b/browser/components/backup/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..fb6dcd6846
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,8 @@
+[DEFAULT]
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+
+["test_BrowserResource.js"]
+support-files = ["data/test_xulstore.json"]
+
+["test_measurements.js"]