From def92d1b8e9d373e2f6f27c366d578d97d8960c6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 15 May 2024 05:34:50 +0200 Subject: Merging upstream version 126.0. Signed-off-by: Daniel Baumann --- browser/components/backup/BackupService.sys.mjs | 129 ++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 6 deletions(-) (limited to 'browser/components/backup/BackupService.sys.mjs') 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 = {}; @@ -36,6 +36,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 @@ -48,26 +55,129 @@ 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} + */ + 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. * @@ -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 + ); + } } } } -- cgit v1.2.3