Bug 818593 - Add file size to bookmarks restore UI. r=mak
authorRaymond Lee <raymond@raysquare.com>
Wed, 31 Jul 2013 22:51:09 +0800
changeset 153221 1788bbaa0f6adfd096aefe87ac92359ef43afa32
parent 153220 80b6d90df8311f54af3c8497de57366985236f63
child 153222 03a9b0dead08d1a99a48286ec187ee4b259d6cb7
push id2859
push userakeybl@mozilla.com
push dateMon, 16 Sep 2013 19:14:59 +0000
treeherdermozilla-beta@87d3c51cd2bf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs818593
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 818593 - Add file size to bookmarks restore UI. r=mak
browser/components/places/content/places.js
browser/components/places/tests/unit/test_browserGlue_corrupt.js
browser/components/places/tests/unit/test_browserGlue_prefs.js
browser/components/places/tests/unit/test_browserGlue_restore.js
browser/components/places/tests/unit/test_browserGlue_shutdown.js
browser/components/places/tests/unit/test_clearHistory_shutdown.js
toolkit/components/places/BookmarkJSONUtils.jsm
toolkit/components/places/PlacesBackups.jsm
toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js
toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js
toolkit/components/places/tests/bookmarks/test_818593-store-backup-metadata.js
toolkit/components/places/tests/bookmarks/xpcshell.ini
toolkit/components/places/tests/head_common.js
toolkit/components/places/tests/unit/test_utils_backups_create.js
toolkit/locales/en-US/chrome/places/places.properties
--- a/browser/components/places/content/places.js
+++ b/browser/components/places/content/places.js
@@ -6,16 +6,18 @@
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource:///modules/MigrationUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
                                   "resource://gre/modules/BookmarkJSONUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
                                   "resource://gre/modules/PlacesBackups.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
+                                  "resource://gre/modules/DownloadUtils.jsm");
 
 var PlacesOrganizer = {
   _places: null,
 
   // IDs of fields from editBookmarkOverlay that should be hidden when infoBox
   // is minimal. IDs should be kept in sync with the IDs of the elements
   // observing additionalInfoBroadcaster.
   _additionalInfoFields: [
@@ -406,25 +408,41 @@ var PlacesOrganizer = {
       restorePopup.removeChild(restorePopup.firstChild);
 
     let backupFiles = PlacesBackups.entries;
     if (backupFiles.length == 0)
       return;
 
     // Populate menu with backups.
     for (let i = 0; i < backupFiles.length; i++) {
+      let [size, unit] = DownloadUtils.convertByteUnits(backupFiles[i].fileSize);
+      let sizeString = PlacesUtils.getFormattedString("backupFileSizeText",
+                                                      [size, unit]);
+      let sizeInfo;
+      let bookmarkCount = PlacesBackups.getBookmarkCountForFile(backupFiles[i]);
+      if (bookmarkCount != null) {
+        sizeInfo = " (" + sizeString + " - " +
+                   PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
+                                                  bookmarkCount,
+                                                  [bookmarkCount]) +
+                   ")";
+      } else {
+        sizeInfo = " (" + sizeString + ")";
+      }
+
       let backupDate = PlacesBackups.getDateForFile(backupFiles[i]);
       let m = restorePopup.insertBefore(document.createElement("menuitem"),
                                         document.getElementById("restoreFromFile"));
       m.setAttribute("label",
                      dateSvc.FormatDate("",
                                         Ci.nsIScriptableDateFormat.dateFormatLong,
                                         backupDate.getFullYear(),
                                         backupDate.getMonth() + 1,
-                                        backupDate.getDate()));
+                                        backupDate.getDate()) +
+                                        sizeInfo);
       m.setAttribute("value", backupFiles[i].leafName);
       m.setAttribute("oncommand",
                      "PlacesOrganizer.onRestoreMenuItemClick(this);");
     }
 
     // Add the restoreFromFile item.
     restorePopup.insertBefore(document.createElement("menuseparator"),
                               document.getElementById("restoreFromFile"));
@@ -511,17 +529,17 @@ var PlacesOrganizer = {
    */
   backupBookmarks: function PO_backupBookmarks() {
     let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
                  getService(Ci.nsIProperties);
     let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile);
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
     let fpCallback = function fpCallback_done(aResult) {
       if (aResult != Ci.nsIFilePicker.returnCancel) {
-        BookmarkJSONUtils.exportToFile(fp.file);
+        PlacesBackups.saveBookmarksToJSONFile(fp.file);
       }
     };
 
     fp.init(window, PlacesUIUtils.getString("bookmarksBackupTitle"),
             Ci.nsIFilePicker.modeSave);
     fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
                     PlacesUIUtils.getString("bookmarksRestoreFilterExtension"));
     fp.defaultString = PlacesBackups.getFilenameForDate();
