mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-11 12:25:53 +00:00
Bug 824433 - Bookmarks backup takes a long time to write out on shutdown. r=mano
This commit is contained in:
parent
239a6f697a
commit
3504ba0f46
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
@ -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"
|
||||
];
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user