Bug 851466 - Import downloads.sqlite to downloads.json. r=paolo
authorFelipe Gomes <felipc@gmail.com>
Fri, 16 Aug 2013 15:35:42 -0300
changeset 143011 92b20164931fa8d38713af8d9a05c931bc020a17
parent 143010 f40527322ff18e8277124d035d78151cda58bfd6
child 143012 4a6b9390ff5a2a8de95d91bbc1e8d44d75c91375
push id32605
push userphilringnalda@gmail.com
push dateMon, 19 Aug 2013 00:51:46 +0000
treeherdermozilla-inbound@7f882e063eaf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspaolo
bugs851466
milestone26.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 851466 - Import downloads.sqlite to downloads.json. r=paolo
toolkit/components/jsdownloads/src/DownloadCore.jsm
toolkit/components/jsdownloads/src/DownloadImport.jsm
toolkit/components/jsdownloads/src/DownloadIntegration.jsm
toolkit/components/jsdownloads/src/Downloads.jsm
toolkit/components/jsdownloads/src/moz.build
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -1102,17 +1102,17 @@ function DownloadError(aResult, aMessage
   const NS_ERROR_MODULE_FILES = 13;
 
   // Set the error name used by the Error object prototype first.
   this.name = "DownloadError";
   this.result = aResult || Cr.NS_ERROR_FAILURE;
   if (aMessage) {
     this.message = aMessage;
   } else {
-    let exception = new Components.Exception(this.result);
+    let exception = new Components.Exception("", this.result);
     this.message = exception.toString();
   }
   if (aInferCause) {
     let module = ((aResult & 0x7FFF0000) >> 16) - NS_ERROR_MODULE_BASE_OFFSET;
     this.becauseSourceFailed = (module == NS_ERROR_MODULE_NETWORK);
     this.becauseTargetFailed = (module == NS_ERROR_MODULE_FILES);
   }
   this.stack = new Error().stack;
new file mode 100644
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadImport.jsm
@@ -0,0 +1,188 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "DownloadImport",
+];
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+                                  "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm")
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+                                  "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
+
+/**
+ * These values come from the previous interface
+ * nsIDownloadManager, which has now been deprecated.
+ * These are the only types of download states that
+ * we will import.
+ */
+const DOWNLOAD_NOTSTARTED = -1;
+const DOWNLOAD_DOWNLOADING = 0;
+const DOWNLOAD_PAUSED = 4;
+const DOWNLOAD_QUEUED = 5;
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadImport
+
+/**
+ * Provides an object that has a method to import downloads
+ * from the previous SQLite storage format.
+ *
+ * @param aList   A DownloadList where each successfully
+ *                imported download will be added.
+ * @param aPath   The path to the database file.
+ */
+this.DownloadImport = function(aList, aPath) {
+  this.list = aList;
+  this.path = aPath;
+}
+
+this.DownloadImport.prototype = {
+  /**
+   * Imports unfinished downloads from the previous SQLite storage
+   * format (supporting schemas 7 and up), to the new Download object
+   * format. Each imported download will be added to the DownloadList
+   *
+   * @return {Promise}
+   * @resolves When the operation has completed (i.e., every download
+   *           from the previous database has been read and added to
+   *           the DownloadList)
+   */
+  import: function () {
+    return Task.spawn(function task_DI_import() {
+      let connection = yield Sqlite.openConnection({ path: this.path });
+
+      try {
+        let schemaVersion = yield connection.getSchemaVersion();
+        // We don't support schemas older than version 7 (from 2007)
+        // - Version 7 added the columns mimeType, preferredApplication
+        //   and preferredAction in 2007
+        // - Version 8 added the column autoResume in 2007
+        //   (if we encounter version 7 we will treat autoResume = false)
+        // - Version 9 is the last known version, which added a unique
+        //   GUID text column that is not used here
+        if (schemaVersion < 7) {
+          throw new Error("Unable to import in-progress downloads because "
+                          + "the existing profile is too old.");
+        }
+
+        let rows = yield connection.execute("SELECT * FROM moz_downloads");
+
+        for (let row of rows) {
+          try {
+            // Get the DB row data
+            let source = row.getResultByName("source");
+            let target = row.getResultByName("target");
+            let tempPath = row.getResultByName("tempPath");
+            let startTime = row.getResultByName("startTime");
+            let state = row.getResultByName("state");
+            let referrer = row.getResultByName("referrer");
+            let maxBytes = row.getResultByName("maxBytes");
+            let mimeType = row.getResultByName("mimeType");
+            let preferredApplication = row.getResultByName("preferredApplication");
+            let preferredAction = row.getResultByName("preferredAction");
+            let entityID = row.getResultByName("entityID");
+
+            let autoResume = false;
+            try {
+              autoResume = row.getResultByName("autoResume");
+            } catch (ex) {
+              // autoResume wasn't present in schema version 7
+            }
+
+            if (!source) {
+              throw new Error("Attempted to import a row with an empty " +
+                              "source column.");
+            }
+
+            let resumeDownload = false;
+
+            switch (state) {
+              case DOWNLOAD_NOTSTARTED:
+              case DOWNLOAD_QUEUED:
+              case DOWNLOAD_DOWNLOADING:
+                resumeDownload = true;
+                break;
+
+              case DOWNLOAD_PAUSED:
+                resumeDownload = autoResume;
+                break;
+
+              default:
+                // We won't import downloads in other states
+                continue;
+            }
+
+            // Transform the data
+            let targetPath = NetUtil.newURI(target)
+                                    .QueryInterface(Ci.nsIFileURL).path;
+
+            let launchWhenSucceeded = (preferredAction != Ci.nsIMIMEInfo.saveToDisk);
+
+            let downloadOptions = {
+              source: {
+                url: source,
+                referrer: referrer
+              },
+              target: {
+                path: targetPath,
+                partFilePath: tempPath,
+              },
+              saver: {
+                type: "copy",
+                entityID: entityID
+              },
+              startTime: startTime,
+              totalBytes: maxBytes,
+              hasPartialData: true, // true because it's a paused download
+              tryToKeepPartialData: true,
+              launchWhenSucceeded: launchWhenSucceeded,
+              contentType: mimeType,
+              launcherPath: preferredApplication
+            };
+
+            let download = yield Downloads.createDownload(downloadOptions);
+
+            this.list.add(download);
+
+            if (resumeDownload) {
+              download.start();
+            } else {
+              yield download.refresh();
+            }
+
+          } catch (ex) {
+            Cu.reportError("Error importing download: " + ex);
+          }
+        }
+
+      } catch (ex) {
+        Cu.reportError(ex);
+      } finally {
+        yield connection.close();
+      }
+    }.bind(this));
+  }
+}
+
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -22,16 +22,18 @@ const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore",
                                   "resource://gre/modules/DownloadStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport",