--- a/browser/components/places/tests/unit/test_browserGlue_corrupt.js
+++ b/browser/components/places/tests/unit/test_browserGlue_corrupt.js
@@ -37,16 +37,17 @@ let bookmarksObserver = {
 function run_test() {
   do_test_pending();
 
   // Create our bookmarks.html copying bookmarks.glue.html to the profile
   // folder.  It should be ignored.
   create_bookmarks_html("bookmarks.glue.html");
 
   // Create our JSON backup copying bookmarks.glue.json to the profile folder.
+  remove_all_JSON_backups();
   create_JSON_backup("bookmarks.glue.json");
 
   // Remove current database file.
   let db = gProfD.clone();
   db.append("places.sqlite");
   if (db.exists()) {
     db.remove(false);
     do_check_false(db.exists());
--- a/browser/components/places/tests/unit/test_browserGlue_prefs.js
+++ b/browser/components/places/tests/unit/test_browserGlue_prefs.js
@@ -270,13 +270,14 @@ do_register_cleanup(function () {
   remove_bookmarks_html();
   remove_all_JSON_backups();
 });
 
 function run_test()
 {
   // Create our bookmarks.html from bookmarks.glue.html.
   create_bookmarks_html("bookmarks.glue.html");
+  remove_all_JSON_backups();
   // Create our JSON backup from bookmarks.glue.json.
   create_JSON_backup("bookmarks.glue.json");
 
   run_next_test();
 }
--- a/browser/components/places/tests/unit/test_browserGlue_restore.js
+++ b/browser/components/places/tests/unit/test_browserGlue_restore.js
@@ -38,16 +38,17 @@ function run_test() {
   do_test_pending();
 
   // Create our bookmarks.html copying bookmarks.glue.html to the profile
   // folder.  It will be ignored.
   create_bookmarks_html("bookmarks.glue.html");
 
   // Create our JSON backup copying bookmarks.glue.json to the profile
   // folder.  It will be ignored.
+  remove_all_JSON_backups();
   create_JSON_backup("bookmarks.glue.json");
 
   // Remove current database file.
   let db = gProfD.clone();
   db.append("places.sqlite");
   if (db.exists()) {
     db.remove(false);
     do_check_false(db.exists());
--- a/browser/components/places/tests/unit/test_browserGlue_shutdown.js
+++ b/browser/components/places/tests/unit/test_browserGlue_shutdown.js
@@ -24,31 +24,33 @@ const PREF_AUTO_EXPORT_HTML = "browser.b
 
 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();
+    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();
   }
@@ -123,26 +125,20 @@ tests.push({
     do_check_eq(profileBookmarksJSONFile.fileSize, fileSize);
 
     do_test_finished();
   }
 });
 
 //------------------------------------------------------------------------------
 
-function finish_test() {
-  do_test_finished();
-}
-
 var testIndex = 0;
 function next_test() {
   // Remove bookmarks.html from profile.
   remove_bookmarks_html();
-  // Remove JSON backups from profile.
-  remove_all_JSON_backups();
 
   // Execute next test.
   let test = tests.shift();
   dump("\nTEST " + (++testIndex) + ": " + test.description);
   test.exec();
 }
 
 function run_test() {
--- a/browser/components/places/tests/unit/test_clearHistory_shutdown.js
+++ b/browser/components/places/tests/unit/test_clearHistory_shutdown.js
@@ -14,18 +14,18 @@ const URIS = [
 , "http://b.example2.com/"
 , "http://c.example3.com/"
 ];
 
 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"
 ];
 
 const UNEXPECTED_NOTIFICATIONS = [
   "xpcom-shutdown"
 ];
 
 const URL = "ftp://localhost/clearHistoryOnShutdown/";
--- a/toolkit/components/places/BookmarkJSONUtils.jsm
+++ b/toolkit/components/places/BookmarkJSONUtils.jsm
@@ -2,16 +2,17 @@
  * 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/. */
 
 this.EXPORTED_SYMBOLS = [ "BookmarkJSONUtils" ];
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
+const Cr = Components.results;
 
 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/PlacesUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 
@@ -508,63 +509,71 @@ BookmarkExporter.prototype = {
   _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 {
-          yield this._writeContentToFile();
+          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() {
-    // Weep over stream interface variance.
-    let streamProxy = {
-      converter: this._converterOut,
-      write: function(aData, aLen) {
-        this.converter.writeString(aData);
-      }
-    };
+    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);
-    let root = PlacesUtils.getFolderContents(PlacesUtils.placesRootId, false,
-                                             false).root;
+      // Get list of itemIds that must be excluded from the backup.
+      let excludeItems = PlacesUtils.annotations.getItemsWithAnnotation(
+                           PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
+      let root = PlacesUtils.getFolderContents(PlacesUtils.placesRootId, false,
+                                               false).root;
+      // Serialize to JSON and write to stream.
+      let nodeCount = yield BookmarkNode.serializeAsJSONToOutputStream(root,
+                                                                       streamProxy,
+                                                                       false,
+                                                                       false,
+                                                                       excludeItems);
+      root.containerOpen = false;
 
-    // Serialize to JSON and write to stream.
-    yield BookmarkNode.serializeAsJSONToOutputStream(root, streamProxy, false, false,
-                                                     excludeItems);
-    root.containerOpen = false;
+      throw new Task.Result(nodeCount);
+    }.bind(this));
   }
 }
 
 let BookmarkNode = {
   /**
    * Serializes the given node (and all its descendents) as JSON
    * and writes the serialization to the given output stream.
    *
@@ -577,89 +586,101 @@ let BookmarkNode = {
    * @param   aIsUICommand
    *          Boolean - If true, modifies serialization so that each node self-contained.
    *          For Example, tags are serialized inline with each bookmark.
    * @param   aResolveShortcuts
    *          Converts folder shortcuts into actual folders.
    * @param   aExcludeItems
    *          An array of item ids that should not be written to the backup.
    * @returns Task promise
+   * @resolves the number of serialized uri nodes.
    */
   serializeAsJSONToOutputStream: function BN_serializeAsJSONToOutputStream(
     aNode, aStream, aIsUICommand, aResolveShortcuts, aExcludeItems) {
 
     return Task.spawn(function() {
       // Serialize to stream
       let array = [];
-      if (yield this._appendConvertedNode(aNode, null, array, aIsUICommand,
-                                          aResolveShortcuts, aExcludeItems)) {
+      let result = yield this._appendConvertedNode(aNode, null, array,
+                                                   aIsUICommand,
+                                                   aResolveShortcuts,
+                                                   aExcludeItems);
+      if (result.appendedNode) {
         let json = JSON.stringify(array[0]);
         aStream.write(json, json.length);
       } else {
         throw Cr.NS_ERROR_UNEXPECTED;
       }
-    }.bind(this));                                  
+      throw new Task.Result(result.nodeCount);
+    }.bind(this));
   },
 
   _appendConvertedNode: function BN__appendConvertedNode(
     bNode, aIndex, aArray, aIsUICommand, aResolveShortcuts, aExcludeItems) {
     return Task.spawn(function() {
       let node = {};
+      let nodeCount = 0;
 
       // Set index in order received
       // XXX handy shortcut, but are there cases where we don't want
       // to export using the sorting provided by the query?
       if (aIndex)
         node.index = aIndex;
 
       this._addGenericProperties(bNode, node, aResolveShortcuts);
 
       let parent = bNode.parent;
       let grandParent = parent ? parent.parent : null;
 
       if (PlacesUtils.nodeIsURI(bNode)) {
         // Tag root accept only folder nodes
         if (parent && parent.itemId == PlacesUtils.tagsFolderId)
-          throw new Task.Result(false);
+          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(bNode.uri);
         } catch (ex) {
-          throw new Task.Result(false);
+          throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
         }
 
         yield this._addURIProperties(bNode, node, aIsUICommand);
+        nodeCount++;
       } else if (PlacesUtils.nodeIsContainer(bNode)) {
         // Tag containers accept only uri nodes
         if (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId)
-          throw new Task.Result(false);
+          throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
 
         this._addContainerProperties(bNode, node, aIsUICommand,
                                      aResolveShortcuts);
       } else if (PlacesUtils.nodeIsSeparator(bNode)) {
         // Tag root accept only folder nodes
         // Tag containers accept only uri nodes
         if ((parent && parent.itemId == PlacesUtils.tagsFolderId) ||
             (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId))
-          throw new Task.Result(false);
+          throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
 
         this._addSeparatorProperties(bNode, node);
       }
 
       if (!node.feedURI && node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
-        throw new Task.Result(yield this._appendConvertedComplexNode(node, bNode, aArray, aIsUICommand,
-                                                                     aResolveShortcuts, aExcludeItems));
+        nodeCount += yield this._appendConvertedComplexNode(node,
+                                                           bNode,
+                                                           aArray,
+                                                           aIsUICommand,
+                                                           aResolveShortcuts,
+                                                           aExcludeItems)
+        throw new Task.Result({ appendedNode: true, nodeCount: nodeCount });
       }
 
       aArray.push(node);
-      throw new Task.Result(true);
-    }.bind(this));                                   
+      throw new Task.Result({ appendedNode: true, nodeCount: nodeCount });
+    }.bind(this));
   },
 
   _addGenericProperties: function BN__addGenericProperties(
     aPlacesNode, aJSNode, aResolveShortcuts) {
     aJSNode.title = aPlacesNode.title;
     aJSNode.id = aPlacesNode.itemId;
     if (aJSNode.id != -1) {
       let parent = aPlacesNode.parent;
@@ -760,37 +781,39 @@ let BookmarkNode = {
     }
   },
 
   _appendConvertedComplexNode: function BN__appendConvertedComplexNode(
     aNode, aSourceNode, aArray, aIsUICommand, aResolveShortcuts,
     aExcludeItems) {
     return Task.spawn(function() {
       let repr = {};
+      let nodeCount = 0;
 
       for (let [name, value] in Iterator(aNode))
         repr[name] = value;
 
       // Write child nodes
       let children = repr.children = [];
       if (!aNode.livemark) {
         PlacesUtils.asContainer(aSourceNode);
         let wasOpen = aSourceNode.containerOpen;
         if (!wasOpen)
           aSourceNode.containerOpen = true;
         let cc = aSourceNode.childCount;
         for (let i = 0; i < cc; ++i) {
           let childNode = aSourceNode.getChild(i);
           if (aExcludeItems && aExcludeItems.indexOf(childNode.itemId) != -1)
             continue;
-          yield this._appendConvertedNode(aSourceNode.getChild(i), i, children,
-                                          aIsUICommand, aResolveShortcuts,
-                                          aExcludeItems);
+          let result = yield this._appendConvertedNode(aSourceNode.getChild(i), i, children,
+                                                       aIsUICommand, aResolveShortcuts,
+                                                       aExcludeItems);
+          nodeCount += result.nodeCount;
         }
         if (!wasOpen)
           aSourceNode.containerOpen = false;
       }
 
       aArray.push(repr);
-      throw new Task.Result(true);
+      throw new Task.Result(nodeCount);
     }.bind(this));
   }
 }
