toolkit/components/places/PlacesBackups.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
michael@0 2 * vim: sw=2 ts=2 sts=2 expandtab filetype=javascript
michael@0 3 * This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6
michael@0 7 this.EXPORTED_SYMBOLS = ["PlacesBackups"];
michael@0 8
michael@0 9 const Ci = Components.interfaces;
michael@0 10 const Cu = Components.utils;
michael@0 11 const Cc = Components.classes;
michael@0 12
michael@0 13 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 14 Cu.import("resource://gre/modules/Services.jsm");
michael@0 15 Cu.import("resource://gre/modules/PlacesUtils.jsm");
michael@0 16 Cu.import("resource://gre/modules/Task.jsm");
michael@0 17 Cu.import("resource://gre/modules/osfile.jsm");
michael@0 18 Cu.import("resource://gre/modules/NetUtil.jsm");
michael@0 19
michael@0 20 XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
michael@0 21 "resource://gre/modules/BookmarkJSONUtils.jsm");
michael@0 22 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
michael@0 23 "resource://gre/modules/Deprecated.jsm");
michael@0 24 XPCOMUtils.defineLazyModuleGetter(this, "OS",
michael@0 25 "resource://gre/modules/osfile.jsm");
michael@0 26 XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
michael@0 27 "resource://gre/modules/Sqlite.jsm");
michael@0 28
michael@0 29 XPCOMUtils.defineLazyGetter(this, "localFileCtor",
michael@0 30 () => Components.Constructor("@mozilla.org/file/local;1",
michael@0 31 "nsILocalFile", "initWithPath"));
michael@0 32
michael@0 33 XPCOMUtils.defineLazyGetter(this, "filenamesRegex",
michael@0 34 () => new RegExp("^bookmarks-([0-9\-]+)(?:_([0-9]+)){0,1}(?:_([a-z0-9=\+\-]{24})){0,1}\.(json|html)", "i")
michael@0 35 );
michael@0 36
michael@0 37 /**
michael@0 38 * Appends meta-data information to a given filename.
michael@0 39 */
michael@0 40 function appendMetaDataToFilename(aFilename, aMetaData) {
michael@0 41 let matches = aFilename.match(filenamesRegex);
michael@0 42 return "bookmarks-" + matches[1] +
michael@0 43 "_" + aMetaData.count +
michael@0 44 "_" + aMetaData.hash +
michael@0 45 "." + matches[4];
michael@0 46 }
michael@0 47
michael@0 48 /**
michael@0 49 * Gets the hash from a backup filename.
michael@0 50 *
michael@0 51 * @return the extracted hash or null.
michael@0 52 */
michael@0 53 function getHashFromFilename(aFilename) {
michael@0 54 let matches = aFilename.match(filenamesRegex);
michael@0 55 if (matches && matches[3])
michael@0 56 return matches[3];
michael@0 57 return null;
michael@0 58 }
michael@0 59
michael@0 60 /**
michael@0 61 * Given two filenames, checks if they contain the same date.
michael@0 62 */
michael@0 63 function isFilenameWithSameDate(aSourceName, aTargetName) {
michael@0 64 let sourceMatches = aSourceName.match(filenamesRegex);
michael@0 65 let targetMatches = aTargetName.match(filenamesRegex);
michael@0 66
michael@0 67 return sourceMatches && targetMatches &&
michael@0 68 sourceMatches[1] == targetMatches[1] &&
michael@0 69 sourceMatches[4] == targetMatches[4];
michael@0 70 }
michael@0 71
michael@0 72 /**
michael@0 73 * Given a filename, searches for another backup with the same date.
michael@0 74 *
michael@0 75 * @return OS.File path string or null.
michael@0 76 */
michael@0 77 function getBackupFileForSameDate(aFilename) {
michael@0 78 return Task.spawn(function* () {
michael@0 79 let backupFiles = yield PlacesBackups.getBackupFiles();
michael@0 80 for (let backupFile of backupFiles) {
michael@0 81 if (isFilenameWithSameDate(OS.Path.basename(backupFile), aFilename))
michael@0 82 return backupFile;
michael@0 83 }
michael@0 84 return null;
michael@0 85 });
michael@0 86 }
michael@0 87
michael@0 88 this.PlacesBackups = {
michael@0 89 /**
michael@0 90 * Matches the backup filename:
michael@0 91 * 0: file name
michael@0 92 * 1: date in form Y-m-d
michael@0 93 * 2: bookmarks count
michael@0 94 * 3: contents hash
michael@0 95 * 4: file extension
michael@0 96 */
michael@0 97 get filenamesRegex() filenamesRegex,
michael@0 98
michael@0 99 get folder() {
michael@0 100 Deprecated.warning(
michael@0 101 "PlacesBackups.folder is deprecated and will be removed in a future version",
michael@0 102 "https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
michael@0 103 return this._folder;
michael@0 104 },
michael@0 105
michael@0 106 /**
michael@0 107 * This exists just to avoid spamming deprecate warnings from internal calls
michael@0 108 * needed to support deprecated methods themselves.
michael@0 109 */
michael@0 110 get _folder() {
michael@0 111 let bookmarksBackupDir = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
michael@0 112 bookmarksBackupDir.append(this.profileRelativeFolderPath);
michael@0 113 if (!bookmarksBackupDir.exists()) {
michael@0 114 bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0700", 8));
michael@0 115 if (!bookmarksBackupDir.exists())
michael@0 116 throw("Unable to create bookmarks backup folder");
michael@0 117 }
michael@0 118 delete this._folder;
michael@0 119 return this._folder = bookmarksBackupDir;
michael@0 120 },
michael@0 121
michael@0 122 /**
michael@0 123 * Gets backup folder asynchronously.
michael@0 124 * @return {Promise}
michael@0 125 * @resolve the folder (the folder string path).
michael@0 126 */
michael@0 127 getBackupFolder: function PB_getBackupFolder() {
michael@0 128 return Task.spawn(function* () {
michael@0 129 if (this._backupFolder) {
michael@0 130 return this._backupFolder;
michael@0 131 }
michael@0 132 let profileDir = OS.Constants.Path.profileDir;
michael@0 133 let backupsDirPath = OS.Path.join(profileDir, this.profileRelativeFolderPath);
michael@0 134 yield OS.File.makeDir(backupsDirPath, { ignoreExisting: true });
michael@0 135 return this._backupFolder = backupsDirPath;
michael@0 136 }.bind(this));
michael@0 137 },
michael@0 138
michael@0 139 get profileRelativeFolderPath() "bookmarkbackups",
michael@0 140
michael@0 141 /**
michael@0 142 * Cache current backups in a sorted (by date DESC) array.
michael@0 143 */
michael@0 144 get entries() {
michael@0 145 Deprecated.warning(
michael@0 146 "PlacesBackups.entries is deprecated and will be removed in a future version",
michael@0 147 "https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
michael@0 148 return this._entries;
michael@0 149 },
michael@0 150
michael@0 151 /**
michael@0 152 * This exists just to avoid spamming deprecate warnings from internal calls
michael@0 153 * needed to support deprecated methods themselves.
michael@0 154 */
michael@0 155 get _entries() {
michael@0 156 delete this._entries;
michael@0 157 this._entries = [];
michael@0 158 let files = this._folder.directoryEntries;
michael@0 159 while (files.hasMoreElements()) {
michael@0 160 let entry = files.getNext().QueryInterface(Ci.nsIFile);
michael@0 161 // A valid backup is any file that matches either the localized or
michael@0 162 // not-localized filename (bug 445704).
michael@0 163 if (!entry.isHidden() && filenamesRegex.test(entry.leafName)) {
michael@0 164 // Remove bogus backups in future dates.
michael@0 165 if (this.getDateForFile(entry) > new Date()) {
michael@0 166 entry.remove(false);
michael@0 167 continue;
michael@0 168 }
michael@0 169 this._entries.push(entry);
michael@0 170 }
michael@0 171 }
michael@0 172 this._entries.sort((a, b) => {
michael@0 173 let aDate = this.getDateForFile(a);
michael@0 174 let bDate = this.getDateForFile(b);
michael@0 175 return aDate < bDate ? 1 : aDate > bDate ? -1 : 0;
michael@0 176 });
michael@0 177 return this._entries;
michael@0 178 },
michael@0 179
michael@0 180 /**
michael@0 181 * Cache current backups in a sorted (by date DESC) array.
michael@0 182 * @return {Promise}
michael@0 183 * @resolve a sorted array of string paths.
michael@0 184 */
michael@0 185 getBackupFiles: function PB_getBackupFiles() {
michael@0 186 return Task.spawn(function* () {
michael@0 187 if (this._backupFiles)
michael@0 188 return this._backupFiles;
michael@0 189
michael@0 190 this._backupFiles = [];
michael@0 191
michael@0 192 let backupFolderPath = yield this.getBackupFolder();
michael@0 193 let iterator = new OS.File.DirectoryIterator(backupFolderPath);
michael@0 194 yield iterator.forEach(function(aEntry) {
michael@0 195 // Since this is a lazy getter and OS.File I/O is serialized, we can
michael@0 196 // safely remove .tmp files without risking to remove ongoing backups.
michael@0 197 if (aEntry.name.endsWith(".tmp")) {
michael@0 198 OS.File.remove(aEntry.path);
michael@0 199 return;
michael@0 200 }
michael@0 201
michael@0 202 if (filenamesRegex.test(aEntry.name)) {
michael@0 203 // Remove bogus backups in future dates.
michael@0 204 let filePath = aEntry.path;
michael@0 205 if (this.getDateForFile(filePath) > new Date()) {
michael@0 206 return OS.File.remove(filePath);
michael@0 207 } else {
michael@0 208 this._backupFiles.push(filePath);
michael@0 209 }
michael@0 210 }
michael@0 211 }.bind(this));
michael@0 212 iterator.close();
michael@0 213
michael@0 214 this._backupFiles.sort((a, b) => {
michael@0 215 let aDate = this.getDateForFile(a);
michael@0 216 let bDate = this.getDateForFile(b);
michael@0 217 return aDate < bDate ? 1 : aDate > bDate ? -1 : 0;
michael@0 218 });
michael@0 219
michael@0 220 return this._backupFiles;
michael@0 221 }.bind(this));
michael@0 222 },
michael@0 223
michael@0 224 /**
michael@0 225 * Creates a filename for bookmarks backup files.
michael@0 226 *
michael@0 227 * @param [optional] aDateObj
michael@0 228 * Date object used to build the filename.
michael@0 229 * Will use current date if empty.
michael@0 230 * @return A bookmarks backup filename.
michael@0 231 */
michael@0 232 getFilenameForDate: function PB_getFilenameForDate(aDateObj) {
michael@0 233 let dateObj = aDateObj || new Date();
michael@0 234 // Use YYYY-MM-DD (ISO 8601) as it doesn't contain illegal characters
michael@0 235 // and makes the alphabetical order of multiple backup files more useful.
michael@0 236 return "bookmarks-" + dateObj.toLocaleFormat("%Y-%m-%d") + ".json";
michael@0 237 },
michael@0 238
michael@0 239 /**
michael@0 240 * Creates a Date object from a backup file. The date is the backup
michael@0 241 * creation date.
michael@0 242 *
michael@0 243 * @param aBackupFile
michael@0 244 * nsIFile or string path of the backup.
michael@0 245 * @return A Date object for the backup's creation time.
michael@0 246 */
michael@0 247 getDateForFile: function PB_getDateForFile(aBackupFile) {
michael@0 248 let filename = (aBackupFile instanceof Ci.nsIFile) ? aBackupFile.leafName
michael@0 249 : OS.Path.basename(aBackupFile);
michael@0 250 let matches = filename.match(filenamesRegex);
michael@0 251 if (!matches)
michael@0 252 throw("Invalid backup file name: " + filename);
michael@0 253 return new Date(matches[1].replace(/-/g, "/"));
michael@0 254 },
michael@0 255
michael@0 256 /**
michael@0 257 * Get the most recent backup file.
michael@0 258 *
michael@0 259 * @param [optional] aFileExt
michael@0 260 * Force file extension. Either "html" or "json".
michael@0 261 * Will check for both if not defined.
michael@0 262 * @returns nsIFile backup file
michael@0 263 */
michael@0 264 getMostRecent: function PB_getMostRecent(aFileExt) {
michael@0 265 Deprecated.warning(
michael@0 266 "PlacesBackups.getMostRecent is deprecated and will be removed in a future version",
michael@0 267 "https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
michael@0 268
michael@0 269 let fileExt = aFileExt || "(json|html)";
michael@0 270 for (let i = 0; i < this._entries.length; i++) {
michael@0 271 let rx = new RegExp("\." + fileExt + "$");
michael@0 272 if (this._entries[i].leafName.match(rx))
michael@0 273 return this._entries[i];
michael@0 274 }
michael@0 275 return null;
michael@0 276 },
michael@0 277
michael@0 278 /**
michael@0 279 * Get the most recent backup file.
michael@0 280 *
michael@0 281 * @param [optional] aFileExt
michael@0 282 * Force file extension. Either "html" or "json".
michael@0 283 * Will check for both if not defined.
michael@0 284 * @return {Promise}
michael@0 285 * @result the path to the file.
michael@0 286 */
michael@0 287 getMostRecentBackup: function PB_getMostRecentBackup(aFileExt) {
michael@0 288 return Task.spawn(function* () {
michael@0 289 let fileExt = aFileExt || "(json|html)";
michael@0 290 let entries = yield this.getBackupFiles();
michael@0 291 for (let entry of entries) {
michael@0 292 let rx = new RegExp("\." + fileExt + "$");
michael@0 293 if (OS.Path.basename(entry).match(rx)) {
michael@0 294 return entry;
michael@0 295 }
michael@0 296 }
michael@0 297 return null;
michael@0 298 }.bind(this));
michael@0 299 },
michael@0 300
michael@0 301 /**
michael@0 302 * Serializes bookmarks using JSON, and writes to the supplied file.
michael@0 303 * Note: any item that should not be backed up must be annotated with
michael@0 304 * "places/excludeFromBackup".
michael@0 305 *
michael@0 306 * @param aFilePath
michael@0 307 * OS.File path for the "bookmarks.json" file to be created.
michael@0 308 * @return {Promise}
michael@0 309 * @resolves the number of serialized uri nodes.
michael@0 310 * @deprecated passing an nsIFile is deprecated
michael@0 311 */
michael@0 312 saveBookmarksToJSONFile: function PB_saveBookmarksToJSONFile(aFilePath) {
michael@0 313 if (aFilePath instanceof Ci.nsIFile) {
michael@0 314 Deprecated.warning("Passing an nsIFile to PlacesBackups.saveBookmarksToJSONFile " +
michael@0 315 "is deprecated. Please use an OS.File path instead.",
michael@0 316 "https://developer.mozilla.org/docs/JavaScript_OS.File");
michael@0 317 aFilePath = aFilePath.path;
michael@0 318 }
michael@0 319 return Task.spawn(function* () {
michael@0 320 let { count: nodeCount, hash: hash } =
michael@0 321 yield BookmarkJSONUtils.exportToFile(aFilePath);
michael@0 322
michael@0 323 let backupFolderPath = yield this.getBackupFolder();
michael@0 324 if (OS.Path.dirname(aFilePath) == backupFolderPath) {
michael@0 325 // We are creating a backup in the default backups folder,
michael@0 326 // so just update the internal cache.
michael@0 327 this._entries.unshift(new localFileCtor(aFilePath));
michael@0 328 if (!this._backupFiles) {
michael@0 329 yield this.getBackupFiles();
michael@0 330 }
michael@0 331 this._backupFiles.unshift(aFilePath);
michael@0 332 } else {
michael@0 333 // If we are saving to a folder different than our backups folder, then
michael@0 334 // we also want to copy this new backup to it.
michael@0 335 // This way we ensure the latest valid backup is the same saved by the
michael@0 336 // user. See bug 424389.
michael@0 337 let mostRecentBackupFile = yield this.getMostRecentBackup("json");
michael@0 338 if (!mostRecentBackupFile ||
michael@0 339 hash != getHashFromFilename(OS.Path.basename(mostRecentBackupFile))) {
michael@0 340 let name = this.getFilenameForDate();
michael@0 341 let newFilename = appendMetaDataToFilename(name,
michael@0 342 { count: nodeCount,
michael@0 343 hash: hash });
michael@0 344 let newFilePath = OS.Path.join(backupFolderPath, newFilename);
michael@0 345 let backupFile = yield getBackupFileForSameDate(name);
michael@0 346 if (backupFile) {
michael@0 347 // There is already a backup for today, replace it.
michael@0 348 yield OS.File.remove(backupFile, { ignoreAbsent: true });
michael@0 349 if (!this._backupFiles)
michael@0 350 yield this.getBackupFiles();
michael@0 351 else
michael@0 352 this._backupFiles.shift();
michael@0 353 this._backupFiles.unshift(newFilePath);
michael@0 354 } else {
michael@0 355 // There is no backup for today, add the new one.
michael@0 356 this._entries.unshift(new localFileCtor(newFilePath));
michael@0 357 if (!this._backupFiles)
michael@0 358 yield this.getBackupFiles();
michael@0 359 this._backupFiles.unshift(newFilePath);
michael@0 360 }
michael@0 361
michael@0 362 yield OS.File.copy(aFilePath, newFilePath);
michael@0 363 }
michael@0 364 }
michael@0 365
michael@0 366 return nodeCount;
michael@0 367 }.bind(this));
michael@0 368 },
michael@0 369
michael@0 370 /**
michael@0 371 * Creates a dated backup in <profile>/bookmarkbackups.
michael@0 372 * Stores the bookmarks using JSON.
michael@0 373 * Note: any item that should not be backed up must be annotated with
michael@0 374 * "places/excludeFromBackup".
michael@0 375 *
michael@0 376 * @param [optional] int aMaxBackups
michael@0 377 * The maximum number of backups to keep. If set to 0
michael@0 378 * all existing backups are removed and aForceBackup is
michael@0 379 * ignored, so a new one won't be created.
michael@0 380 * @param [optional] bool aForceBackup
michael@0 381 * Forces creating a backup even if one was already
michael@0 382 * created that day (overwrites).
michael@0 383 * @return {Promise}
michael@0 384 */
michael@0 385 create: function PB_create(aMaxBackups, aForceBackup) {
michael@0 386 let limitBackups = function* () {
michael@0 387 let backupFiles = yield this.getBackupFiles();
michael@0 388 if (typeof aMaxBackups == "number" && aMaxBackups > -1 &&
michael@0 389 backupFiles.length >= aMaxBackups) {
michael@0 390 let numberOfBackupsToDelete = backupFiles.length - aMaxBackups;
michael@0 391 while (numberOfBackupsToDelete--) {
michael@0 392 this._entries.pop();
michael@0 393 let oldestBackup = this._backupFiles.pop();
michael@0 394 yield OS.File.remove(oldestBackup);
michael@0 395 }
michael@0 396 }
michael@0 397 }.bind(this);
michael@0 398
michael@0 399 return Task.spawn(function* () {
michael@0 400 if (aMaxBackups === 0) {
michael@0 401 // Backups are disabled, delete any existing one and bail out.
michael@0 402 yield limitBackups(0);
michael@0 403 return;
michael@0 404 }
michael@0 405
michael@0 406 // Ensure to initialize _backupFiles
michael@0 407 if (!this._backupFiles)
michael@0 408 yield this.getBackupFiles();
michael@0 409 let newBackupFilename = this.getFilenameForDate();
michael@0 410 // If we already have a backup for today we should do nothing, unless we
michael@0 411 // were required to enforce a new backup.
michael@0 412 let backupFile = yield getBackupFileForSameDate(newBackupFilename);
michael@0 413 if (backupFile && !aForceBackup)
michael@0 414 return;
michael@0 415
michael@0 416 if (backupFile) {
michael@0 417 // In case there is a backup for today we should recreate it.
michael@0 418 this._backupFiles.shift();
michael@0 419 this._entries.shift();
michael@0 420 yield OS.File.remove(backupFile, { ignoreAbsent: true });
michael@0 421 }
michael@0 422
michael@0 423 // Now check the hash of the most recent backup, and try to create a new
michael@0 424 // backup, if that fails due to hash conflict, just rename the old backup.
michael@0 425 let mostRecentBackupFile = yield this.getMostRecentBackup();
michael@0 426 let mostRecentHash = mostRecentBackupFile &&
michael@0 427 getHashFromFilename(OS.Path.basename(mostRecentBackupFile));
michael@0 428
michael@0 429 // Save bookmarks to a backup file.
michael@0 430 let backupFolder = yield this.getBackupFolder();
michael@0 431 let newBackupFile = OS.Path.join(backupFolder, newBackupFilename);
michael@0 432 let newFilenameWithMetaData;
michael@0 433 try {
michael@0 434 let { count: nodeCount, hash: hash } =
michael@0 435 yield BookmarkJSONUtils.exportToFile(newBackupFile,
michael@0 436 { failIfHashIs: mostRecentHash });
michael@0 437 newFilenameWithMetaData = appendMetaDataToFilename(newBackupFilename,
michael@0 438 { count: nodeCount,
michael@0 439 hash: hash });
michael@0 440 } catch (ex if ex.becauseSameHash) {
michael@0 441 // The last backup already contained up-to-date information, just
michael@0 442 // rename it as if it was today's backup.
michael@0 443 this._backupFiles.shift();
michael@0 444 this._entries.shift();
michael@0 445 newBackupFile = mostRecentBackupFile;
michael@0 446 newFilenameWithMetaData = appendMetaDataToFilename(
michael@0 447 newBackupFilename,
michael@0 448 { count: this.getBookmarkCountForFile(mostRecentBackupFile),
michael@0 449 hash: mostRecentHash });
michael@0 450 }
michael@0 451
michael@0 452 // Append metadata to the backup filename.
michael@0 453 let newBackupFileWithMetadata = OS.Path.join(backupFolder, newFilenameWithMetaData);
michael@0 454 yield OS.File.move(newBackupFile, newBackupFileWithMetadata);
michael@0 455 this._entries.unshift(new localFileCtor(newBackupFileWithMetadata));
michael@0 456 this._backupFiles.unshift(newBackupFileWithMetadata);
michael@0 457
michael@0 458 // Limit the number of backups.
michael@0 459 yield limitBackups(aMaxBackups);
michael@0 460 }.bind(this));
michael@0 461 },
michael@0 462
michael@0 463 /**
michael@0 464 * Gets the bookmark count for backup file.
michael@0 465 *
michael@0 466 * @param aFilePath
michael@0 467 * File path The backup file.
michael@0 468 *
michael@0 469 * @return the bookmark count or null.
michael@0 470 */
michael@0 471 getBookmarkCountForFile: function PB_getBookmarkCountForFile(aFilePath) {
michael@0 472 let count = null;
michael@0 473 let filename = OS.Path.basename(aFilePath);
michael@0 474 let matches = filename.match(filenamesRegex);
michael@0 475 if (matches && matches[2])
michael@0 476 count = matches[2];
michael@0 477 return count;
michael@0 478 },
michael@0 479
michael@0 480 /**
michael@0 481 * Gets a bookmarks tree representation usable to create backups in different
michael@0 482 * file formats. The root or the tree is PlacesUtils.placesRootId.
michael@0 483 * Items annotated with PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO and all of their
michael@0 484 * descendants are excluded.
michael@0 485 *
michael@0 486 * @return an object representing a tree with the places root as its root.
michael@0 487 * Each bookmark is represented by an object having these properties:
michael@0 488 * * id: the item id (make this not enumerable after bug 824502)
michael@0 489 * * title: the title
michael@0 490 * * guid: unique id
michael@0 491 * * parent: item id of the parent folder, not enumerable
michael@0 492 * * index: the position in the parent
michael@0 493 * * dateAdded: microseconds from the epoch
michael@0 494 * * lastModified: microseconds from the epoch
michael@0 495 * * type: type of the originating node as defined in PlacesUtils
michael@0 496 * The following properties exist only for a subset of bookmarks:
michael@0 497 * * annos: array of annotations
michael@0 498 * * uri: url
michael@0 499 * * iconuri: favicon's url
michael@0 500 * * keyword: associated keyword
michael@0 501 * * charset: last known charset
michael@0 502 * * tags: csv string of tags
michael@0 503 * * root: string describing whether this represents a root
michael@0 504 * * children: array of child items in a folder
michael@0 505 */
michael@0 506 getBookmarksTree: function () {
michael@0 507 return Task.spawn(function* () {
michael@0 508 let dbFilePath = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite");
michael@0 509 let conn = yield Sqlite.openConnection({ path: dbFilePath,
michael@0 510 sharedMemoryCache: false });
michael@0 511 let rows = [];
michael@0 512 try {
michael@0 513 rows = yield conn.execute(
michael@0 514 "SELECT b.id, h.url, IFNULL(b.title, '') AS title, b.parent, " +
michael@0 515 "b.position AS [index], b.type, b.dateAdded, b.lastModified, " +
michael@0 516 "b.guid, f.url AS iconuri, " +
michael@0 517 "( SELECT GROUP_CONCAT(t.title, ',') " +
michael@0 518 "FROM moz_bookmarks b2 " +
michael@0 519 "JOIN moz_bookmarks t ON t.id = +b2.parent AND t.parent = :tags_folder " +
michael@0 520 "WHERE b2.fk = h.id " +
michael@0 521 ") AS tags, " +
michael@0 522 "EXISTS (SELECT 1 FROM moz_items_annos WHERE item_id = b.id LIMIT 1) AS has_annos, " +
michael@0 523 "( SELECT a.content FROM moz_annos a " +
michael@0 524 "JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id " +
michael@0 525 "WHERE place_id = h.id AND n.name = :charset_anno " +
michael@0 526 ") AS charset " +
michael@0 527 "FROM moz_bookmarks b " +
michael@0 528 "LEFT JOIN moz_bookmarks p ON p.id = b.parent " +
michael@0 529 "LEFT JOIN moz_places h ON h.id = b.fk " +
michael@0 530 "LEFT JOIN moz_favicons f ON f.id = h.favicon_id " +
michael@0 531 "WHERE b.id <> :tags_folder AND b.parent <> :tags_folder AND p.parent <> :tags_folder " +
michael@0 532 "ORDER BY b.parent, b.position",
michael@0 533 { tags_folder: PlacesUtils.tagsFolderId,
michael@0 534 charset_anno: PlacesUtils.CHARSET_ANNO });
michael@0 535 } catch(e) {
michael@0 536 Cu.reportError("Unable to query the database " + e);
michael@0 537 } finally {
michael@0 538 yield conn.close();
michael@0 539 }
michael@0 540
michael@0 541 let startTime = Date.now();
michael@0 542 // Create a Map for lookup and recursive building of the tree.
michael@0 543 let itemsMap = new Map();
michael@0 544 for (let row of rows) {
michael@0 545 let id = row.getResultByName("id");
michael@0 546 try {
michael@0 547 let bookmark = sqliteRowToBookmarkObject(row);
michael@0 548 if (itemsMap.has(id)) {
michael@0 549 // Since children may be added before parents, we should merge with
michael@0 550 // the existing object.
michael@0 551 let original = itemsMap.get(id);
michael@0 552 for (let prop of Object.getOwnPropertyNames(bookmark)) {
michael@0 553 original[prop] = bookmark[prop];
michael@0 554 }
michael@0 555 bookmark = original;
michael@0 556 }
michael@0 557 else {
michael@0 558 itemsMap.set(id, bookmark);
michael@0 559 }
michael@0 560
michael@0 561 // Append bookmark to its parent.
michael@0 562 if (!itemsMap.has(bookmark.parent))
michael@0 563 itemsMap.set(bookmark.parent, {});
michael@0 564 let parent = itemsMap.get(bookmark.parent);
michael@0 565 if (!("children" in parent))
michael@0 566 parent.children = [];
michael@0 567 parent.children.push(bookmark);
michael@0 568 } catch (e) {
michael@0 569 Cu.reportError("Error while reading node " + id + " " + e);
michael@0 570 }
michael@0 571 }
michael@0 572
michael@0 573 // Handle excluded items, by removing entire subtrees pointed by them.
michael@0 574 function removeFromMap(id) {
michael@0 575 // Could have been removed by a previous call, since we can't
michael@0 576 // predict order of items in EXCLUDE_FROM_BACKUP_ANNO.
michael@0 577 if (itemsMap.has(id)) {
michael@0 578 let excludedItem = itemsMap.get(id);
michael@0 579 if (excludedItem.children) {
michael@0 580 for (let child of excludedItem.children) {
michael@0 581 removeFromMap(child.id);
michael@0 582 }
michael@0 583 }
michael@0 584 // Remove the excluded item from its parent's children...
michael@0 585 let parentItem = itemsMap.get(excludedItem.parent);
michael@0 586 parentItem.children = parentItem.children.filter(aChild => aChild.id != id);
michael@0 587 // ...then remove it from the map.
michael@0 588 itemsMap.delete(id);
michael@0 589 }
michael@0 590 }
michael@0 591
michael@0 592 for (let id of PlacesUtils.annotations.getItemsWithAnnotation(
michael@0 593 PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO)) {
michael@0 594 removeFromMap(id);
michael@0 595 }
michael@0 596
michael@0 597 // Report the time taken to build the tree. This doesn't take into
michael@0 598 // account the time spent in the query since that's off the main-thread.
michael@0 599 try {
michael@0 600 Services.telemetry
michael@0 601 .getHistogramById("PLACES_BACKUPS_BOOKMARKSTREE_MS")
michael@0 602 .add(Date.now() - startTime);
michael@0 603 } catch (ex) {
michael@0 604 Components.utils.reportError("Unable to report telemetry.");
michael@0 605 }
michael@0 606
michael@0 607 return [itemsMap.get(PlacesUtils.placesRootId), itemsMap.size];
michael@0 608 });
michael@0 609 }
michael@0 610 }
michael@0 611
michael@0 612 /**
michael@0 613 * Helper function to convert a Sqlite.jsm row to a bookmark object
michael@0 614 * representation.
michael@0 615 *
michael@0 616 * @param aRow The Sqlite.jsm result row.
michael@0 617 */
michael@0 618 function sqliteRowToBookmarkObject(aRow) {
michael@0 619 let bookmark = {};
michael@0 620 for (let p of [ "id" ,"guid", "title", "index", "dateAdded", "lastModified" ]) {
michael@0 621 bookmark[p] = aRow.getResultByName(p);
michael@0 622 }
michael@0 623 Object.defineProperty(bookmark, "parent",
michael@0 624 { value: aRow.getResultByName("parent") });
michael@0 625
michael@0 626 let type = aRow.getResultByName("type");
michael@0 627
michael@0 628 // Add annotations.
michael@0 629 if (aRow.getResultByName("has_annos")) {
michael@0 630 try {
michael@0 631 bookmark.annos = PlacesUtils.getAnnotationsForItem(bookmark.id);
michael@0 632 } catch (e) {
michael@0 633 Cu.reportError("Unexpected error while reading annotations " + e);
michael@0 634 }
michael@0 635 }
michael@0 636
michael@0 637 switch (type) {
michael@0 638 case Ci.nsINavBookmarksService.TYPE_BOOKMARK:
michael@0 639 // TODO: What about shortcuts?
michael@0 640 bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE;
michael@0 641 // This will throw if we try to serialize an invalid url and the node will
michael@0 642 // just be skipped.
michael@0 643 bookmark.uri = NetUtil.newURI(aRow.getResultByName("url")).spec;
michael@0 644 // Keywords are cached, so this should be decently fast.
michael@0 645 let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(bookmark.id);
michael@0 646 if (keyword)
michael@0 647 bookmark.keyword = keyword;
michael@0 648 let charset = aRow.getResultByName("charset");
michael@0 649 if (charset)
michael@0 650 bookmark.charset = charset;
michael@0 651 let tags = aRow.getResultByName("tags");
michael@0 652 if (tags)
michael@0 653 bookmark.tags = tags;
michael@0 654 let iconuri = aRow.getResultByName("iconuri");
michael@0 655 if (iconuri)
michael@0 656 bookmark.iconuri = iconuri;
michael@0 657 break;
michael@0 658 case Ci.nsINavBookmarksService.TYPE_FOLDER:
michael@0 659 bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
michael@0 660
michael@0 661 // Mark root folders.
michael@0 662 if (bookmark.id == PlacesUtils.placesRootId)
michael@0 663 bookmark.root = "placesRoot";
michael@0 664 else if (bookmark.id == PlacesUtils.bookmarksMenuFolderId)
michael@0 665 bookmark.root = "bookmarksMenuFolder";
michael@0 666 else if (bookmark.id == PlacesUtils.unfiledBookmarksFolderId)
michael@0 667 bookmark.root = "unfiledBookmarksFolder";
michael@0 668 else if (bookmark.id == PlacesUtils.toolbarFolderId)
michael@0 669 bookmark.root = "toolbarFolder";
michael@0 670 break;
michael@0 671 case Ci.nsINavBookmarksService.TYPE_SEPARATOR:
michael@0 672 bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
michael@0 673 break;
michael@0 674 default:
michael@0 675 Cu.reportError("Unexpected bookmark type");
michael@0 676 break;
michael@0 677 }
michael@0 678 return bookmark;
michael@0 679 }

mercurial