BackupResource.sys.mjs (13855B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 Sqlite: "resource://gre/modules/Sqlite.sys.mjs", 11 BackupError: "resource:///modules/backup/BackupError.mjs", 12 ERRORS: "chrome://browser/content/backup/backup-constants.mjs", 13 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 14 }); 15 16 XPCOMUtils.defineLazyPreferenceGetter( 17 lazy, 18 "isBrowsingHistoryEnabled", 19 "places.history.enabled", 20 true 21 ); 22 23 XPCOMUtils.defineLazyPreferenceGetter( 24 lazy, 25 "isSanitizeOnShutdownEnabled", 26 "privacy.sanitize.sanitizeOnShutdown", 27 false 28 ); 29 30 XPCOMUtils.defineLazyPreferenceGetter( 31 lazy, 32 "isHistoryClearedOnShutdown2", 33 "privacy.clearOnShutdown_v2.browsingHistoryAndDownloads", 34 false 35 ); 36 37 XPCOMUtils.defineLazyPreferenceGetter( 38 lazy, 39 "useOldClearHistoryDialog", 40 "privacy.sanitize.useOldClearHistoryDialog", 41 false 42 ); 43 44 XPCOMUtils.defineLazyPreferenceGetter( 45 lazy, 46 "isHistoryClearedOnShutdown", 47 "privacy.clearOnShutdown.history", 48 false 49 ); 50 51 // Convert from bytes to kilobytes (not kibibytes). 52 export const BYTES_IN_KB = 1000; 53 54 /** 55 * Convert bytes to the nearest multiple of 10 kilobytes to make the measurements fuzzier. 56 * Returns 1 if size is < 5 kB. 57 * 58 * @param {number} bytes - size in bytes. 59 * @returns {number} - size in kilobytes, rounded to the nearest multiple of 10 60 */ 61 export function bytesToFuzzyKilobytes(bytes) { 62 let sizeInKb = Math.ceil(bytes / BYTES_IN_KB); 63 let nearestTenKb = Math.round(sizeInKb / 10) * 10; 64 return Math.max(nearestTenKb, 1); 65 } 66 67 /** 68 * An abstract class representing a set of data within a user profile 69 * that can be persisted to a separate backup archive file, and restored 70 * to a new user profile from that backup archive file. 71 */ 72 export class BackupResource { 73 /** 74 * This must be overridden to return a simple string identifier for the 75 * resource, for example "places" or "extensions". This key is used as 76 * a unique identifier for the resource. 77 * 78 * @type {string} 79 */ 80 static get key() { 81 throw new lazy.BackupError( 82 "BackupResource::key needs to be overridden.", 83 lazy.ERRORS.INTERNAL_ERROR 84 ); 85 } 86 87 /** 88 * This must be overridden to return a boolean indicating whether the 89 * resource requires encryption when being backed up. Encryption should be 90 * required for particularly sensitive data, such as passwords / credentials, 91 * cookies, or payment methods. If you're not sure, talk to someone from the 92 * Privacy team. 93 * 94 * @type {boolean} 95 */ 96 static get requiresEncryption() { 97 throw new lazy.BackupError( 98 "BackupResource::requiresEncryption needs to be overridden.", 99 lazy.ERRORS.INTERNAL_ERROR 100 ); 101 } 102 103 /** 104 * This can be overridden to return a number indicating the priority the 105 * resource should have in the backup order. 106 * 107 * Resources with a higher priority will be backed up first. 108 * The default priority of 0 indicates it can be processed in any order. 109 * 110 * @returns {number} 111 */ 112 static get priority() { 113 return 0; 114 } 115 116 /** 117 * Get the size of a file. 118 * 119 * @param {string} filePath - path to a file. 120 * @returns {Promise<number|null>} - the size of the file in kilobytes, or null if the 121 * file does not exist, the path is a directory or the size is unknown. 122 */ 123 static async getFileSize(filePath) { 124 if (!(await IOUtils.exists(filePath))) { 125 return null; 126 } 127 128 let { size } = await IOUtils.stat(filePath); 129 130 if (size < 0) { 131 return null; 132 } 133 134 let nearestTenKb = bytesToFuzzyKilobytes(size); 135 136 return nearestTenKb; 137 } 138 139 /** 140 * Get the total size of a directory. 141 * 142 * @param {string} directoryPath - path to a directory. 143 * @param {object} options - A set of additional optional parameters. 144 * @param {Function} [options.shouldExclude] - an optional callback which based on file path and file type should return true 145 * if the file should be excluded from the computed directory size. 146 * @returns {Promise<number|null>} - the size of all descendants of the directory in kilobytes, or null if the 147 * directory does not exist, the path is not a directory or the size is unknown. 148 */ 149 static async getDirectorySize( 150 directoryPath, 151 { shouldExclude = () => false } = {} 152 ) { 153 if (!(await IOUtils.exists(directoryPath))) { 154 return null; 155 } 156 157 let { type } = await IOUtils.stat(directoryPath); 158 159 if (type != "directory") { 160 return null; 161 } 162 163 let children = await IOUtils.getChildren(directoryPath, { 164 ignoreAbsent: true, 165 }); 166 167 let size = 0; 168 for (const childFilePath of children) { 169 let { size: childSize, type: childType } = 170 await IOUtils.stat(childFilePath); 171 172 if (shouldExclude(childFilePath, childType, directoryPath)) { 173 continue; 174 } 175 176 if (childSize >= 0) { 177 let nearestTenKb = bytesToFuzzyKilobytes(childSize); 178 179 size += nearestTenKb; 180 } 181 182 if (childType == "directory") { 183 let childDirectorySize = await this.getDirectorySize(childFilePath, { 184 shouldExclude, 185 }); 186 if (Number.isInteger(childDirectorySize)) { 187 size += childDirectorySize; 188 } 189 } 190 } 191 192 return size; 193 } 194 195 /** 196 * Copy a set of SQLite databases safely from a source directory to a 197 * destination directory. A new read-only connection is opened for each 198 * database, and then a backup is created. If the source database does not 199 * exist, it is ignored. 200 * 201 * @param {string} sourcePath 202 * Path to the source directory of the SQLite databases. 203 * @param {string} destPath 204 * Path to the destination directory where the SQLite databases should be 205 * copied to. 206 * @param {Array<string>} sqliteDatabases 207 * An array of filenames of the SQLite databases to copy. 208 * @returns {Promise<undefined>} 209 */ 210 static async copySqliteDatabases(sourcePath, destPath, sqliteDatabases) { 211 for (let fileName of sqliteDatabases) { 212 let sourceFilePath = PathUtils.join(sourcePath, fileName); 213 214 if (!(await IOUtils.exists(sourceFilePath))) { 215 continue; 216 } 217 218 let destFilePath = PathUtils.join(destPath, fileName); 219 let connection; 220 221 try { 222 connection = await lazy.Sqlite.openConnection({ 223 path: sourceFilePath, 224 readOnly: true, 225 }); 226 227 await connection.backup( 228 destFilePath, 229 BackupResource.SQLITE_PAGES_PER_STEP, 230 BackupResource.SQLITE_STEP_DELAY_MS 231 ); 232 } finally { 233 await connection?.close(); 234 } 235 } 236 } 237 238 /** 239 * A helper function to copy a set of files from a source directory to a 240 * destination directory. Callers should ensure that the source files can be 241 * copied safely before invoking this function. Files that do not exist will 242 * be ignored. Callers that wish to copy SQLite databases should use 243 * copySqliteDatabases() instead. 244 * 245 * @param {string} sourcePath 246 * Path to the source directory of the files to be copied. 247 * @param {string} destPath 248 * Path to the destination directory where the files should be 249 * copied to. 250 * @param {string[]} fileNames 251 * An array of filenames of the files to copy. 252 * @returns {Promise<undefined>} 253 */ 254 static async copyFiles(sourcePath, destPath, fileNames) { 255 for (let fileName of fileNames) { 256 let sourceFilePath = PathUtils.join(sourcePath, fileName); 257 let destFilePath = PathUtils.join(destPath, fileName); 258 if (await IOUtils.exists(sourceFilePath)) { 259 await IOUtils.copy(sourceFilePath, destFilePath, { recursive: true }); 260 } 261 } 262 } 263 264 /** 265 * Returns true if the browser is configured in such a way that backing up 266 * things related to browsing history is allowed. Otherwise, returns false. 267 * 268 * @returns {boolean} 269 */ 270 271 /** 272 * Returns true if the resource is enabled for backup based on different 273 * browser preferences and configurations. Otherwise, returns false. 274 * 275 * @returns {boolean} 276 */ 277 static get canBackupResource() { 278 // This is meant to be overridden if a resource requires checks; default is true. 279 return true; 280 } 281 282 /** 283 * Helper function to see if we are going to be backing up and restoring places.sqlite 284 * 285 * @returns {boolean} 286 */ 287 static get backingUpPlaces() { 288 if ( 289 lazy.PrivateBrowsingUtils.permanentPrivateBrowsing || 290 !lazy.isBrowsingHistoryEnabled 291 ) { 292 return false; 293 } 294 295 if (!lazy.isSanitizeOnShutdownEnabled) { 296 return true; 297 } 298 299 if (!lazy.useOldClearHistoryDialog) { 300 if (lazy.isHistoryClearedOnShutdown2) { 301 return false; 302 } 303 } else if (lazy.isHistoryClearedOnShutdown) { 304 return false; 305 } 306 307 return true; 308 } 309 310 constructor() {} 311 312 /** 313 * This must be overridden to record telemetry on the size of any 314 * data associated with this BackupResource. 315 * 316 * @param {string} profilePath - path to a profile directory. 317 * @returns {Promise<undefined>} 318 */ 319 // eslint-disable-next-line no-unused-vars 320 async measure(profilePath) { 321 throw new lazy.BackupError( 322 "BackupResource::measure needs to be overridden.", 323 lazy.ERRORS.INTERNAL_ERROR 324 ); 325 } 326 327 /** 328 * Perform a safe copy of the datastores that this resource manages and write 329 * them into the backup database. The Promise should resolve with an object 330 * that can be serialized to JSON, as it will be written to the manifest file. 331 * This same object will be deserialized and passed to restore() when 332 * restoring the backup. This object can be null if no additional information 333 * is needed to restore the backup. 334 * 335 * @param {string} stagingPath 336 * The path to the staging folder where copies of the datastores for this 337 * BackupResource should be written to. 338 * @param {string} [profilePath=null] 339 * This is null if the backup is being run on the currently running user 340 * profile. If, however, the backup is being run on a different user profile 341 * (for example, it's being run from a BackgroundTask on a user profile that 342 * just shut down, or during test), then this is a string set to that user 343 * profile path. 344 * @param {boolean} [isEncrypting=false] 345 * True if the backup is being encrypted. A BackupResource may not require 346 * encryption, but might still choose to behave differently when encrypting, 347 * so this flag can be used to support that kind of behaviour. 348 * 349 * @returns {Promise<object|null>} 350 */ 351 // eslint-disable-next-line no-unused-vars 352 async backup(stagingPath, profilePath = null, isEncrypting = false) { 353 throw new lazy.BackupError( 354 "BackupResource::backup must be overridden", 355 lazy.ERRORS.INTERNAL_ERROR 356 ); 357 } 358 359 /** 360 * Recovers the datastores that this resource manages from a backup archive 361 * that has been decompressed into the recoveryPath. A pre-existing unlocked 362 * user profile should be available to restore into, and destProfilePath 363 * should point at its location on the file system. 364 * 365 * This method is not expected to be running in an app connected to the 366 * destProfilePath. If the BackupResource needs to run some operations 367 * while attached to the recovery profile, it should do that work inside of 368 * postRecovery(). If data needs to be transferred to postRecovery(), it 369 * should be passed as a JSON serializable object in the return value of this 370 * method. 371 * 372 * @see BackupResource.postRecovery() 373 * @param {object|null} manifestEntry 374 * The object that was returned by the backup() method when the backup was 375 * created. This object can be null if no additional information was needed 376 * for recovery. 377 * @param {string} recoveryPath 378 * The path to the resource directory where the backup archive has been 379 * decompressed. 380 * @param {string} destProfilePath 381 * The path to the profile directory where the backup should be restored to. 382 * @returns {Promise<object|null>} 383 * This should return a JSON serializable object that will be passed to 384 * postRecovery() if any data needs to be passed to it. This object can be 385 * null if no additional information is needed for postRecovery(). 386 */ 387 // eslint-disable-next-line no-unused-vars 388 async recover(manifestEntry, recoveryPath, destProfilePath) { 389 throw new lazy.BackupError( 390 "BackupResource::recover must be overridden", 391 lazy.ERRORS.INTERNAL_ERROR 392 ); 393 } 394 395 /** 396 * Perform any post-recovery operations that need to be done after the 397 * recovery has been completed and the recovered profile has been attached 398 * to. 399 * 400 * This method is running in an app connected to the recovered profile. The 401 * profile is locked, but this postRecovery method can be used to insert 402 * data into connected datastores, or perform any other operations that can 403 * only occur within the context of the recovered profile. 404 * 405 * @see BackupResource.recover() 406 * @param {object|null} postRecoveryEntry 407 * The object that was returned by the recover() method when the recovery 408 * was originally done. This object can be null if no additional information 409 * is needed for post-recovery. 410 */ 411 // eslint-disable-next-line no-unused-vars 412 async postRecovery(postRecoveryEntry) { 413 // no-op by default 414 } 415 } 416 417 XPCOMUtils.defineLazyPreferenceGetter( 418 BackupResource, 419 "SQLITE_PAGES_PER_STEP", 420 "browser.backup.sqlite.pages_per_step", 421 5 422 ); 423 424 XPCOMUtils.defineLazyPreferenceGetter( 425 BackupResource, 426 "SQLITE_STEP_DELAY_MS", 427 "browser.backup.sqlite.step_delay_ms", 428 250 429 );