\ No newline at end of file
--- a/toolkit/components/places/PlacesBackups.jsm
+++ b/toolkit/components/places/PlacesBackups.jsm
@@ -4,32 +4,38 @@
  * 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/. */
 
 this.EXPORTED_SYMBOLS = ["PlacesBackups"];
 
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 Cu.import("resource://gre/modules/BookmarkJSONUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+  "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+  "resource://gre/modules/FileUtils.jsm");
+
 this.PlacesBackups = {
   get _filenamesRegex() {
     // Get the localized backup filename, will be used to clear out
     // old backups with a localized name (bug 445704).
     let localizedFilename =
       PlacesUtils.getFormattedString("bookmarksArchiveFilename", [new Date()]);
     let localizedFilenamePrefix =
       localizedFilename.substr(0, localizedFilename.indexOf("-"));
     delete this._filenamesRegex;
     return this._filenamesRegex =
-      new RegExp("^(bookmarks|" + localizedFilenamePrefix + ")-([0-9-]+)\.(json|html)");
+      new RegExp("^(bookmarks|" + localizedFilenamePrefix + ")-([0-9-]+)(_[0-9]+)*\.(json|html)");
   },
 
   get folder() {
     let bookmarksBackupDir = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
     bookmarksBackupDir.append(this.profileRelativeFolderPath);
     if (!bookmarksBackupDir.exists()) {
       bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0700", 8));
       if (!bookmarksBackupDir.exists())
@@ -122,50 +128,55 @@ this.PlacesBackups = {
   /**
    * Serializes bookmarks using JSON, and writes to the supplied file.
    * Note: any item that should not be backed up must be annotated with
    *       "places/excludeFromBackup".
    *
    * @param aFile
    *        nsIFile where to save JSON backup.
    * @return {Promise}
+   * @resolves the number of serialized uri nodes.
    */
   saveBookmarksToJSONFile: function PB_saveBookmarksToJSONFile(aFile) {
     return Task.spawn(function() {
       if (!aFile.exists())
         aFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0600", 8));
       if (!aFile.exists() || !aFile.isWritable()) {
         throw new Error("Unable to create bookmarks backup file: " + aFile.leafName);
       }
 
-      yield BookmarkJSONUtils.exportToFile(aFile);
+      let nodeCount = yield BookmarkJSONUtils.exportToFile(aFile);
 
       if (aFile.parent.equals(this.folder)) {
         // Update internal cache.
         this.entries.push(aFile);
       } else {
         // If we are saving to a folder different than our backups folder, then
         // we also want to copy this new backup to it.
         // This way we ensure the latest valid backup is the same saved by the
         // user.  See bug 424389.
-        let latestBackup = this.getMostRecent("json");
-        if (!latestBackup || latestBackup != aFile) {
-          let name = this.getFilenameForDate();
+        let name = this.getFilenameForDate();
+        let newFilename = this._appendMetaDataToFilename(name,
+                                                         { nodeCount: nodeCount });
+
+        let backupFile = yield this._getBackupFileForSameDate(name);
+        if (backupFile) {
+          backupFile.remove(false);
+        } else {
           let file = this.folder.clone();
-          file.append(name);
-          if (file.exists()) {
-            file.remove(false);
-          } else {
-            // Update internal cache if we are not replacing an existing
-            // backup file.
-            this.entries.push(file);
-          }
-          aFile.copyTo(this.folder, name);
+          file.append(newFilename);
+
+          // Update internal cache if we are not replacing an existing
+          // backup file.
+          this.entries.push(file);
         }
+        aFile.copyTo(this.folder, newFilename);
       }
+
+      throw new Task.Result(nodeCount);
     }.bind(this));
   },
 
   /**
    * Creates a dated backup in <profile>/bookmarkbackups.
    * Stores the bookmarks using JSON.
    * Note: any item that should not be backed up must be annotated with
    *       "places/excludeFromBackup".
@@ -188,37 +199,107 @@ this.PlacesBackups = {
         if (aMaxBackups !== undefined && aMaxBackups > -1)
           numberOfBackupsToDelete = this.entries.length - aMaxBackups;
 
         if (numberOfBackupsToDelete > 0) {
           // If we don't have today's backup, remove one more so that
           // the total backups after this operation does not exceed the
           // number specified in the pref.
           if (!mostRecentBackupFile ||
-              mostRecentBackupFile.leafName != newBackupFilename)
+              !this._isFilenameWithSameDate(mostRecentBackupFile.leafName,
+                                            newBackupFilename))
             numberOfBackupsToDelete++;
 
           while (numberOfBackupsToDelete--) {
             let oldestBackup = this.entries.pop();
             oldestBackup.remove(false);
           }
         }
 
         // Do nothing if we already have this backup or we don't want backups.
         if (aMaxBackups === 0 ||
             (mostRecentBackupFile &&
-             mostRecentBackupFile.leafName == newBackupFilename))
+             this._isFilenameWithSameDate(mostRecentBackupFile.leafName,
+                                          newBackupFilename)))
+          return;
+      }
+
+      let backupFile = yield this._getBackupFileForSameDate(newBackupFilename);
+      if (backupFile) {
+        if (aForceBackup)
+          backupFile.remove(false);
+        else
           return;
       }
 
+      // Save bookmarks to a backup file.
       let newBackupFile = this.folder.clone();
       newBackupFile.append(newBackupFilename);
+      let nodeCount = yield this.saveBookmarksToJSONFile(newBackupFile);
 
-      if (aForceBackup && newBackupFile.exists())
-        newBackupFile.remove(false);
+      // Rename the filename with metadata.
+      let newFilenameWithMetaData = this._appendMetaDataToFilename(
+                                      newBackupFile.leafName,
+                                      { nodeCount: nodeCount });
+      newBackupFile.moveTo(this.folder, newFilenameWithMetaData);
+
+      // Update internal cache.
+      let newFileWithMetaData = this.folder.clone();
+      newFileWithMetaData.append(newFilenameWithMetaData);
+      this.entries.pop();
+      this.entries.push(newFileWithMetaData);
+    }.bind(this));
+  },
+
+  _appendMetaDataToFilename:
+  function PB__appendMetaDataToFilename(aFilename, aMetaData) {
+    let matches = aFilename.match(this._filenamesRegex);
+    let newFilename = matches[1] + "-" + matches[2] + "_" +
+                      aMetaData.nodeCount + "." + matches[4];
+    return newFilename;
+  },
 
-      if (newBackupFile.exists())
-        return;
+  /**
+   * Gets the bookmark count for backup file.
+   *
+   * @param aFile
+   *        String The backup file.
+   *
+   * @return the bookmark count or null.
+   */
+  getBookmarkCountForFile: function PB_getBookmarkCountForFile(aFile) {
+    let count = null;
+    let matches = aFile.leafName.match(this._filenamesRegex);
+
+    if (matches && matches[3])
+      count = matches[3].replace(/_/g, "");
+    return count;
+  },
+
+  _isFilenameWithSameDate:
+  function PB__isFilenameWithSameDate(aSourceName, aTargetName) {
+    let sourceMatches = aSourceName.match(this._filenamesRegex);
+    let targetMatches = aTargetName.match(this._filenamesRegex);
 
-      yield this.saveBookmarksToJSONFile(newBackupFile);
-    }.bind(this));
-  }
+    return (sourceMatches && targetMatches &&
+            sourceMatches[1] == targetMatches[1] &&
+            sourceMatches[2] == targetMatches[2] &&
+            sourceMatches[4] == targetMatches[4]);
+    },
+
+   _getBackupFileForSameDate:
+   function PB__getBackupFileForSameDate(aFilename) {
+     return Task.spawn(function() {
+       let iterator = new OS.File.DirectoryIterator(this.folder.path);
+       let backupFile;
+
+       yield iterator.forEach(function(aEntry) {
+         if (this._isFilenameWithSameDate(aEntry.name, aFilename)) {
+           backupFile = new FileUtils.File(aEntry.path);
+           return iterator.close();
+         }
+       }.bind(this));
+       yield iterator.close();
+
+       throw new Task.Result(backupFile);
+     }.bind(this));
+   }
 }
--- a/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js
+++ b/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js
@@ -27,24 +27,33 @@ function run_test() {
   if (jsonBackupFile.exists())
     jsonBackupFile.remove(false);
   do_check_false(jsonBackupFile.exists());
   jsonBackupFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0600);
   do_check_true(jsonBackupFile.exists());
 
   // Export bookmarks to JSON.
   var backupFilename = PlacesBackups.getFilenameForDate();
-  var lastBackupFile = bookmarksBackupDir.clone();
-  lastBackupFile.append(backupFilename);
-  if (lastBackupFile.exists())
-    lastBackupFile.remove(false);
-  do_check_false(lastBackupFile.exists());
+  var rx = new RegExp("^" + backupFilename.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
+  var files = bookmarksBackupDir.directoryEntries;
+  var entry;
+  while (files.hasMoreElements()) {
+    entry = files.getNext().QueryInterface(Ci.nsIFile);
+    if (entry.leafName.match(rx))
+      entry.remove(false);
+  }
 
   Task.spawn(function() {
     yield PlacesBackups.create(NUMBER_OF_BACKUPS);
+    files = bookmarksBackupDir.directoryEntries;
+    while (files.hasMoreElements()) {
+      entry = files.getNext().QueryInterface(Ci.nsIFile);
+      if (entry.leafName.match(rx))
+        lastBackupFile = entry;
+    }
     do_check_true(lastBackupFile.exists());
 
     // Check that last backup has been retained
     do_check_false(htmlBackupFile.exists());
     do_check_false(jsonBackupFile.exists());
     do_check_true(lastBackupFile.exists());
 
     // cleanup
--- a/toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js
+++ b/toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js
@@ -15,34 +15,40 @@ function run_test() {
     entry.remove(false);
   }
 
   // Create a json dummy backup in the future.
   let dateObj = new Date();
   dateObj.setYear(dateObj.getFullYear() + 1);
   let name = PlacesBackups.getFilenameForDate(dateObj);
   do_check_eq(name, "bookmarks-" + dateObj.toLocaleFormat("%Y-%m-%d") + ".json");
+  let rx = new RegExp("^" + name.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
+  let files = bookmarksBackupDir.directoryEntries;
+  while (files.hasMoreElements()) {
+    let entry = files.getNext().QueryInterface(Ci.nsIFile);
+    if (entry.leafName.match(rx))
+      entry.remove(false);
+  }
+
   let futureBackupFile = bookmarksBackupDir.clone();
   futureBackupFile.append(name);
-  if (futureBackupFile.exists())
-    futureBackupFile.remove(false);
-  do_check_false(futureBackupFile.exists());
   futureBackupFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0600);
   do_check_true(futureBackupFile.exists());
 
   do_check_eq(PlacesBackups.entries.length, 0);
 
   Task.spawn(function() {
     yield PlacesBackups.create();
     // Check that a backup for today has been created.
     do_check_eq(PlacesBackups.entries.length, 1);
     let mostRecentBackupFile = PlacesBackups.getMostRecent();
     do_check_neq(mostRecentBackupFile, null);
-    let todayName = PlacesBackups.getFilenameForDate();
-    do_check_eq(mostRecentBackupFile.leafName, todayName);
+    let todayFilename = PlacesBackups.getFilenameForDate();
+    rx = new RegExp("^" + todayFilename.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
+    do_check_true(mostRecentBackupFile.leafName.match(rx).length > 0);
 
     // Check that future backup has been removed.
     do_check_false(futureBackupFile.exists());
 
     // Cleanup.
     mostRecentBackupFile.remove(false);
     do_check_false(mostRecentBackupFile.exists());
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_818593-store-backup-metadata.js
@@ -0,0 +1,57 @@
+/* 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/. */
+
+/**
+ * To confirm that metadata i.e. bookmark count is set and retrieved for
+ * automatic backups.
+ */
+function run_test() {
+  run_next_test();
+}
+
+add_task(function test_saveBookmarksToJSONFile_and_create()
+{
+  // Add a bookmark
+  let uri = NetUtil.newURI("http://getfirefox.com/");
+  let bookmarkId =
+    PlacesUtils.bookmarks.insertBookmark(
+      PlacesUtils.unfiledBookmarksFolderId, uri,
+      PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
+
+  // Test saveBookmarksToJSONFile()
+  let backupFile = FileUtils.getFile("TmpD", ["bookmarks.json"]);
+  backupFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, parseInt("0600", 8));
+
+  let nodeCount = yield PlacesBackups.saveBookmarksToJSONFile(backupFile, true);
+  do_check_true(nodeCount > 0);
+  do_check_true(backupFile.exists());
+  do_check_eq(backupFile.leafName, "bookmarks.json");
+
+  // Ensure the backup would be copied to our backups folder when the original
+  // backup is saved somewhere else.
+  let recentBackup = PlacesBackups.getMostRecent();
+  let todayFilename = PlacesBackups.getFilenameForDate();
+  do_check_eq(recentBackup.leafName,
+              todayFilename.replace(/\.json/, "_" + nodeCount + ".json"));
+
+  // Clear all backups in our backups folder.
+  yield PlacesBackups.create(0);
+  do_check_eq(PlacesBackups.entries.length, 0);
+
+  // Test create() which saves bookmarks with metadata on the filename.
+  yield PlacesBackups.create();
+  do_check_eq(PlacesBackups.entries.length, 1);
+
+  mostRecentBackupFile = PlacesBackups.getMostRecent();
+  do_check_neq(mostRecentBackupFile, null);
+  let rx = new RegExp("^" + todayFilename.replace(/\.json/, "") + "_([0-9]+)\.json$");
+  let matches = mostRecentBackupFile.leafName.match(rx);
+  do_check_true(matches.length > 0 && parseInt(matches[1]) == nodeCount);
+
+  // Cleanup
+  backupFile.remove(false);
+  yield PlacesBackups.create(0);
+  PlacesUtils.bookmarks.removeItem(bookmarkId);
+});
+
--- a/toolkit/components/places/tests/bookmarks/xpcshell.ini
+++ b/toolkit/components/places/tests/bookmarks/xpcshell.ini
@@ -24,8 +24,9 @@ tail =
 [test_getBookmarkedURIFor.js]
 [test_keywords.js]
 [test_nsINavBookmarkObserver.js]
 [test_removeItem.js]
 [test_savedsearches.js]
 [test_675416.js]
 [test_711914.js]
 [test_protectRoots.js]
+[test_818593-store-backup-metadata.js]
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -465,28 +465,32 @@ function check_bookmarks_html() {
  *        Name of the file to copy to the profile folder.  This file must
  *        exist in the directory that contains the test files.
  *
  * @return nsIFile object for the file.
  */
 function create_JSON_backup(aFilename) {
   if (!aFilename)
     do_throw("you must pass a filename to create_JSON_backup function");
-  remove_all_JSON_backups();
   let bookmarksBackupDir = gProfD.clone();
   bookmarksBackupDir.append("bookmarkbackups");
   if (!bookmarksBackupDir.exists()) {
     bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
     do_check_true(bookmarksBackupDir.exists());
   }
+  let profileBookmarksJSONFile = bookmarksBackupDir.clone();
+  profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
+  if (profileBookmarksJSONFile.exists()) {
+    profileBookmarksJSONFile.remove();
+  }
   let bookmarksJSONFile = gTestDir.clone();
   bookmarksJSONFile.append(aFilename);
   do_check_true(bookmarksJSONFile.exists());
   bookmarksJSONFile.copyTo(bookmarksBackupDir, FILENAME_BOOKMARKS_JSON);
-  let profileBookmarksJSONFile = bookmarksBackupDir.clone();
+  profileBookmarksJSONFile = bookmarksBackupDir.clone();
   profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
   do_check_true(profileBookmarksJSONFile.exists());
   return profileBookmarksJSONFile;
 }
 
 
 /**
  * Remove bookmarksbackup dir and all backups from the profile folder.
@@ -499,22 +503,40 @@ function remove_all_JSON_backups() {
     do_check_false(bookmarksBackupDir.exists());
   }
 }
 
 
 /**
  * Check a JSON backup file for today exists in the profile folder.
  *
+ * @param aIsAutomaticBackup The boolean indicates whether it's an automatic
+ *        backup.
  * @return nsIFile object for the file.
  */
-function check_JSON_backup() {
-  let profileBookmarksJSONFile = gProfD.clone();
-  profileBookmarksJSONFile.append("bookmarkbackups");
-  profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
+function check_JSON_backup(aIsAutomaticBackup) {
+  let profileBookmarksJSONFile;
+  if (aIsAutomaticBackup) {
+    let bookmarksBackupDir = gProfD.clone();
+    bookmarksBackupDir.append("bookmarkbackups");
+    let files = bookmarksBackupDir.directoryEntries;
+    let backup_date = new Date().toLocaleFormat("%Y-%m-%d");
+    let rx = new RegExp("^bookmarks-" + backup_date + "_[0-9]+\.json$");
+    while (files.hasMoreElements()) {
+      let entry = files.getNext().QueryInterface(Ci.nsIFile);
+      if (entry.leafName.match(rx)) {
+        profileBookmarksJSONFile = entry;
+        break;
+      }
+    }
+  } else {
+    profileBookmarksJSONFile = gProfD.clone();
+    profileBookmarksJSONFile.append("bookmarkbackups");
+    profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
+  }
   do_check_true(profileBookmarksJSONFile.exists());
   return profileBookmarksJSONFile;
 }
 
 /**
  * Returns the frecency of a url.
  *
  * @param aURI
--- a/toolkit/components/places/tests/unit/test_utils_backups_create.js
+++ b/toolkit/components/places/tests/unit/test_utils_backups_create.js
@@ -61,31 +61,48 @@ function run_test() {
     yield PlacesBackups.create(Math.floor(dates.length/2));
     // Add today's backup.
     dates.push(dateObj.toLocaleFormat("%Y-%m-%d"));
 
     // Check backups.
     for (var i = 0; i < dates.length; i++) {
       let backupFilename;
       let shouldExist;
+      let backupFile;
       if (i > Math.floor(dates.length/2)) {
-        backupFilename = PREFIX + dates[i] + SUFFIX;
+        let files = bookmarksBackupDir.directoryEntries;
+        let rx = new RegExp("^" + PREFIX + dates[i] + "(_[0-9]+){0,1}" + SUFFIX + "$");
+        while (files.hasMoreElements()) {
+          let entry = files.getNext().QueryInterface(Ci.nsIFile);
+          if (entry.leafName.match(rx)) {
+            backupFilename = entry.leafName;
+            backupFile = entry;
+            break;
+          }
+        }
         shouldExist = true;
       }
       else {
         backupFilename = LOCALIZED_PREFIX + dates[i] + SUFFIX;
+        backupFile = bookmarksBackupDir.clone();
+        backupFile.append(backupFilename);
         shouldExist = false;
       }
-      var backupFile = bookmarksBackupDir.clone();
-      backupFile.append(backupFilename);
       if (backupFile.exists() != shouldExist)
         do_throw("Backup should " + (shouldExist ? "" : "not") + " exist: " + backupFilename);
     }
 
     // Cleanup backups folder.
-    bookmarksBackupDir.remove(true);
-    do_check_false(bookmarksBackupDir.exists());
+    // XXX: Can't use bookmarksBackupDir.remove(true) because file lock happens
+    // on WIN XP.
+    let files = bookmarksBackupDir.directoryEntries;
+    while (files.hasMoreElements()) {
+      let entry = files.getNext().QueryInterface(Ci.nsIFile);
+      entry.remove(false);
+    }
+    do_check_false(bookmarksBackupDir.directoryEntries.hasMoreElements());
+
     // Recreate the folder.
     PlacesBackups.folder;
 
     do_test_finished();
   });
 }
--- a/toolkit/locales/en-US/chrome/places/places.properties
+++ b/toolkit/locales/en-US/chrome/places/places.properties
@@ -25,8 +25,15 @@ finduri-MonthYear=%1$S %2$S
 localhost=(local files)
 
 # LOCALIZATION NOTE (bookmarksArchiveFilename):
 # Do not change this string! It's used only to
 # detect older localized bookmark archives from
 # before bug 445704 was fixed. It will be removed
 # in a subsequent release.
 bookmarksArchiveFilename=bookmarks-%S.json
+
+# LOCALIZATION NOTE
+# The string is used for showing file size of each backup in the "fileRestorePopup" popup
+# %1$S is the file size
+# %2$S is the file size unit
+backupFileSizeText=%1$S %2$S
+