+                                  "resource://gre/modules/DownloadImport.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
@@ -84,16 +86,22 @@ const Timer = Components.Constructor("@m
  * file will not be deleted when the browser starts again.
  *
  * In all cases, for best efficiency, this value should be high enough that the
  * input/output for opening or closing the target file does not overlap with the
  * one for saving the list of downloads.
  */
 const kSaveDelayMs = 1500;
 
+/**
+ * This pref indicates if we have already imported (or attempted to import)
+ * the downloads database from the previous SQLite storage.
+ */
+const kPrefImportedFromSqlite = "browser.download.importedFromSqlite";
+
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadIntegration
 
 /**
  * Provides functions to integrate with the host application, handling for
  * example the global prompts on shutdown.
  */
 this.DownloadIntegration = {
@@ -131,36 +139,69 @@ this.DownloadIntegration = {
    *        DownloadList object to be populated with the download objects
    *        serialized from the previous session.  This list will be persisted
    *        to disk during the session lifetime.
    *
    * @return {Promise}
    * @resolves When the list has been populated.
    * @rejects JavaScript exception.
    */
-  loadPersistent: function DI_loadPersistent(aList)
-  {
-    if (this.dontLoad) {
-      return Promise.resolve();
-    }
+  initializePublicDownloadList: function(aList) {
+    return Task.spawn(function task_DI_initializePublicDownloadList() {
+      if (this.dontLoad) {
+        return;
+      }
+
+      if (this._store) {
+        throw new Error("initializePublicDownloadList may be called only once.");
+      }
 
-    if (this._store) {
-      throw new Error("loadPersistent may be called only once.");
-    }
+      this._store = new DownloadStore(aList, OS.Path.join(
+                                                OS.Constants.Path.profileDir,
+                                                "downloads.json"));
+      this._store.onsaveitem = this.shouldPersistDownload.bind(this);
+
+      if (this._importedFromSqlite) {
+        try {
+          yield this._store.load();
+        } catch (ex) {
+          Cu.reportError(ex);
+        }
+      } else {
+        let sqliteDBpath = OS.Path.join(OS.Constants.Path.profileDir,
+                                        "downloads.sqlite");
 
-    this._store = new DownloadStore(aList, OS.Path.join(
-                                              OS.Constants.Path.profileDir,
-                                              "downloads.json"));
-    this._store.onsaveitem = this.shouldPersistDownload.bind(this);
+        if (yield OS.File.exists(sqliteDBpath)) {
+          let sqliteImport = new DownloadImport(aList, sqliteDBpath);
+          yield sqliteImport.import();
+
+          let importCount = (yield aList.getAll()).length;
+          if (importCount > 0) {
+            try {
+              yield this._store.save();
+            } catch (ex) { }
+          }
 
-    // Load the list of persistent downloads, then add the DownloadAutoSaveView
-    // even if the load operation failed.
-    return this._store.load().then(null, Cu.reportError).then(() => {
+          // No need to wait for the file removal.
+          OS.File.remove(sqliteDBpath).then(null, Cu.reportError);
+        }
+
+        Services.prefs.setBoolPref(kPrefImportedFromSqlite, true);
+
+        // Don't even report error here because this file is pre Firefox 3
+        // and most likely doesn't exist.
+        OS.File.remove(OS.Path.join(OS.Constants.Path.profileDir,
+                                    "downloads.rdf"));
+
+      }
+
+      // After the list of persisten downloads have been loaded, add
+      // the DownloadAutoSaveView (even if the load operation failed).
       new DownloadAutoSaveView(aList, this._store);
-    });
+    }.bind(this));
   },
 
   /**
    * Determines if a Download object from the list of persistent downloads
    * should be saved into a file, so that it can be restored across sessions.
    *
    * This function allows filtering out downloads that the host application is
    * not interested in persisting across sessions, for example downloads that
@@ -191,17 +232,17 @@ this.DownloadIntegration = {
    * @resolves The nsIFile of downloads directory.
    */
   getSystemDownloadsDirectory: function DI_getSystemDownloadsDirectory() {
     return Task.spawn(function() {
       if (this._downloadsDirectory) {
         // This explicitly makes this function a generator for Task.jsm. We
         // need this because calls to the "yield" operator below may be
         // preprocessed out on some platforms.
-        yield;
+        yield undefined;
         throw new Task.Result(this._downloadsDirectory);
       }
 
       let directory = null;
 #ifdef XP_MACOSX
       directory = this._getDirectory("DfltDwnld");
 #elifdef XP_WIN
       // For XP/2K, use My Documents/Downloads. Other version uses
@@ -557,17 +598,31 @@ this.DownloadIntegration = {
     DownloadObserver.registerView(aList, aIsPrivate);
     if (!DownloadObserver.observersAdded) {
       DownloadObserver.observersAdded = true;
       Services.obs.addObserver(DownloadObserver, "quit-application-requested", true);
       Services.obs.addObserver(DownloadObserver, "offline-requested", true);
       Services.obs.addObserver(DownloadObserver, "last-pb-context-exiting", true);
     }
     return Promise.resolve();
-  }
+  },
+
+  /**
+   * Checks if we have already imported (or attempted to import)
+   * the downloads database from the previous SQLite storage.
+   *
+   * @return boolean True if we the previous DB was imported.
+   */
+  get _importedFromSqlite() {
+    try {
+      return Services.prefs.getBoolPref(kPrefImportedFromSqlite);
+    } catch (ex) {
+      return false;
+    }
+  },
 };
 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadObserver
 
 this.DownloadObserver = {
   /**
    * Flag to determine if the observers have been added previously.
--- a/toolkit/components/jsdownloads/src/Downloads.jsm
+++ b/toolkit/components/jsdownloads/src/Downloads.jsm
@@ -143,17 +143,17 @@ this.Downloads = {
   getPublicDownloadList: function D_getPublicDownloadList()
   {
     if (!this._promisePublicDownloadList) {
       this._promisePublicDownloadList = Task.spawn(
         function task_D_getPublicDownloadList() {
           let list = new DownloadList(true);
           try {
             yield DownloadIntegration.addListObservers(list, false);
-            yield DownloadIntegration.loadPersistent(list);
+            yield DownloadIntegration.initializePublicDownloadList(list);
           } catch (ex) {
             Cu.reportError(ex);
           }
           throw new Task.Result(list);
         });
     }
     return this._promisePublicDownloadList;
   },
--- a/toolkit/components/jsdownloads/src/moz.build
+++ b/toolkit/components/jsdownloads/src/moz.build
@@ -6,16 +6,17 @@
 
 EXTRA_COMPONENTS += [
     'DownloadLegacy.js',
     'Downloads.manifest',
 ]
 
 EXTRA_JS_MODULES += [
     'DownloadCore.jsm',
+    'DownloadImport.jsm',
     'DownloadList.jsm',
     'DownloadStore.jsm',
     'DownloadUIHelper.jsm',
     'Downloads.jsm',
 ]
 
 EXTRA_PP_JS_MODULES += [
     'DownloadIntegration.jsm',