Files
tubestation/browser/components/backup/resources/BackupResource.sys.mjs

256 lines
8.1 KiB
JavaScript

/* 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/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
});
// Convert from bytes to kilobytes (not kibibytes).
export const BYTES_IN_KB = 1000;
/**
* Convert bytes to the nearest 10th kilobyte to make the measurements fuzzier.
*
* @param {number} bytes - size in bytes.
* @returns {number} - size in kilobytes rounded to the nearest 10th kilobyte.
*/
export function bytesToFuzzyKilobytes(bytes) {
let sizeInKb = Math.ceil(bytes / BYTES_IN_KB);
let nearestTenthKb = Math.round(sizeInKb / 10) * 10;
return Math.max(nearestTenthKb, 1);
}
/**
* 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.");
}
/**
* This must be overridden to return a boolean indicating whether the
* resource requires encryption when being backed up. Encryption should be
* required for particularly sensitive data, such as passwords / credentials,
* cookies, or payment methods. If you're not sure, talk to someone from the
* Privacy team.
*
* @type {boolean}
*/
static get requiresEncryption() {
throw new Error(
"BackupResource::requiresEncryption 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 nearestTenthKb = bytesToFuzzyKilobytes(size);
return nearestTenthKb;
}
/**
* Get the total size of a directory.
*
* @param {string} directoryPath - path to a directory.
* @param {object} options - A set of additional optional parameters.
* @param {Function} [options.shouldExclude] - an optional callback which based on file path and file type should return true
* if the file should be excluded from the computed directory size.
* @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,
{ shouldExclude = () => false } = {}
) {
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 (shouldExclude(childFilePath, childType, directoryPath)) {
continue;
}
if (childSize >= 0) {
let nearestTenthKb = bytesToFuzzyKilobytes(childSize);
size += nearestTenthKb;
}
if (childType == "directory") {
let childDirectorySize = await this.getDirectorySize(childFilePath, {
shouldExclude,
});
if (Number.isInteger(childDirectorySize)) {
size += childDirectorySize;
}
}
}
return size;
}
/**
* Copy a set of SQLite databases safely from a source directory to a
* destination directory. A new read-only connection is opened for each
* database, and then a backup is created.
*
* @param {string} sourcePath
* Path to the source directory of the SQLite databases.
* @param {string} destPath
* Path to the destination directory where the SQLite databases should be
* copied to.
* @param {Array<string>} sqliteDatabases
* An array of filenames of the SQLite databases to copy.
* @returns {Promise<undefined>}
*/
static async copySqliteDatabases(sourcePath, destPath, sqliteDatabases) {
for (let fileName of sqliteDatabases) {
let sourceFilePath = PathUtils.join(sourcePath, fileName);
let destFilePath = PathUtils.join(destPath, fileName);
let connection;
try {
connection = await lazy.Sqlite.openConnection({
path: sourceFilePath,
readOnly: true,
});
await connection.backup(
destFilePath,
BackupResource.SQLITE_PAGES_PER_STEP,
BackupResource.SQLITE_STEP_DELAY_MS
);
} finally {
await connection?.close();
}
}
}
/**
* A helper function to copy a set of files from a source directory to a
* destination directory. Callers should ensure that the source files can be
* copied safely before invoking this function. Files that do not exist will
* be ignored. Callers that wish to copy SQLite databases should use
* copySqliteDatabases() instead.
*
* @param {string} sourcePath
* Path to the source directory of the files to be copied.
* @param {string} destPath
* Path to the destination directory where the files should be
* copied to.
* @param {string[]} fileNames
* An array of filenames of the files to copy.
* @returns {Promise<undefined>}
*/
static async copyFiles(sourcePath, destPath, fileNames) {
for (let fileName of fileNames) {
let sourceFilePath = PathUtils.join(sourcePath, fileName);
let destFilePath = PathUtils.join(destPath, fileName);
if (await IOUtils.exists(sourceFilePath)) {
await IOUtils.copy(sourceFilePath, destFilePath, { recursive: true });
}
}
}
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.");
}
/**
* Perform a safe copy of the resource(s) and write them into the backup
* database. The Promise should resolve with an object that can be serialized
* to JSON, as it will be written to the manifest file. This same object will
* be deserialized and passed to restore() when restoring the backup. This
* object can be null if no additional information is needed to restore the
* backup.
*
* @param {string} stagingPath
* The path to the staging folder where copies of the datastores for this
* BackupResource should be written to.
* @param {string} [profilePath=null]
* This is null if the backup is being run on the currently running user
* profile. If, however, the backup is being run on a different user profile
* (for example, it's being run from a BackgroundTask on a user profile that
* just shut down, or during test), then this is a string set to that user
* profile path.
*
* @returns {Promise<object|null>}
*/
// eslint-disable-next-line no-unused-vars
async backup(stagingPath, profilePath = null) {
throw new Error("BackupResource::backup must be overridden");
}
}
XPCOMUtils.defineLazyPreferenceGetter(
BackupResource,
"SQLITE_PAGES_PER_STEP",
"browser.backup.sqlite.pages_per_step",
5
);
XPCOMUtils.defineLazyPreferenceGetter(
BackupResource,
"SQLITE_STEP_DELAY_MS",
"browser.backup.sqlite.step_delay_ms",
250
);