commit 519a0057957012f50903ec5edc5b3f964dafc864
parent 33e94c6ee9937ddcafadbf26e3d227510b189eba
Author: David P. <daparks@mozilla.com>
Date: Wed, 3 Dec 2025 00:28:29 +0000
Bug 1996786: Filter downloads from places.sqlite backup r=places-reviewers,kpatenio,mak
It doesn't make sense to backup downloads since the common use case is to
restore the profile on a new OS install or another machine.
Differential Revision: https://phabricator.services.mozilla.com/D270774
Diffstat:
6 files changed, 156 insertions(+), 0 deletions(-)
diff --git a/browser/components/backup/resources/PlacesBackupResource.sys.mjs b/browser/components/backup/resources/PlacesBackupResource.sys.mjs
@@ -9,6 +9,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.sys.mjs",
+ PlacesDBUtils: "resource://gre/modules/PlacesDBUtils.sys.mjs",
});
const BOOKMARKS_BACKUP_FILENAME = "bookmarks.jsonlz4";
@@ -69,6 +70,12 @@ export class PlacesBackupResource extends BackupResource {
];
await Promise.all(timedCopies);
+ // Now that both databases are copied, open the places db copy to remove
+ // downloaded files, since they won't be valid in the restored profile.
+ await lazy.PlacesDBUtils.removeDownloadsMetadataFromDb(
+ PathUtils.join(stagingPath, "places.sqlite")
+ );
+
return null;
}
diff --git a/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js b/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js
@@ -9,6 +9,9 @@ const { BookmarkJSONUtils } = ChromeUtils.importESModule(
const { PlacesBackupResource } = ChromeUtils.importESModule(
"resource:///modules/backup/PlacesBackupResource.sys.mjs"
);
+const { PlacesDBUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesDBUtils.sys.mjs"
+);
registerCleanupFunction(() => {
/**
@@ -109,6 +112,7 @@ add_task(async function test_backup() {
close: sandbox.stub().resolves(true),
};
sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
+ sandbox.stub(PlacesDBUtils, "removeDownloadsMetadataFromDb");
let manifestEntry = await placesBackupResource.backup(
stagingPath,
@@ -121,6 +125,10 @@ add_task(async function test_backup() {
);
Assert.ok(
+ PlacesDBUtils.removeDownloadsMetadataFromDb.calledOnce,
+ "PlacesDBUtils.removeDownloadsMetadataFromDb was called"
+ );
+ Assert.ok(
fakeConnection.backup.calledTwice,
"Backup should have been called twice"
);
diff --git a/toolkit/components/places/PlacesDBUtils.sys.mjs b/toolkit/components/places/PlacesDBUtils.sys.mjs
@@ -1358,6 +1358,44 @@ export var PlacesDBUtils = {
}
return tasksMap;
},
+
+ /**
+ * Helper used by FxBackup to remove downloads metadata from a copy of the Places
+ * database, for profile migration to another install (such as on another
+ * machine).
+ *
+ * @param {string} placesDbPath Full path to places.sqlite database to filter
+ * downloads metadata from.
+ */
+ async removeDownloadsMetadataFromDb(placesDbPath) {
+ // Don't create the database if it doesn't exist.
+ if (!(await IOUtils.exists(placesDbPath))) {
+ return;
+ }
+
+ let connection;
+ try {
+ connection = await lazy.Sqlite.openConnection({
+ path: placesDbPath,
+ });
+ const removeDownloads = `
+ -- Find download annotations
+ WITH found_annos AS (
+ SELECT a.id AS anno_id
+ FROM moz_annos a
+ JOIN moz_anno_attributes attr
+ ON a.anno_attribute_id = attr.id
+ WHERE INSTR(attr.name, 'downloads/') = 1
+ )
+ -- Delete downloads from moz_annos but leave the URLs in moz_places history
+ DELETE FROM moz_annos
+ WHERE id IN (SELECT anno_id FROM found_annos);
+ `;
+ await connection.execute(removeDownloads);
+ } finally {
+ await connection?.close();
+ }
+ },
};
async function integrity(dbName) {
diff --git a/toolkit/components/places/tests/unit/test_places.sqlite b/toolkit/components/places/tests/unit/test_places.sqlite
Binary files differ.
diff --git a/toolkit/components/places/tests/unit/test_removeDownloadsMetadataFromDb.js b/toolkit/components/places/tests/unit/test_removeDownloadsMetadataFromDb.js
@@ -0,0 +1,98 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+ChromeUtils.defineESModuleGetters(this, {
+ Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
+});
+
+const selectDownloads = `
+ -- Find annotation
+ WITH found_annos AS (
+ SELECT a.id AS anno_id, a.anno_attribute_id
+ FROM moz_annos a
+ JOIN moz_anno_attributes attr
+ ON a.anno_attribute_id = attr.id
+ WHERE attr.name IN ('downloads/destinationFileURI', 'downloads/metaData')
+ )
+ -- Find downloads in moz_annos
+ SELECT * FROM moz_annos
+ WHERE id IN (SELECT anno_id FROM found_annos);
+`;
+
+add_task(async function test_removeDownloadsMetadataFromDb() {
+ Services.prefs.setStringPref("toolkit.sqlitejsm.loglevel", "Debug");
+ // Confirm that test_places.sqlite has the download information.
+ const testDbPath = PathUtils.join(do_get_cwd().path, "test_places.sqlite");
+ let dbConnection;
+ try {
+ dbConnection = await Sqlite.openConnection({
+ path: testDbPath,
+ readOnly: true,
+ });
+ let rows = await dbConnection.execute(selectDownloads, null, null);
+ const expectedResultsList = [
+ { id: 3, isFilename: true, filename: "20MB\(3\).zip" },
+ { id: 4, isFilename: false },
+ { id: 5, isFilename: true, filename: "50MB.zip" },
+ { id: 6, isFilename: false },
+ { id: 11, isFilename: true, filename: "5MB\(3\).zip" },
+ { id: 12, isFilename: true, filename: "10MB\(2\).zip" },
+ { id: 13, isFilename: false },
+ { id: 14, isFilename: false },
+ ];
+ for (let i = 0; i < expectedResultsList.length; i++) {
+ const row = rows[i];
+ const expectedResults = expectedResultsList[i];
+ Assert.equal(
+ row.getResultByIndex(0),
+ expectedResults.id,
+ `id match #${i}`
+ );
+ if (expectedResults.isFilename) {
+ Assert.ok(
+ row.getResultByIndex(3).endsWith(expectedResults.filename),
+ `filename match #${i}`
+ );
+ } else {
+ Assert.equal(
+ JSON.parse(row.getResultByIndex(3)).state,
+ 1,
+ `metadata state match #${i}`
+ );
+ }
+ }
+ } finally {
+ await dbConnection?.close();
+ }
+
+ // Make a copy of test_places.sqlite and remove the download entries from it.
+ const testDbCopyDir = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "test_removeDownloadsMetadataFromDb"
+ );
+ registerCleanupFunction(async () => {
+ await IOUtils.remove(testDbCopyDir, { recursive: true });
+ });
+ const testDbCopyPath = PathUtils.join(
+ testDbCopyDir,
+ "copy_test_places.sqlite"
+ );
+ await IOUtils.copy(testDbPath, testDbCopyPath);
+ await PlacesDBUtils.removeDownloadsMetadataFromDb(testDbCopyPath);
+
+ // Confirm that copy_test_places.sqlite does not have download information.
+ try {
+ dbConnection = await Sqlite.openConnection({
+ path: testDbCopyPath,
+ readOnly: true,
+ });
+ let rows = await dbConnection.execute(selectDownloads, null, null);
+ Assert.equal(
+ rows.length,
+ 0,
+ "moz_annos had no downloads or download metadata"
+ );
+ } finally {
+ await dbConnection?.close();
+ }
+});
diff --git a/toolkit/components/places/tests/unit/xpcshell.toml b/toolkit/components/places/tests/unit/xpcshell.toml
@@ -211,6 +211,11 @@ requesttimeoutfactor = 3 # Slow on Mac debug
["test_promiseBookmarksTree.js"]
+["test_removeDownloadsMetadataFromDb.js"]
+support-files = [
+ "test_places.sqlite",
+]
+
["test_resolveNullBookmarkTitles.js"]
["test_result_sort.js"]