/* 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 DefaultBackupResources 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} */ #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. * * @static * @type {BackupService} */ static init() { if (this.#instance) { return this.#instance; } 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=DefaultBackupResources] - Object containing BackupResource classes to associate with this service. */ constructor(backupResources = DefaultBackupResources) { lazy.logConsole.debug("Instantiated"); for (const resourceName in backupResources) { 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} */ 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} * 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} */ 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()) { try { await new resourceClass().measure(PathUtils.profileDir); } catch (e) { lazy.logConsole.error( `Failed to measure for resource: ${resourceClass.key}`, e ); } } } }