Bug 1884995 - Add BackupResource abstract class. r=mconley,backup-reviewers

Adds a `BackupResource` abstract class to be extended by more specific resource handlers and a `BackupResources` module which resources can be registered with.

The BackupResource base includes helpers to get the size of files and directories.

All registed resources will be provided to the `BackupService` constructor for it instantiate them.

Differential Revision: https://phabricator.services.mozilla.com/D203795
This commit is contained in:
Fred Chasen 2024-03-13 19:08:05 +00:00
parent b3eba1dc18
commit ae5f491626
7 changed files with 236 additions and 2 deletions

View File

@ -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 {};

View File

@ -2,6 +2,8 @@
* 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 () {
@ -27,6 +29,13 @@ export class BackupService {
*/
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
@ -39,14 +48,24 @@ export class BackupService {
if (this.#instance) {
return this.#instance;
}
this.#instance = new BackupService();
this.#instance = new BackupService(BackupResources);
this.#instance.takeMeasurements();
return this.#instance;
}
constructor() {
/**
* 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);
}
}
/**
@ -75,5 +94,10 @@ export class BackupService {
// 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);
}
}
}

View File

@ -12,5 +12,7 @@ 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",
]

View File

@ -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.");
}
}

View File

@ -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" }
}
}

View File

@ -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 });
});

View File

@ -2,4 +2,7 @@
firefox-appdir = "browser"
skip-if = ["os == 'android'"]
["test_BrowserResource.js"]
support-files = ["data/test_xulstore.json"]
["test_measurements.js"]