Bug 824433 - Bookmarks backup takes a long time to write out on shutdown. r=mano

This commit is contained in:
Marco Bonardo 2014-02-04 14:43:20 +01:00
parent 239a6f697a
commit 3504ba0f46
10 changed files with 534 additions and 727 deletions

View File

@ -479,7 +479,7 @@ pref("browser.bookmarks.autoExportHTML", false);
// keep in {PROFILEDIR}/bookmarkbackups. Special values:
// -1: unlimited
// 0: no backups created (and deletes all existing backups)
pref("browser.bookmarks.max_backups", 10);
pref("browser.bookmarks.max_backups", 15);
// Scripts & Windows prefs
pref("dom.disable_open_during_load", true);

View File

@ -82,16 +82,20 @@ XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
"resource:///modules/BrowserUITelemetry.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
"resource:///modules/AsyncShutdown.jsm");
const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
const PREF_PLUGINS_UPDATEURL = "plugins.update.url";
// We try to backup bookmarks at idle times, to avoid doing that at shutdown.
// Number of idle seconds before trying to backup bookmarks. 10 minutes.
const BOOKMARKS_BACKUP_IDLE_TIME = 10 * 60;
// Minimum interval in milliseconds between backups.
const BOOKMARKS_BACKUP_INTERVAL = 86400 * 1000;
// Maximum number of backups to create. Old ones will be purged.
const BOOKMARKS_BACKUP_MAX_BACKUPS = 10;
// Seconds of idle before trying to create a bookmarks backup.
const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 10 * 60;
// Minimum interval between backups. We try to not create more than one backup
// per interval.
const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS = 1;
// Maximum interval between backups. If the last backup is older than these
// days we will try to create a new one more aggressively.
const BOOKMARKS_BACKUP_MAX_INTERVAL_DAYS = 5;
// Factory object
const BrowserGlueServiceFactory = {
@ -134,7 +138,6 @@ function BrowserGlue() {
BrowserGlue.prototype = {
_saveSession: false,
_isIdleObserver: false,
_isPlacesInitObserver: false,
_isPlacesLockedObserver: false,
_isPlacesShutdownObserver: false,
@ -262,8 +265,7 @@ BrowserGlue.prototype = {
this._onPlacesShutdown();
break;
case "idle":
if (this._idleService.idleTime > BOOKMARKS_BACKUP_IDLE_TIME * 1000)
this._backupBookmarks();
this._backupBookmarks();
break;
case "distribution-customization-complete":
Services.obs.removeObserver(this, "distribution-customization-complete");
@ -422,8 +424,10 @@ BrowserGlue.prototype = {
os.removeObserver(this, "weave:engine:clients:display-uri");
#endif
os.removeObserver(this, "session-save");
if (this._isIdleObserver)
this._idleService.removeIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME);
if (this._bookmarksBackupIdleTime) {
this._idleService.removeIdleObserver(this, this._bookmarksBackupIdleTime);
delete this._bookmarksBackupIdleTime;
}
if (this._isPlacesInitObserver)
os.removeObserver(this, "places-init-complete");
if (this._isPlacesLockedObserver)
@ -1060,14 +1064,18 @@ BrowserGlue.prototype = {
}
} catch(ex) {}
// This may be reused later, check for "=== undefined" to see if it has
// been populated already.
let lastBackupFile;
// If the user did not require to restore default bookmarks, or import
// from bookmarks.html, we will try to restore from JSON
if (importBookmarks && !restoreDefaultBookmarks && !importBookmarksHTML) {
// get latest JSON backup
var bookmarksBackupFile = yield PlacesBackups.getMostRecent("json");
if (bookmarksBackupFile) {
lastBackupFile = yield PlacesBackups.getMostRecentBackup("json");
if (lastBackupFile) {
// restore from JSON backup
yield BookmarkJSONUtils.importFromFile(bookmarksBackupFile, true);
yield BookmarkJSONUtils.importFromFile(lastBackupFile, true);
importBookmarks = false;
}
else {
@ -1162,10 +1170,39 @@ BrowserGlue.prototype = {
}
// Initialize bookmark archiving on idle.
// Once a day, either on idle or shutdown, bookmarks are backed up.
if (!this._isIdleObserver) {
this._idleService.addIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME);
this._isIdleObserver = true;
if (!this._bookmarksBackupIdleTime) {
this._bookmarksBackupIdleTime = BOOKMARKS_BACKUP_IDLE_TIME_SEC;
// If there is no backup, or the last bookmarks backup is too old, use
// a more aggressive idle observer.
if (lastBackupFile === undefined)
lastBackupFile = yield PlacesBackups.getMostRecentBackup();
if (!lastBackupFile) {
this._bookmarksBackupIdleTime /= 2;
}
else {
let lastBackupTime = PlacesBackups.getDateForFile(lastBackupFile);
let profileLastUse = Services.appinfo.replacedLockTime || Date.now();
// If there is a backup after the last profile usage date it's fine,
// regardless its age. Otherwise check how old is the last
// available backup compared to that session.
if (profileLastUse > lastBackupTime) {
let backupAge = Math.round((profileLastUse - lastBackupTime) / 86400000);
// Report the age of the last available backup.
try {
Services.telemetry
.getHistogramById("PLACES_BACKUPS_DAYSFROMLAST")
.add(backupAge);
} catch (ex) {
Components.utils.reportError("Unable to report telemetry.");
}
if (backupAge > BOOKMARKS_BACKUP_MAX_INTERVAL_DAYS)
this._bookmarksBackupIdleTime /= 2;
}
}
this._idleService.addIdleObserver(this, this._bookmarksBackupIdleTime);
}
Services.obs.notifyObservers(null, "places-browser-init-complete", "");
@ -1174,63 +1211,34 @@ BrowserGlue.prototype = {
/**
* Places shut-down tasks
* - back up bookmarks if needed.
* - export bookmarks as HTML, if so configured.
* - finalize components depending on Places.
* - export bookmarks as HTML, if so configured.
*/
_onPlacesShutdown: function BG__onPlacesShutdown() {
this._sanitizer.onShutdown();
PageThumbs.uninit();
if (this._isIdleObserver) {
this._idleService.removeIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME);
this._isIdleObserver = false;
if (this._bookmarksBackupIdleTime) {
this._idleService.removeIdleObserver(this, this._bookmarksBackupIdleTime);
delete this._bookmarksBackupIdleTime;
}
let waitingForBackupToComplete = true;
this._backupBookmarks().then(
function onSuccess() {
waitingForBackupToComplete = false;
},
function onFailure() {
Cu.reportError("Unable to backup bookmarks.");
waitingForBackupToComplete = false;
// Support legacy bookmarks.html format for apps that depend on that format.
try {
if (Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML")) {
// places-shutdown happens at profile-change-teardown, so here we
// can safely add a profile-before-change blocker.
AsyncShutdown.profileBeforeChange.addBlocker(
"Places: bookmarks.html",
() => BookmarkHTMLUtils.exportToFile(Services.dirsvc.get("BMarks", Ci.nsIFile))
.then(null, Cu.reportError)
);
}
);
// Backup bookmarks to bookmarks.html to support apps that depend
// on the legacy format.
let waitingForHTMLExportToComplete = false;
// If this fails to get the preference value, we don't export.
if (Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML")) {
// Exceptionally, since this is a non-default setting and HTML format is
// discouraged in favor of the JSON backups, we spin the event loop on
// shutdown, to wait for the export to finish. We cannot safely spin
// the event loop on shutdown until we include a watchdog to prevent
// potential hangs (bug 518683). The asynchronous shutdown operations
// will then be handled by a shutdown service (bug 435058).
waitingForHTMLExportToComplete = true;
BookmarkHTMLUtils.exportToFile(Services.dirsvc.get("BMarks", Ci.nsIFile)).then(
function onSuccess() {
waitingForHTMLExportToComplete = false;
},
function onFailure() {
Cu.reportError("Unable to auto export html.");
waitingForHTMLExportToComplete = false;
}
);
}
// The events loop should spin at least once because waitingForBackupToComplete
// is true before checking whether backup should be made.
let thread = Services.tm.currentThread;
while (waitingForBackupToComplete || waitingForHTMLExportToComplete) {
thread.processNextEvent(true);
}
} catch (ex) {} // Do not export.
},
/**
* Backup bookmarks.
* If a backup for today doesn't exist, this creates one.
*/
_backupBookmarks: function BG__backupBookmarks() {
return Task.spawn(function() {
@ -1238,14 +1246,9 @@ BrowserGlue.prototype = {
// Should backup bookmarks if there are no backups or the maximum
// interval between backups elapsed.
if (!lastBackupFile ||
new Date() - PlacesBackups.getDateForFile(lastBackupFile) > BOOKMARKS_BACKUP_INTERVAL) {
let maxBackups = BOOKMARKS_BACKUP_MAX_BACKUPS;
try {
maxBackups = Services.prefs.getIntPref("browser.bookmarks.max_backups");
}
catch(ex) { /* Use default. */ }
yield PlacesBackups.create(maxBackups); // Don't force creation.
new Date() - PlacesBackups.getDateForFile(lastBackupFile) > BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS * 86400000) {
let maxBackups = Services.prefs.getIntPref("browser.bookmarks.max_backups");
yield PlacesBackups.create(maxBackups);
}
});
},

View File

@ -540,7 +540,8 @@ var PlacesOrganizer = {
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
let fpCallback = function fpCallback_done(aResult) {
if (aResult != Ci.nsIFilePicker.returnCancel) {
PlacesBackups.saveBookmarksToJSONFile(fp.file);
// There is no OS.File version of the filepicker yet (Bug 937812).
PlacesBackups.saveBookmarksToJSONFile(fp.file.path);
}
};

View File

@ -4,91 +4,78 @@
* 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/. */
// Get bookmarks service
try {
var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
getService(Ci.nsINavBookmarksService);
} catch(ex) {
do_throw("Could not get Bookmarks service\n");
}
// Get annotation service
try {
var annosvc = Cc["@mozilla.org/browser/annotation-service;1"].
getService(Ci.nsIAnnotationService);
} catch(ex) {
do_throw("Could not get Annotation service\n");
}
// Get browser glue
try {
var gluesvc = Cc["@mozilla.org/browser/browserglue;1"].
getService(Ci.nsIBrowserGlue).
QueryInterface(Ci.nsIObserver);
// Avoid default bookmarks import.
gluesvc.observe(null, "initial-migration-will-import-default-bookmarks", "");
// gluesvc.observe(null, "initial-migration-did-import-default-bookmarks", "");
} catch(ex) {
do_throw("Could not get BrowserGlue service\n");
}
// Get pref service
try {
var pref = Cc["@mozilla.org/preferences-service;1"].
getService(Ci.nsIPrefBranch);
} catch(ex) {
do_throw("Could not get Preferences service\n");
}
const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
const SMART_BOOKMARKS_PREF = "browser.places.smartBookmarksVersion";
// main
let gluesvc = Cc["@mozilla.org/browser/browserglue;1"].
getService(Ci.nsIBrowserGlue).
QueryInterface(Ci.nsIObserver);
// Avoid default bookmarks import.
gluesvc.observe(null, "initial-migration-will-import-default-bookmarks", "");
function run_test() {
// TEST 1: smart bookmarks disabled
pref.setIntPref("browser.places.smartBookmarksVersion", -1);
gluesvc.ensurePlacesDefaultQueriesInitialized();
var smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
do_check_eq(smartBookmarkItemIds.length, 0);
// check that pref has not been bumped up
do_check_eq(pref.getIntPref("browser.places.smartBookmarksVersion"), -1);
// TEST 2: create smart bookmarks
pref.setIntPref("browser.places.smartBookmarksVersion", 0);
gluesvc.ensurePlacesDefaultQueriesInitialized();
smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
do_check_neq(smartBookmarkItemIds.length, 0);
// check that pref has been bumped up
do_check_true(pref.getIntPref("browser.places.smartBookmarksVersion") > 0);
var smartBookmarksCount = smartBookmarkItemIds.length;
// TEST 3: smart bookmarks restore
// remove one smart bookmark and restore
bmsvc.removeItem(smartBookmarkItemIds[0]);
pref.setIntPref("browser.places.smartBookmarksVersion", 0);
gluesvc.ensurePlacesDefaultQueriesInitialized();
smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount);
// check that pref has been bumped up
do_check_true(pref.getIntPref("browser.places.smartBookmarksVersion") > 0);
// TEST 4: move a smart bookmark, change its title, then restore
// smart bookmark should be restored in place
var parent = bmsvc.getFolderIdForItem(smartBookmarkItemIds[0]);
var oldTitle = bmsvc.getItemTitle(smartBookmarkItemIds[0]);
// create a subfolder and move inside it
var newParent = bmsvc.createFolder(parent, "test", bmsvc.DEFAULT_INDEX);
bmsvc.moveItem(smartBookmarkItemIds[0], newParent, bmsvc.DEFAULT_INDEX);
// change title
bmsvc.setItemTitle(smartBookmarkItemIds[0], "new title");
// restore
pref.setIntPref("browser.places.smartBookmarksVersion", 0);
gluesvc.ensurePlacesDefaultQueriesInitialized();
smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount);
do_check_eq(bmsvc.getFolderIdForItem(smartBookmarkItemIds[0]), newParent);
do_check_eq(bmsvc.getItemTitle(smartBookmarkItemIds[0]), oldTitle);
// check that pref has been bumped up
do_check_true(pref.getIntPref("browser.places.smartBookmarksVersion") > 0);
run_next_test();
}
add_task(function smart_bookmarks_disabled() {
Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1);
gluesvc.ensurePlacesDefaultQueriesInitialized();
let smartBookmarkItemIds =
PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
do_check_eq(smartBookmarkItemIds.length, 0);
do_log_info("check that pref has not been bumped up");
do_check_eq(Services.prefs.getIntPref("browser.places.smartBookmarksVersion"), -1);
});
add_task(function create_smart_bookmarks() {
Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
gluesvc.ensurePlacesDefaultQueriesInitialized();
let smartBookmarkItemIds =
PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
do_check_neq(smartBookmarkItemIds.length, 0);
do_log_info("check that pref has been bumped up");
do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
});
add_task(function remove_smart_bookmark_and_restore() {
let smartBookmarkItemIds =
PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
let smartBookmarksCount = smartBookmarkItemIds.length;
do_log_info("remove one smart bookmark and restore");
PlacesUtils.bookmarks.removeItem(smartBookmarkItemIds[0]);
Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
gluesvc.ensurePlacesDefaultQueriesInitialized();
let smartBookmarkItemIds =
PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount);
do_log_info("check that pref has been bumped up");
do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
});
add_task(function move_smart_bookmark_rename_and_restore() {
let smartBookmarkItemIds =
PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
let smartBookmarksCount = smartBookmarkItemIds.length;
do_log_info("smart bookmark should be restored in place");
let parent = PlacesUtils.bookmarks.getFolderIdForItem(smartBookmarkItemIds[0]);
let oldTitle = PlacesUtils.bookmarks.getItemTitle(smartBookmarkItemIds[0]);
// create a subfolder and move inside it
let newParent =
PlacesUtils.bookmarks.createFolder(parent, "test",
PlacesUtils.bookmarks.DEFAULT_INDEX);
PlacesUtils.bookmarks.moveItem(smartBookmarkItemIds[0], newParent,
PlacesUtils.bookmarks.DEFAULT_INDEX);
// change title
PlacesUtils.bookmarks.setItemTitle(smartBookmarkItemIds[0], "new title");
// restore
Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
gluesvc.ensurePlacesDefaultQueriesInitialized();
smartBookmarkItemIds =
PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount);
do_check_eq(PlacesUtils.bookmarks.getFolderIdForItem(smartBookmarkItemIds[0]), newParent);
do_check_eq(PlacesUtils.bookmarks.getItemTitle(smartBookmarkItemIds[0]), oldTitle);
do_log_info("check that pref has been bumped up");
do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
});

View File

@ -1,153 +0,0 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et: */
/* 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 http://mozilla.org/MPL/2.0/. */
/**
* Tests that nsBrowserGlue is correctly exporting based on preferences values,
* and creating bookmarks backup if one does not exist for today.
*/
// Initialize nsBrowserGlue after Places.
let bg = Cc["@mozilla.org/browser/browserglue;1"].
getService(Ci.nsIBrowserGlue);
// Initialize Places through Bookmarks Service.
let bs = PlacesUtils.bookmarks;
// Get other services.
let ps = Services.prefs;
let os = Services.obs;
const PREF_AUTO_EXPORT_HTML = "browser.bookmarks.autoExportHTML";
let tests = [];
//------------------------------------------------------------------------------
tests.push({
description: "Export to bookmarks.html if autoExportHTML is true.",
exec: function() {
remove_all_JSON_backups();
// Sanity check: we should have bookmarks on the toolbar.
do_check_true(bs.getIdForItemAt(bs.toolbarFolder, 0) > 0);
// Set preferences.
ps.setBoolPref(PREF_AUTO_EXPORT_HTML, true);
// Force nsBrowserGlue::_shutdownPlaces().
bg.QueryInterface(Ci.nsIObserver).observe(null,
PlacesUtils.TOPIC_SHUTDOWN,
null);
// Check bookmarks.html has been created.
check_bookmarks_html();
// Check JSON backup has been created.
check_JSON_backup(true);
// Check preferences have not been reverted.
do_check_true(ps.getBoolPref(PREF_AUTO_EXPORT_HTML));
// Reset preferences.
ps.setBoolPref(PREF_AUTO_EXPORT_HTML, false);
next_test();
}
});
//------------------------------------------------------------------------------
tests.push({
description: "Export to bookmarks.html if autoExportHTML is true and a bookmarks.html exists.",
exec: function() {
// Sanity check: we should have bookmarks on the toolbar.
do_check_true(bs.getIdForItemAt(bs.toolbarFolder, 0) > 0);
// Set preferences.
ps.setBoolPref(PREF_AUTO_EXPORT_HTML, true);
// Create a bookmarks.html in the profile.
let profileBookmarksHTMLFile = create_bookmarks_html("bookmarks.glue.html");
// set the file's lastModifiedTime to one minute ago and get its size.
let lastMod = Date.now() - 60*1000;
profileBookmarksHTMLFile.lastModifiedTime = lastMod;
let fileSize = profileBookmarksHTMLFile.fileSize;
// Force nsBrowserGlue::_shutdownPlaces().
bg.QueryInterface(Ci.nsIObserver).observe(null,
PlacesUtils.TOPIC_SHUTDOWN,
null);
// Check a new bookmarks.html has been created.
let profileBookmarksHTMLFile = check_bookmarks_html();
do_check_true(profileBookmarksHTMLFile.lastModifiedTime > lastMod);
do_check_neq(profileBookmarksHTMLFile.fileSize, fileSize);
// Check preferences have not been reverted.
do_check_true(ps.getBoolPref(PREF_AUTO_EXPORT_HTML));
// Reset preferences.
ps.setBoolPref(PREF_AUTO_EXPORT_HTML, false);
next_test();
}
});
//------------------------------------------------------------------------------
tests.push({
description: "Backup to JSON should be a no-op if a backup for today already exists.",
exec: function() {
// Sanity check: we should have bookmarks on the toolbar.
do_check_true(bs.getIdForItemAt(bs.toolbarFolder, 0) > 0);
// Create a JSON backup in the profile.
let profileBookmarksJSONFile = create_JSON_backup("bookmarks.glue.json");
// Get file lastModified and size.
let lastMod = profileBookmarksJSONFile.lastModifiedTime;
let fileSize = profileBookmarksJSONFile.fileSize;
// Force nsBrowserGlue::_shutdownPlaces().
bg.QueryInterface(Ci.nsIObserver).observe(null,
PlacesUtils.TOPIC_SHUTDOWN,
null);
// Check a new JSON backup has not been created.
do_check_true(profileBookmarksJSONFile.exists());
do_check_eq(profileBookmarksJSONFile.lastModifiedTime, lastMod);
do_check_eq(profileBookmarksJSONFile.fileSize, fileSize);
do_test_finished();
}
});
//------------------------------------------------------------------------------
var testIndex = 0;
function next_test() {
// Remove bookmarks.html from profile.
remove_bookmarks_html();
// Execute next test.
let test = tests.shift();
dump("\nTEST " + (++testIndex) + ": " + test.description);
test.exec();
}
function run_test() {
do_test_pending();
// Clean up bookmarks.
remove_all_bookmarks();
// Create some bookmarks.
bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://mozilla.org/"),
bs.DEFAULT_INDEX, "bookmark-on-menu");
bs.insertBookmark(bs.toolbarFolder, uri("http://mozilla.org/"),
bs.DEFAULT_INDEX, "bookmark-on-toolbar");
// Kick-off tests.
next_test();
}

View File

@ -19,8 +19,8 @@ const TOPIC_CONNECTION_CLOSED = "places-connection-closed";
let EXPECTED_NOTIFICATIONS = [
"places-shutdown"
, "places-expiration-finished"
, "places-will-close-connection"
, "places-expiration-finished"
, "places-connection-closed"
];

View File

@ -16,7 +16,6 @@ support-files =
[test_browserGlue_migrate.js]
[test_browserGlue_prefs.js]
[test_browserGlue_restore.js]
[test_browserGlue_shutdown.js]
[test_browserGlue_smartBookmarks.js]
[test_clearHistory_shutdown.js]
[test_leftpane_corruption_handling.js]

View File

@ -9,14 +9,24 @@ const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://gre/modules/Sqlite.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
"resource://gre/modules/PlacesBackups.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
"resource://gre/modules/Deprecated.jsm");
XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => new TextDecoder());
XPCOMUtils.defineLazyGetter(this, "gTextEncoder", () => new TextEncoder());
XPCOMUtils.defineLazyGetter(this, "localFileCtor",
() => Components.Constructor("@mozilla.org/file/local;1",
"nsILocalFile", "initWithPath"));
this.BookmarkJSONUtils = Object.freeze({
/**
@ -41,8 +51,8 @@ this.BookmarkJSONUtils = Object.freeze({
* @note any item annotated with "places/excludeFromBackup" won't be removed
* before executing the restore.
*
* @param aFile
* nsIFile of bookmarks in JSON format to be restored.
* @param aFilePath
* OS.File path or nsIFile of bookmarks in JSON format to be restored.
* @param aReplace
* Boolean if true, replace existing bookmarks, else merge.
*
@ -50,24 +60,51 @@ this.BookmarkJSONUtils = Object.freeze({
* @resolves When the new bookmarks have been created.
* @rejects JavaScript exception.
*/
importFromFile: function BJU_importFromFile(aFile, aReplace) {
importFromFile: function BJU_importFromFile(aFilePath, aReplace) {
let importer = new BookmarkImporter();
return importer.importFromFile(aFile, aReplace);
// TODO (bug 967192): convert to pure OS.File
let file = aFilePath instanceof Ci.nsIFile ? aFilePath
: new localFileCtor(aFilePath);
return importer.importFromFile(file, aReplace);
},
/**
* Serializes bookmarks using JSON, and writes to the supplied file.
* Serializes bookmarks using JSON, and writes to the supplied file path.
*
* @param aLocalFile
* nsIFile for the "bookmarks.json" file to be created.
* @param aFilePath
* OS.File path for the "bookmarks.json" file to be created.
*
* @return {Promise}
* @resolves When the file has been created.
* @resolves To the exported bookmarks count when the file has been created.
* @rejects JavaScript exception.
* @deprecated passing an nsIFile is deprecated
*/
exportToFile: function BJU_exportToFile(aLocalFile) {
let exporter = new BookmarkExporter();
return exporter.exportToFile(aLocalFile);
exportToFile: function BJU_exportToFile(aFilePath) {
if (aFilePath instanceof Ci.nsIFile) {
Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.exportToFile " +
"is deprecated. Please use an OS.File path instead.",
"https://developer.mozilla.org/docs/JavaScript_OS.File");
aFilePath = aFilePath.path;
}
return Task.spawn(function* () {
let [bookmarks, count] = yield PlacesBackups.getBookmarksTree();
let startTime = Date.now();
let jsonString = JSON.stringify(bookmarks);
// Report the time taken to convert the tree to JSON.
try {
Services.telemetry
.getHistogramById("PLACES_BACKUPS_TOJSON_MS")
.add(Date.now() - startTime);
} catch (ex) {
Components.utils.reportError("Unable to report telemetry.");
}
// Write to the temp folder first, to avoid leaving back partial files.
let tmpPath = OS.Path.join(OS.Constants.Path.tmpDir,
OS.Path.basename(aFilePath) + ".tmp");
yield OS.File.writeAtomic(aFilePath, jsonString, { tmpPath: tmpPath });
return count;
});
},
/**
@ -362,7 +399,8 @@ BookmarkImporter.prototype = {
});
return [folderIdMap, searchIds];
}
} else if (aData.livemark && aData.annos) {
} else if (aData.annos &&
aData.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) {
// Node is a livemark
let feedURI = null;
let siteURI = null;
@ -422,7 +460,8 @@ BookmarkImporter.prototype = {
if (aData.keyword)
PlacesUtils.bookmarks.setKeywordForBookmark(id, aData.keyword);
if (aData.tags) {
let tags = aData.tags.split(", ");
// TODO (bug 967196) the tagging service should trim by itself.
let tags = aData.tags.split(",").map(tag => tag.trim());
if (tags.length)
PlacesUtils.tagging.tagURI(NetUtil.newURI(aData.uri), tags);
}
@ -502,306 +541,6 @@ function fixupQuery(aQueryURI, aFolderIdMap) {
return NetUtil.newURI(stringURI);
}
function BookmarkExporter() {}
BookmarkExporter.prototype = {
exportToFile: function BE_exportToFile(aLocalFile) {
return Task.spawn(this._writeToFile(aLocalFile));
},
_converterOut: null,
_writeToFile: function BE__writeToFile(aLocalFile) {
// Create a file that can be accessed by the current user only.
let safeFileOut = Cc["@mozilla.org/network/safe-file-output-stream;1"].
createInstance(Ci.nsIFileOutputStream);
safeFileOut.init(aLocalFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
FileUtils.MODE_TRUNCATE, parseInt("0600", 8), 0);
let nodeCount;
try {
// We need a buffered output stream for performance. See bug 202477.
let bufferedOut = Cc["@mozilla.org/network/buffered-output-stream;1"].
createInstance(Ci.nsIBufferedOutputStream);
bufferedOut.init(safeFileOut, 4096);
try {
// Write bookmarks in UTF-8.
this._converterOut = Cc["@mozilla.org/intl/converter-output-stream;1"].
createInstance(Ci.nsIConverterOutputStream);
this._converterOut.init(bufferedOut, "utf-8", 0, 0);
try {
nodeCount = yield this._writeContentToFile();
// Flush the buffer and retain the target file on success only.
bufferedOut.QueryInterface(Ci.nsISafeOutputStream).finish();
} finally {
this._converterOut.close();
this._converterOut = null;
}
} finally {
bufferedOut.close();
}
} finally {
safeFileOut.close();
}
throw new Task.Result(nodeCount);
},
_writeContentToFile: function BE__writeContentToFile() {
return Task.spawn(function() {
// Weep over stream interface variance.
let streamProxy = {
converter: this._converterOut,
write: function(aData, aLen) {
this.converter.writeString(aData);
}
};
// Get list of itemIds that must be excluded from the backup.
let excludeItems = PlacesUtils.annotations.getItemsWithAnnotation(
PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
// Serialize to JSON and write to stream.
let nodeCount = yield BookmarkRow.serializeJSONToOutputStream(streamProxy,
excludeItems);
throw new Task.Result(nodeCount);
}.bind(this));
}
}
let BookmarkRow = {
/**
* Serializes the SQL results as JSON with async SQL call and writes the
* serialization to the given output stream.
*
* @param aStream
* An nsIOutputStream. NOTE: it only uses the write(str, len)
* method of nsIOutputStream. The caller is responsible for
* closing the stream.
* @param aExcludeItems
* An array of item ids that should not be written to the backup.
* @return {Promise}
* @resolves the number of serialized uri nodes.
*/
serializeJSONToOutputStream: function(aStream, aExcludeItems) {
return Task.spawn(function() {
let nodes = [];
let nodeCount = 0;
let dbFilePath = OS.Path.join(OS.Constants.Path.profileDir,
"places.sqlite");
let conn = yield Sqlite.openConnection({ path: dbFilePath,
sharedMemoryCache: false });
try {
let rows = yield conn.execute(
"SELECT b.id, h.url, b.position, b.title, b.parent, " +
"b.type, b.dateAdded, b.lastModified, b.guid, t.parent AS grandParent " +
"FROM moz_bookmarks b " +
"LEFT JOIN moz_bookmarks t ON t.id = b.parent " +
"LEFT JOIN moz_places h ON h.id = b.fk " +
"ORDER BY b.parent, b.position, b.id");
// Create a Map for lookup.
let rowMap = new Map();
for (let row of rows) {
let parent = row.getResultByName("parent");
if (rowMap.has(parent)) {
let data = rowMap.get(parent);
data.children.push(row);
} else {
rowMap.set(parent, { children: [row] });
}
}
let root = rowMap.get(0);
if (!root) {
throw new Error("Root does not exist.");
}
let result = yield BookmarkRow._appendConvertedNode(root.children[0],
rowMap,
nodes,
aExcludeItems);
if (result.appendedNode) {
nodeCount = result.nodeCount;
let json = JSON.stringify(nodes[0]);
aStream.write(json, json.length);
}
} catch(e) {
Cu.reportError("serializeJSONToOutputStream error " + e);
} finally {
yield conn.close();
}
throw new Task.Result(nodeCount);
});
},
_appendConvertedNode: function BR__appendConvertedNode(
aRow, aRowMap, aNodes, aExcludeItems) {
return Task.spawn(function() {
let node = {};
let nodeCount = 0;
this._addGenericProperties(aRow, node);
let parent = aRow.getResultByName("parent");
let grandParent = parent ? aRow.getResultByName("grandParent") : null;
let type = aRow.getResultByName("type");
if (type == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
// Tag root accept only folder nodes
if (parent == PlacesUtils.tagsFolderId)
throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
// Check for url validity, since we can't halt while writing a backup.
// This will throw if we try to serialize an invalid url and it does
// not make sense saving a wrong or corrupt uri node.
try {
NetUtil.newURI(aRow.getResultByName("url"));
} catch (ex) {
throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
}
yield this._addURIProperties(aRow, node);
nodeCount++;
} else if (type == Ci.nsINavBookmarksService.TYPE_FOLDER) {
// Tag containers accept only uri nodes
if (grandParent && grandParent == PlacesUtils.tagsFolderId) {
throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
}
this._addContainerProperties(aRow, node);
} else if (type == Ci.nsINavBookmarksService.TYPE_SEPARATOR) {
// Tag root accept only folder nodes
// Tag containers accept only uri nodes
if ((parent == PlacesUtils.tagsFolderId) ||
(grandParent == PlacesUtils.tagsFolderId)) {
throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
}
this._addSeparatorProperties(aRow, node);
}
if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
nodeCount += yield this._appendConvertedComplexNode(node,
aNodes,
aRowMap,
aExcludeItems);
throw new Task.Result({ appendedNode: true, nodeCount: nodeCount });
}
aNodes.push(node);
throw new Task.Result({ appendedNode: true, nodeCount: nodeCount });
}.bind(this));
},
_addGenericProperties: function BR__addGenericProperties(aRow, aJSNode) {
let title = aRow.getResultByName("title")
aJSNode.title = title ? title : "";
aJSNode.guid = aRow.getResultByName("guid");
aJSNode.id = aRow.getResultByName("id");
aJSNode.index = aRow.getResultByName("position");
if (aJSNode.id != -1) {
let parent = aRow.getResultByName("parent");
if (parent)
aJSNode.parent = parent;
let dateAdded = aRow.getResultByName("dateAdded");;
if (dateAdded)
aJSNode.dateAdded = dateAdded;
let lastModified = aRow.getResultByName("lastModified");
if (lastModified)
aJSNode.lastModified = lastModified;
// XXX need a hasAnnos api
let annos = [];
try {
annos =
PlacesUtils.getAnnotationsForItem(aJSNode.id).filter(function(anno) {
// XXX should whitelist this instead, w/ a pref for
// backup/restore of non-whitelisted annos
// XXX causes JSON encoding errors, so utf-8 encode
// anno.value = unescape(encodeURIComponent(anno.value));
if (anno.name == PlacesUtils.LMANNO_FEEDURI)
aJSNode.livemark = 1;
return true;
});
} catch(ex) {}
if (annos.length != 0)
aJSNode.annos = annos;
}
// XXXdietrich - store annos for non-bookmark items
},
_addURIProperties: function BR__addURIProperties(aRow, aJSNode) {
return Task.spawn(function() {
aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
aJSNode.uri = aRow.getResultByName("url");
if (aJSNode.id && aJSNode.id != -1) {
// Harvest bookmark-specific properties
let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(aJSNode.id);
if (keyword)
aJSNode.keyword = keyword;
}
// Last character-set
let uri = NetUtil.newURI(aRow.getResultByName("url"));
let lastCharset = yield PlacesUtils.getCharsetForURI(uri)
if (lastCharset)
aJSNode.charset = lastCharset;
});
},
_addSeparatorProperties: function BR__addSeparatorProperties(aRow, aJSNode) {
aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
},
_addContainerProperties: function BR__addContainerProperties(aRow, aJSNode) {
// This is a bookmark or a tag container.
// Bookmark folder or a shortcut we should convert to folder.
aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
// Mark root folders
let itemId = aRow.getResultByName("id");
if (itemId == PlacesUtils.placesRootId)
aJSNode.root = "placesRoot";
else if (itemId == PlacesUtils.bookmarksMenuFolderId)
aJSNode.root = "bookmarksMenuFolder";
else if (itemId == PlacesUtils.tagsFolderId)
aJSNode.root = "tagsFolder";
else if (itemId == PlacesUtils.unfiledBookmarksFolderId)
aJSNode.root = "unfiledBookmarksFolder";
else if (itemId == PlacesUtils.toolbarFolderId)
aJSNode.root = "toolbarFolder";
},
_appendConvertedComplexNode: function BR__appendConvertedComplexNode(
aNode, aNodes, aRowMap, aExcludeItems) {
return Task.spawn(function() {
let repr = {};
let nodeCount = 0;
for (let [name, value] in Iterator(aNode))
repr[name] = value;
repr.children = [];
let data = aRowMap.get(aNode.id);
if (data) {
for (let row of data.children) {
let id = row.getResultByName("id");
// ignore exclude items
if (aExcludeItems && aExcludeItems.indexOf(id) != -1) {
continue;
}
let result = yield this._appendConvertedNode(row,
aRowMap,
repr.children,
aExcludeItems);
nodeCount += result.nodeCount;
}
} else {
Cu.reportError("_appendConvertedComplexNode error: Unable to find node");
}
aNodes.push(repr);
throw new Task.Result(nodeCount);
}.bind(this));
}
}
let BookmarkNode = {
/**
* Serializes the given node (and all its descendents) as JSON
@ -826,7 +565,7 @@ let BookmarkNode = {
serializeAsJSONToOutputStream: function BN_serializeAsJSONToOutputStream(
aNode, aStream, aIsUICommand, aResolveShortcuts, aExcludeItems) {
return Task.spawn(function() {
return Task.spawn(function* () {
// Serialize to stream
let array = [];
let result = yield this._appendConvertedNode(aNode, null, array,
@ -834,18 +573,18 @@ let BookmarkNode = {
aResolveShortcuts,
aExcludeItems);
if (result.appendedNode) {
let json = JSON.stringify(array[0]);
aStream.write(json, json.length);
let jsonString = JSON.stringify(array[0]);
aStream.write(jsonString, jsonString.length);
} else {
throw Cr.NS_ERROR_UNEXPECTED;
}
throw new Task.Result(result.nodeCount);
return result.nodeCount;
}.bind(this));
},
_appendConvertedNode: function BN__appendConvertedNode(
bNode, aIndex, aArray, aIsUICommand, aResolveShortcuts, aExcludeItems) {
return Task.spawn(function() {
return Task.spawn(function* () {
let node = {};
let nodeCount = 0;
@ -863,7 +602,7 @@ let BookmarkNode = {
if (PlacesUtils.nodeIsURI(bNode)) {
// Tag root accept only folder nodes
if (parent && parent.itemId == PlacesUtils.tagsFolderId)
throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
return { appendedNode: false, nodeCount: nodeCount };
// Check for url validity, since we can't halt while writing a backup.
// This will throw if we try to serialize an invalid url and it does
@ -871,7 +610,7 @@ let BookmarkNode = {
try {
NetUtil.newURI(bNode.uri);
} catch (ex) {
throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
return { appendedNode: false, nodeCount: nodeCount };
}
yield this._addURIProperties(bNode, node, aIsUICommand);
@ -879,7 +618,7 @@ let BookmarkNode = {
} else if (PlacesUtils.nodeIsContainer(bNode)) {
// Tag containers accept only uri nodes
if (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId)
throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
return { appendedNode: false, nodeCount: nodeCount };
this._addContainerProperties(bNode, node, aIsUICommand,
aResolveShortcuts);
@ -888,7 +627,7 @@ let BookmarkNode = {
// Tag containers accept only uri nodes
if ((parent && parent.itemId == PlacesUtils.tagsFolderId) ||
(grandParent && grandParent.itemId == PlacesUtils.tagsFolderId))
throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
return { appendedNode: false, nodeCount: nodeCount };
this._addSeparatorProperties(bNode, node);
}
@ -900,11 +639,11 @@ let BookmarkNode = {
aIsUICommand,
aResolveShortcuts,
aExcludeItems)
throw new Task.Result({ appendedNode: true, nodeCount: nodeCount });
return { appendedNode: true, nodeCount: nodeCount };
}
aArray.push(node);
throw new Task.Result({ appendedNode: true, nodeCount: nodeCount });
return { appendedNode: true, nodeCount: nodeCount };
}.bind(this));
},
@ -932,8 +671,6 @@ let BookmarkNode = {
// backup/restore of non-whitelisted annos
// XXX causes JSON encoding errors, so utf-8 encode
// anno.value = unescape(encodeURIComponent(anno.value));
if (anno.name == PlacesUtils.LMANNO_FEEDURI)
aJSNode.livemark = 1;
if (anno.name == PlacesUtils.READ_ONLY_ANNO && aResolveShortcuts) {
// When copying a read-only node, remove the read-only annotation.
return false;
@ -1014,7 +751,7 @@ let BookmarkNode = {
_appendConvertedComplexNode: function BN__appendConvertedComplexNode(
aNode, aSourceNode, aArray, aIsUICommand, aResolveShortcuts,
aExcludeItems) {
return Task.spawn(function() {
return Task.spawn(function* () {
let repr = {};
let nodeCount = 0;
@ -1023,7 +760,8 @@ let BookmarkNode = {
// Write child nodes
let children = repr.children = [];
if (!aNode.livemark) {
if (!aNode.annos ||
!aNode.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) {
PlacesUtils.asContainer(aSourceNode);
let wasOpen = aSourceNode.containerOpen;
if (!wasOpen)
@ -1043,7 +781,7 @@ let BookmarkNode = {
}
aArray.push(repr);
throw new Task.Result(nodeCount);
return nodeCount;
}.bind(this));
}
}
}

View File

@ -17,11 +17,16 @@ Cu.import("resource://gre/modules/BookmarkJSONUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Deprecated.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
"resource://gre/modules/Sqlite.jsm");
XPCOMUtils.defineLazyGetter(this, "localFileCtor",
() => Components.Constructor("@mozilla.org/file/local;1",
"nsILocalFile", "initWithPath"));
this.PlacesBackups = {
get _filenamesRegex() {
@ -40,7 +45,14 @@ this.PlacesBackups = {
Deprecated.warning(
"PlacesBackups.folder is deprecated and will be removed in a future version",
"https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
return this._folder;
},
/**
* This exists just to avoid spamming deprecate warnings from internal calls
* needed to support deprecated methods themselves.
*/
get _folder() {
let bookmarksBackupDir = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
bookmarksBackupDir.append(this.profileRelativeFolderPath);
if (!bookmarksBackupDir.exists()) {
@ -48,8 +60,8 @@ this.PlacesBackups = {
if (!bookmarksBackupDir.exists())
throw("Unable to create bookmarks backup folder");
}
delete this.folder;
return this.folder = bookmarksBackupDir;
delete this._folder;
return this._folder = bookmarksBackupDir;
},
/**
@ -58,21 +70,15 @@ this.PlacesBackups = {
* @resolve the folder (the folder string path).
*/
getBackupFolder: function PB_getBackupFolder() {
return Task.spawn(function() {
if (this._folder) {
throw new Task.Result(this._folder);
return Task.spawn(function* () {
if (this._backupFolder) {
return this._backupFolder;
}
let profileDir = OS.Constants.Path.profileDir;
let backupsDirPath = OS.Path.join(profileDir, this.profileRelativeFolderPath);
yield OS.File.makeDir(backupsDirPath, { ignoreExisting: true }).then(
function onSuccess() {
this._folder = backupsDirPath;
}.bind(this),
function onError() {
throw("Unable to create bookmarks backup folder");
});
throw new Task.Result(this._folder);
}.bind(this));
yield OS.File.makeDir(backupsDirPath, { ignoreExisting: true });
return this._backupFolder = backupsDirPath;
}.bind(this));
},
get profileRelativeFolderPath() "bookmarkbackups",
@ -84,10 +90,17 @@ this.PlacesBackups = {
Deprecated.warning(
"PlacesBackups.entries is deprecated and will be removed in a future version",
"https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
return this._entries;
},
delete this.entries;
this.entries = [];
let files = this.folder.directoryEntries;
/**
* This exists just to avoid spamming deprecate warnings from internal calls
* needed to support deprecated methods themselves.
*/
get _entries() {
delete this._entries;
this._entries = [];
let files = this._folder.directoryEntries;
while (files.hasMoreElements()) {
let entry = files.getNext().QueryInterface(Ci.nsIFile);
// A valid backup is any file that matches either the localized or
@ -99,15 +112,15 @@ this.PlacesBackups = {
entry.remove(false);
continue;
}
this.entries.push(entry);
this._entries.push(entry);
}
}
this.entries.sort((a, b) => {
this._entries.sort((a, b) => {
let aDate = this.getDateForFile(a);
let bDate = this.getDateForFile(b);
return aDate < bDate ? 1 : aDate > bDate ? -1 : 0;
});
return this.entries;
return this._entries;
},
/**
@ -116,10 +129,10 @@ this.PlacesBackups = {
* @resolve a sorted array of string paths.
*/
getBackupFiles: function PB_getBackupFiles() {
return Task.spawn(function() {
if (this._backupFiles) {
throw new Task.Result(this._backupFiles);
}
return Task.spawn(function* () {
if (this._backupFiles)
return this._backupFiles;
this._backupFiles = [];
let backupFolderPath = yield this.getBackupFolder();
@ -138,13 +151,13 @@ this.PlacesBackups = {
}.bind(this));
iterator.close();
this._backupFiles.sort(function(a, b) {
this._backupFiles.sort((a, b) => {
let aDate = this.getDateForFile(a);
let bDate = this.getDateForFile(b);
return aDate < bDate ? 1 : aDate > bDate ? -1 : 0;
}.bind(this));
});
throw new Task.Result(this._backupFiles);
return this._backupFiles;
}.bind(this));
},
@ -194,10 +207,10 @@ this.PlacesBackups = {
"https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
let fileExt = aFileExt || "(json|html)";
for (let i = 0; i < this.entries.length; i++) {
for (let i = 0; i < this._entries.length; i++) {
let rx = new RegExp("\." + fileExt + "$");
if (this.entries[i].leafName.match(rx))
return this.entries[i];
if (this._entries[i].leafName.match(rx))
return this._entries[i];
}
return null;
},
@ -212,16 +225,16 @@ this.PlacesBackups = {
* @result the path to the file.
*/
getMostRecentBackup: function PB_getMostRecentBackup(aFileExt) {
return Task.spawn(function() {
return Task.spawn(function* () {
let fileExt = aFileExt || "(json|html)";
let entries = yield this.getBackupFiles();
for (let entry of entries) {
let rx = new RegExp("\." + fileExt + "$");
if (OS.Path.basename(entry).match(rx)) {
throw new Task.Result(entry);
return entry;
}
}
throw new Task.Result(null);
return null;
}.bind(this));
},
@ -230,23 +243,31 @@ this.PlacesBackups = {
* Note: any item that should not be backed up must be annotated with
* "places/excludeFromBackup".
*
* @param aFile
* nsIFile where to save JSON backup.
* @param aFilePath
* OS.File path for the "bookmarks.json" file to be created.
* @return {Promise}
* @resolves the number of serialized uri nodes.
* @deprecated passing an nsIFile is deprecated
*/
saveBookmarksToJSONFile: function PB_saveBookmarksToJSONFile(aFile) {
return Task.spawn(function() {
let nodeCount = yield BookmarkJSONUtils.exportToFile(aFile);
saveBookmarksToJSONFile: function PB_saveBookmarksToJSONFile(aFilePath) {
if (aFilePath instanceof Ci.nsIFile) {
Deprecated.warning("Passing an nsIFile to PlacesBackups.saveBookmarksToJSONFile " +
"is deprecated. Please use an OS.File path instead.",
"https://developer.mozilla.org/docs/JavaScript_OS.File");
aFilePath = aFilePath.path;
}
return Task.spawn(function* () {
let nodeCount = yield BookmarkJSONUtils.exportToFile(aFilePath);
let backupFolderPath = yield this.getBackupFolder();
if (aFile.parent.path == backupFolderPath) {
// Update internal cache.
this.entries.push(aFile);
if (OS.Path.dirname(aFilePath) == backupFolderPath) {
// We are creating a backup in the default backups folder,
// so just update the internal cache.
this._entries.unshift(new localFileCtor(aFilePath));
if (!this._backupFiles) {
yield this.getBackupFiles();
}
this._backupFiles.push(aFile.path);
this._backupFiles.unshift(aFilePath);
} else {
// If we are saving to a folder different than our backups folder, then
// we also want to copy this new backup to it.
@ -257,24 +278,20 @@ this.PlacesBackups = {
{ nodeCount: nodeCount });
let newFilePath = OS.Path.join(backupFolderPath, newFilename);
let backupFile = yield this._getBackupFileForSameDate(name);
if (backupFile) {
yield OS.File.remove(backupFile, { ignoreAbsent: true });
} else {
let file = new FileUtils.File(newFilePath);
if (!backupFile) {
// Update internal cache if we are not replacing an existing
// backup file.
this.entries.push(file);
this._entries.unshift(new localFileCtor(newFilePath));
if (!this._backupFiles) {
yield this.getBackupFiles();
}
this._backupFiles.push(file.path);
this._backupFiles.unshift(newFilePath);
}
yield OS.File.copy(aFile.path, newFilePath);
yield OS.File.copy(aFilePath, newFilePath);
}
throw new Task.Result(nodeCount);
return nodeCount;
}.bind(this));
},
@ -292,7 +309,7 @@ this.PlacesBackups = {
* @return {Promise}
*/
create: function PB_create(aMaxBackups, aForceBackup) {
return Task.spawn(function() {
return Task.spawn(function* () {
// Construct the new leafname.
let newBackupFilename = this.getFilenameForDate();
let mostRecentBackupFile = yield this.getMostRecentBackup();
@ -314,7 +331,7 @@ this.PlacesBackups = {
numberOfBackupsToDelete++;
while (numberOfBackupsToDelete--) {
this.entries.pop();
this._entries.pop();
if (!this._backupFiles) {
yield this.getBackupFiles();
}
@ -341,25 +358,22 @@ this.PlacesBackups = {
}
// Save bookmarks to a backup file.
let backupFolderPath = yield this.getBackupFolder();
let backupFolder = new FileUtils.File(backupFolderPath);
let newBackupFile = backupFolder.clone();
newBackupFile.append(newBackupFilename);
let backupFolder = yield this.getBackupFolder();
let newBackupFile = OS.Path.join(backupFolder, newBackupFilename);
let nodeCount = yield this.saveBookmarksToJSONFile(newBackupFile);
// Rename the filename with metadata.
let newFilenameWithMetaData = this._appendMetaDataToFilename(
newBackupFilename,
{ nodeCount: nodeCount });
newBackupFile.moveTo(backupFolder, newFilenameWithMetaData);
let newBackupFileWithMetadata = OS.Path.join(backupFolder, newFilenameWithMetaData);
yield OS.File.move(newBackupFile, newBackupFileWithMetadata);
// Update internal cache.
let newFileWithMetaData = backupFolder.clone();
newFileWithMetaData.append(newFilenameWithMetaData);
this.entries.pop();
this.entries.push(newFileWithMetaData);
let newFileWithMetaData = new localFileCtor(newBackupFileWithMetadata);
this._entries.pop();
this._entries.unshift(newFileWithMetaData);
this._backupFiles.pop();
this._backupFiles.push(newFileWithMetaData.path);
this._backupFiles.unshift(newBackupFileWithMetadata);
}.bind(this));
},
@ -400,22 +414,216 @@ this.PlacesBackups = {
sourceMatches[4] == targetMatches[4]);
},
_getBackupFileForSameDate:
function PB__getBackupFileForSameDate(aFilename) {
return Task.spawn(function() {
let backupFolderPath = yield this.getBackupFolder();
let iterator = new OS.File.DirectoryIterator(backupFolderPath);
let backupFile;
_getBackupFileForSameDate:
function PB__getBackupFileForSameDate(aFilename) {
return Task.spawn(function* () {
let backupFolderPath = yield this.getBackupFolder();
let iterator = new OS.File.DirectoryIterator(backupFolderPath);
let backupFile;
yield iterator.forEach(function(aEntry) {
if (this._isFilenameWithSameDate(aEntry.name, aFilename)) {
backupFile = aEntry.path;
return iterator.close();
}
}.bind(this));
yield iterator.close();
yield iterator.forEach(function(aEntry) {
if (this._isFilenameWithSameDate(aEntry.name, aFilename)) {
backupFile = aEntry.path;
return iterator.close();
}
}.bind(this));
yield iterator.close();
throw new Task.Result(backupFile);
}.bind(this));
}
return backupFile;
}.bind(this));
},
/**
* Gets a bookmarks tree representation usable to create backups in different
* file formats. The root or the tree is PlacesUtils.placesRootId.
* Items annotated with PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO and all of their
* descendants are excluded.
*
* @return an object representing a tree with the places root as its root.
* Each bookmark is represented by an object having these properties:
* * id: the item id (make this not enumerable after bug 824502)
* * title: the title
* * guid: unique id
* * parent: item id of the parent folder, not enumerable
* * index: the position in the parent
* * dateAdded: microseconds from the epoch
* * lastModified: microseconds from the epoch
* * type: type of the originating node as defined in PlacesUtils
* The following properties exist only for a subset of bookmarks:
* * annos: array of annotations
* * uri: url
* * keyword: associated keyword
* * charset: last known charset
* * tags: csv string of tags
* * root: string describing whether this represents a root
* * children: array of child items in a folder
*/
getBookmarksTree: function () {
return Task.spawn(function* () {
let dbFilePath = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite");
let conn = yield Sqlite.openConnection({ path: dbFilePath,
sharedMemoryCache: false });
let rows = [];
try {
rows = yield conn.execute(
"SELECT b.id, h.url, IFNULL(b.title, '') AS title, b.parent, " +
"b.position AS [index], b.type, b.dateAdded, b.lastModified, b.guid, " +
"( SELECT GROUP_CONCAT(t.title, ',') " +
"FROM moz_bookmarks b2 " +
"JOIN moz_bookmarks t ON t.id = +b2.parent AND t.parent = :tags_folder " +
"WHERE b2.fk = h.id " +
") AS tags, " +
"EXISTS (SELECT 1 FROM moz_items_annos WHERE item_id = b.id LIMIT 1) AS has_annos, " +
"( SELECT a.content FROM moz_annos a " +
"JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id " +
"WHERE place_id = h.id AND n.name = :charset_anno " +
") AS charset " +
"FROM moz_bookmarks b " +
"LEFT JOIN moz_bookmarks p ON p.id = b.parent " +
"LEFT JOIN moz_places h ON h.id = b.fk " +
"WHERE b.id <> :tags_folder AND b.parent <> :tags_folder AND p.parent <> :tags_folder " +
"ORDER BY b.parent, b.position",
{ tags_folder: PlacesUtils.tagsFolderId,
charset_anno: PlacesUtils.CHARSET_ANNO });
} catch(e) {
Cu.reportError("Unable to query the database " + e);
} finally {
yield conn.close();
}
let startTime = Date.now();
// Create a Map for lookup and recursive building of the tree.
let itemsMap = new Map();
for (let row of rows) {
let id = row.getResultByName("id");
try {
let bookmark = sqliteRowToBookmarkObject(row);
if (itemsMap.has(id)) {
// Since children may be added before parents, we should merge with
// the existing object.
let original = itemsMap.get(id);
for (prop in bookmark) {
original[prop] = bookmark[prop];
}
bookmark = original;
}
else {
itemsMap.set(id, bookmark);
}
// Append bookmark to its parent.
if (!itemsMap.has(bookmark.parent))
itemsMap.set(bookmark.parent, {});
let parent = itemsMap.get(bookmark.parent);
if (!("children" in parent))
parent.children = [];
parent.children.push(bookmark);
} catch (e) {
Cu.reportError("Error while reading node " + id + " " + e);
}
}
// Handle excluded items, by removing entire subtrees pointed by them.
function removeFromMap(id) {
// Could have been removed by a previous call, since we can't
// predict order of items in EXCLUDE_FROM_BACKUP_ANNO.
if (itemsMap.has(id)) {
let excludedItem = itemsMap.get(id);
if (excludedItem.children) {
for (let child of excludedItem.children) {
removeFromMap(child.id);
}
}
// Remove the excluded item from its parent's children...
let parentItem = itemsMap.get(excludedItem.parent);
parentItem.children = parentItem.children.filter(aChild => aChild.id != id);
// ...then remove it from the map.
itemsMap.delete(id);
}
}
for (let id of PlacesUtils.annotations.getItemsWithAnnotation(
PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO)) {
removeFromMap(id);
}
// Report the time taken to build the tree. This doesn't take into
// account the time spent in the query since that's off the main-thread.
try {
Services.telemetry
.getHistogramById("PLACES_BACKUPS_BOOKMARKSTREE_MS")
.add(Date.now() - startTime);
} catch (ex) {
Components.utils.reportError("Unable to report telemetry.");
}
return [itemsMap.get(PlacesUtils.placesRootId), itemsMap.size];
});
}
}
/**
* Helper function to convert a Sqlite.jsm row to a bookmark object
* representation.
*
* @param aRow The Sqlite.jsm result row.
*/
function sqliteRowToBookmarkObject(aRow) {
let bookmark = {};
for (let p of [ "id" ,"guid", "title", "index", "dateAdded", "lastModified" ]) {
bookmark[p] = aRow.getResultByName(p);
}
Object.defineProperty(bookmark, "parent",
{ value: aRow.getResultByName("parent") });
let type = aRow.getResultByName("type");
// Add annotations.
if (aRow.getResultByName("has_annos")) {
try {
bookmark.annos = PlacesUtils.getAnnotationsForItem(bookmark.id);
} catch (e) {
Cu.reportError("Unexpected error while reading annotations " + e);
}
}
switch (type) {
case Ci.nsINavBookmarksService.TYPE_BOOKMARK:
// TODO: What about shortcuts?
bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE;
// This will throw if we try to serialize an invalid url and the node will
// just be skipped.
bookmark.uri = NetUtil.newURI(aRow.getResultByName("url")).spec;
// Keywords are cached, so this should be decently fast.
let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(bookmark.id);
if (keyword)
bookmark.keyword = keyword;
let charset = aRow.getResultByName("charset");
if (charset)
bookmark.charset = charset;
let tags = aRow.getResultByName("tags");
if (tags)
bookmark.tags = tags;
break;
case Ci.nsINavBookmarksService.TYPE_FOLDER:
bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
// Mark root folders.
if (bookmark.id == PlacesUtils.placesRootId)
bookmark.root = "placesRoot";
else if (bookmark.id == PlacesUtils.bookmarksMenuFolderId)
bookmark.root = "bookmarksMenuFolder";
else if (bookmark.id == PlacesUtils.unfiledBookmarksFolderId)
bookmark.root = "unfiledBookmarksFolder";
else if (bookmark.id == PlacesUtils.toolbarFolderId)
bookmark.root = "toolbarFolder";
break;
case Ci.nsINavBookmarksService.TYPE_SEPARATOR:
bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
break;
default:
Cu.reportError("Unexpected bookmark type");
break;
}
return bookmark;
}

View File

@ -2844,6 +2844,30 @@
"extended_statistics_ok": true,
"description": "PLACES: Number of keywords"
},
"PLACES_BACKUPS_DAYSFROMLAST": {
"expires_in_version": "never",
"kind": "enumerated",
"n_values": 15,
"description": "PLACES: Days from last backup"
},
"PLACES_BACKUPS_BOOKMARKSTREE_MS": {
"expires_in_version": "never",
"kind": "exponential",
"low": 50,
"high": 2000,
"n_buckets": 10,
"extended_statistics_ok": true,
"description": "PLACES: Time to build the bookmarks tree"
},
"PLACES_BACKUPS_TOJSON_MS": {
"expires_in_version": "never",
"kind": "exponential",
"low": 50,
"high": 2000,
"n_buckets": 10,
"extended_statistics_ok": true,
"description": "PLACES: Time to convert and write the backup"
},
"FENNEC_FAVICONS_COUNT": {
"expires_in_version": "never",
"kind": "exponential",