diff options
Diffstat (limited to 'browser/components/backup')
-rw-r--r-- | browser/components/backup/.eslintrc.js | 9 | ||||
-rw-r--r-- | browser/components/backup/BackupResources.sys.mjs | 15 | ||||
-rw-r--r-- | browser/components/backup/BackupService.sys.mjs | 103 | ||||
-rw-r--r-- | browser/components/backup/docs/backup-service.rst | 8 | ||||
-rw-r--r-- | browser/components/backup/docs/index.rst | 13 | ||||
-rw-r--r-- | browser/components/backup/metrics.yaml | 30 | ||||
-rw-r--r-- | browser/components/backup/moz.build | 18 | ||||
-rw-r--r-- | browser/components/backup/resources/BackupResource.sys.mjs | 109 | ||||
-rw-r--r-- | browser/components/backup/tests/xpcshell/data/test_xulstore.json | 18 | ||||
-rw-r--r-- | browser/components/backup/tests/xpcshell/test_BrowserResource.js | 63 | ||||
-rw-r--r-- | browser/components/backup/tests/xpcshell/test_measurements.js | 40 | ||||
-rw-r--r-- | browser/components/backup/tests/xpcshell/xpcshell.toml | 8 |
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"] |