fix handling of being offline, and going offline in the middle of uploading cloud files, r=mconley,a=bienvenu. bug 744004
authorDavid Bienvenu <dbienvenu@mozilla.com>
Mon, 16 Apr 2012 14:43:14 -0400
changeset 11212 fc5dec27443ccf2de0faea758943ed685fd9a371
parent 11211 73a1f3d5bda743474fc50ef5fd2837f036f259ba
child 11213 359552838186df21a1579785b0cc39cbe45482f5
push id463
push userbugzilla@standard8.plus.com
push dateTue, 24 Apr 2012 17:34:51 +0000
treeherdercomm-beta@e53588e8f7b0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley, bienvenu
bugs744004
fix handling of being offline, and going offline in the middle of uploading cloud files, r=mconley,a=bienvenu. bug 744004
mail/components/cloudfile/nsDropbox.js
mail/components/cloudfile/nsIMsgCloudFileProvider.idl
mail/components/cloudfile/nsYouSendIt.js
mail/components/compose/content/MsgComposeCommands.js
mail/components/compose/content/bigFileObserver.js
mail/test/mozmill/shared-modules/test-cloudfile-helpers.js
--- a/mail/components/cloudfile/nsDropbox.js
+++ b/mail/components/cloudfile/nsDropbox.js
@@ -109,32 +109,37 @@ nsDropbox.prototype = {
     aRequestObserver.onStopRequest(null, null, aStatus);
     this._uploadingFile = null;
     this._uploads.shift();
     if (this._uploads.length > 0) {
       let nextUpload = this._uploads[0];
       this.log.info("chaining upload, file = " + nextUpload.file.leafName);
       this._uploadingFile = nextUpload.file;
       this._uploader = nextUpload;
-      this.uploadFile(nextUpload.file, nextUpload.callback);
+      try {
+        this.uploadFile(nextUpload.file, nextUpload.callback);
+      }
+      catch (ex) {
+        nextUpload.callback(nextUpload.requestObserver, Cr.NS_ERROR_FAILURE);
+      }
     }
     else
       this._uploader = null;
   },
 
   /** 
    * Attempts to upload a file to Dropbox.
    *
    * @param aFile the nsILocalFile to be uploaded
    * @param aCallback an nsIRequestObserver for listening for the starting
    *                  and ending states of the upload.
    */
   uploadFile: function nsDropbox_uploadFile(aFile, aCallback) {
     if (Services.io.offline)
-      return Ci.nsIMsgCloudFileProvider.offlineErr;
+      throw Ci.nsIMsgCloudFileProvider.offlineErr;
 
     this.log.info("uploading " + aFile.leafName);
 
     // Some ugliness here - we stash requestObserver here, because we might
     // use it again in _getUserInfo.
     this.requestObserver = aCallback;
 
     // if we're uploading a file, queue this request.
@@ -301,17 +306,17 @@ nsDropbox.prototype = {
    *
    * @param aWithUI a boolean for whether or not we should display authorization
    *                UI if we don't have a valid token anymore, or just fail out.
    * @param aCallback an nsIRequestObserver for observing the starting and
    *                  ending states of the request.
    */
   refreshUserInfo: function nsDropbox_refreshUserInfo(aWithUI, aCallback) {
     if (Services.io.offline)
-      return Ci.nsIMsgCloudFileProvider.offlineErr;
+      throw Ci.nsIMsgCloudFileProvider.offlineErr;
     this.requestObserver = aCallback;
     aCallback.onStartRequest(null, null);
     if (!this._loggedIn)
       return this._logonAndGetUserInfo(null, null, aWithUI);
     if (!this._userInfo)
       return this._getUserInfo();
     return this._userInfo;
   },
@@ -378,17 +383,17 @@ nsDropbox.prototype = {
    * Attempt to delete an upload file if we've uploaded it.
    *
    * @param aFile the file that was originall uploaded
    * @param aCallback an nsIRequestObserver for monitoring the starting and
    *                  ending states of the deletion request.
    */
   deleteFile: function nsDropbox_deleteFile(aFile, aCallback) {
     if (Services.io.offline)
-      return Ci.nsIMsgCloudFileProvider.offlineErr;
+      throw Ci.nsIMsgCloudFileProvider.offlineErr;
 
     let uploadInfo = this._uploadInfo[aFile.path];
     if (!uploadInfo)
       throw Cr.NS_ERROR_FAILURE;
 
     this.requestObserver = aCallback;
     let path = wwwFormUrlEncode(uploadInfo.path);
     let url = gServerUrl + kDeletePath + "&path=" + uploadInfo.path;
--- a/mail/components/cloudfile/nsIMsgCloudFileProvider.idl
+++ b/mail/components/cloudfile/nsIMsgCloudFileProvider.idl
@@ -31,16 +31,17 @@ interface nsIMsgCloudFileProvider : nsIS
 
   /**
    * upload the file to the cloud provider. The callback's OnStopRequest
    * method will be called when finished, with success or an error code.
    *
    * @param aFile file to upload
    * @param aCallback callback when finished.
    *
+   * @throws nsIMsgCloudFileProvider.offlineErr if we are offline.
    */
   void uploadFile(in nsILocalFile aFile, in nsIRequestObserver aCallback);
   ACString urlForFile(in nsILocalFile aFile);
 
   /**
    * Cancels the upload of the passed in file, if it hasn't finished yet.
    * If it hasn't started yet, it will be removed from the upload queue.
    *
@@ -50,41 +51,44 @@ interface nsIMsgCloudFileProvider : nsIS
 
   /**
    * Refresh the user info for this account. This will fill in the quota info,
    * if supported by the provider.
    *
    * @param aWithUI if true, we may prompt for a password, or bring up an auth
    *                page. If false, we won't, and will fail if auth is required.
    * @param aCallback callback when finished.
+   * @throws nsIMsgCloudFileProvider.offlineErr if we are offline.
    */
   void refreshUserInfo(in boolean aWithUI, in nsIRequestObserver aCallback);
 
   /**
    * Delete a file that we've uploaded in this session and discarded. This
    * operation is asynchronous.
    *
    * @param aFile File we previously uploaded in this session.
    * @param aCallback callback when finished
    *
-   * throws NS_ERROR_FAILURE if we don't know about the file.
+   * @throws NS_ERROR_FAILURE if we don't know about the file.
+   * @throws nsIMsgCloudFileProvider.offlineErr if we are offline.
    */
   void deleteFile(in nsILocalFile aFile, in nsIRequestObserver aCallback);
 
   /**
    * If the provider has an API for creating an account, this will start the
    * process of creating one. There will probably have to be some sort of
    * validation on the part of the user before the account is created.
    * If not, this will throw NS_ERROR_NOT_IMPLEMENTED.
    *
    * If the REST call succeeds, aCallback.onStopRequest will get called back
    * with an http status. Generally, status between 200 and 300 is OK,
    * otherwise, an error occurred which is * probably specific to the provider.
    * If the request fails completely, onStopRequest will get called with
    * Components.results.NS_ERROR_FAILURE
+   * @throws nsIMsgCloudFileProvider.offlineErr if we are offline.
    */
   void createNewAccount(in ACString aEmailAddress, in ACString aPassword,
                         in ACString aFirstName, in ACString aLastName,
                         in nsIRequestObserver aCallback);
 
   void createExistingAccount(in nsIRequestObserver aCallback);
 
   /**
--- a/mail/components/cloudfile/nsYouSendIt.js
+++ b/mail/components/cloudfile/nsYouSendIt.js
@@ -102,32 +102,38 @@ nsYouSendIt.prototype = {
 
     this._uploadingFile = null;
     this._uploads.shift();
     if (this._uploads.length > 0) {
       let nextUpload = this._uploads[0];
       this.log.info("chaining upload, file = " + nextUpload.file.leafName);
       this._uploadingFile = nextUpload.file;
       this._uploader = nextUpload;
-      this.uploadFile(nextUpload.file, nextUpload.requestObserver);
+      try {
+        this.uploadFile(nextUpload.file, nextUpload.requestObserver);
+      }
+      catch (ex) {
+        // I'd like to pass ex.result, but that doesn't seem to be defined.
+        nextUpload.callback(nextUpload.requestObserver, Cr.NS_ERROR_FAILURE);
+      }
     }
     else
       this._uploader = null;
   },
 
   /**
    * Attempt to upload a file to YouSendIt's servers.
    *
    * @param aFile an nsILocalFile for uploading.
    * @param aCallback an nsIRequestObserver for monitoring the start and
    *                  stop states of the upload procedure.
    */
   uploadFile: function nsYouSendIt_uploadFile(aFile, aCallback) {
     if (Services.io.offline)
-      return Ci.nsIMsgCloudFileProvider.offlineErr;
+      throw Ci.nsIMsgCloudFileProvider.offlineErr;
 
     this.log.info("Preparing to upload a file");
 
     // if we're uploading a file, queue this request.
     if (this._uploadingFile && this._uploadingFile != aFile) {
       let uploader = new nsYouSendItFileUploader(this, aFile,
                                                  this._uploaderCallback
                                                      .bind(this),
@@ -315,17 +321,17 @@ nsYouSendIt.prototype = {
    *
    * @param aWithUI a boolean for whether or not we should prompt the user for
    *                a password if we don't have a proper token.
    * @param aListener an nsIRequestObserver for monitoring the start and stop
    *                  states of fetching profile information.
    */
   refreshUserInfo: function nsYouSendIt_refreshUserInfo(aWithUI, aListener) {
     if (Services.io.offline)
-      return Ci.nsIMsgCloudFileProvider.offlineErr;
+      throw Ci.nsIMsgCloudFileProvider.offlineErr;
 
     aListener.onStartRequest(null, null);
 
     // Let's define some reusable callback functions...
     let onGetUserInfoSuccess = function() {
       aListener.onStopRequest(null, null, Cr.NS_OK);
     }
 
@@ -354,17 +360,17 @@ nsYouSendIt.prototype = {
   /**
    * Creates an account for a user.  Note that, currently, this function is
    * not being used by the UI.
    */
   createNewAccount: function nsYouSendIt_createNewAccount(aEmailAddress, aPassword,
                                                           aFirstName, aLastName,
                                                           aRequestObserver) {
     if (Services.io.offline)
-      return Ci.nsIMsgCloudFileProvider.offlineErr;
+      throw Ci.nsIMsgCloudFileProvider.offlineErr;
 
     let args = "?email=" + aEmailAddress + "&password=" + aPassword + "&firstname="
                + aFirstName + "&lastname=" + aLastName;
 
     let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
                 .createInstance(Ci.nsIXMLHttpRequest);
 
     req.open("POST", gServerUrl + kUserInfoPath + args, true);
@@ -453,17 +459,17 @@ nsYouSendIt.prototype = {
    * @param aCallback an nsIRequestObserver for monitoring the start and stop
    *                  states of the delete procedure.
    */
   deleteFile: function nsYouSendIt_deleteFile(aFile, aCallback) {
     this.log.info("Deleting a file");
 
     if (Services.io.offline) {
       this.log.error("We're offline - we can't delete the file.");
-      return Ci.nsIMsgCloudFileProvider.offlineErr;
+      throw Ci.nsIMsgCloudFileProvider.offlineErr;
     }
 
     let uploadInfo = this._uploadInfo[aFile.path];
     if (!uploadInfo) {
       this.log.error("Could not find a record for the file to be deleted.");
       throw Cr.NS_ERROR_FAILURE;
     }
 
--- a/mail/components/compose/content/MsgComposeCommands.js
+++ b/mail/components/compose/content/MsgComposeCommands.js
@@ -437,18 +437,19 @@ var defaultController = {
       }
     },
 
     cmd_attachCloud: {
       isEnabled: function() {
         // Hide the command entirely if there are no cloud accounts or
         // the feature is disbled.
         let cmd = document.getElementById("cmd_attachCloud");
-        cmd.hidden = !Services.prefs.getBoolPref("mail.cloud_files.enabled")
-                     || (cloudFileAccounts.accounts.length == 0);
+        cmd.hidden = !Services.prefs.getBoolPref("mail.cloud_files.enabled") ||
+                     (cloudFileAccounts.accounts.length == 0) ||
+                     Services.io.offline;
         return !cmd.hidden;
       },
       doCommand: function() {
         // We should never actually call this, since the <command> node calls
         // a different function.
       }
     },
 
@@ -677,17 +678,18 @@ var attachmentBucketController = {
 
     cmd_convertCloud: {
       isEnabled: function() {
         // Hide the command entirely if Filelink is disabled, or if there are
         // no cloud accounts.
         let cmd = document.getElementById("cmd_convertCloud");
 
         cmd.hidden = (!Services.prefs.getBoolPref("mail.cloud_files.enabled") ||
-                      cloudFileAccounts.accounts.length == 0);
+                      cloudFileAccounts.accounts.length == 0) ||
+                      Services.io.offline;
         if (cmd.hidden)
           return false;
 
         let bucket = document.getElementById("attachmentBucket");
         for (let [,item] in Iterator(bucket.selectedItems)) {
           if (item.uploading)
             return false;
         }
@@ -1064,31 +1066,22 @@ uploadListener.prototype = {
           attachmentItem.image = null;
         }
       }
 
       let event = document.createEvent("Events");
       event.initEvent("attachment-uploaded", true, true);
       attachmentItem.dispatchEvent(event);
     }
-    else if (aStatusCode == Components.interfaces
-                                      .nsIMsgCloudFileProvider
-                                      .uploadCanceled) {
-      attachmentItem.setAttribute("tooltiptext", attachmentItem.attachment.url);
-      attachmentItem.image = null;
-      attachmentItem.uploading = false;
-      attachmentItem.attachment.sendViaCloud = false;
-      delete attachmentItem.cloudProvider;
-    }
     else {
       let title;
       let msg;
       let displayName = cloudFileAccounts.getDisplayName(this.cloudProvider);
       let bundle = getComposeBundle();
-
+      let displayError = true;
       switch (aStatusCode) {
       case this.cloudProvider.authErr:
         title = bundle.getString("errorCloudFileAuth.title");
         msg = bundle.getFormattedString("errorCloudFileAuth.message",
                                         [displayName]);
         break;
       case this.cloudProvider.uploadErr:
         title = bundle.getString("errorCloudFileUpload.title");
@@ -1103,38 +1096,47 @@ uploadListener.prototype = {
                                          this.attachment.name]);
         break;
       case this.cloudProvider.uploadExceedsFileLimit:
         title = bundle.getString("errorCloudFileLimit.title");
         msg = bundle.getFormattedString("errorCloudFileLimit.message",
                                         [displayName,
                                          this.attachment.name]);
         break;
+      case this.cloudProvider.uploadCanceled:
+        displayError = false;
+        break;
       default:
         title = bundle.getString("errorCloudFileOther.title");
         msg = bundle.getFormattedString("errorCloudFileOther.message",
                                         [displayName]);
         break;
       }
 
       // TODO: support actions other than "Upgrade"
-      const prompt = Services.prompt;
-      let url = this.cloudProvider.providerUrlForError(aStatusCode);
-      let flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_OK;
-      if (url)
-        flags += prompt.BUTTON_POS_1 * prompt.BUTTON_TITLE_IS_STRING;
-      if (prompt.confirmEx(window, title, msg, flags, null,
-                           bundle.getString("errorCloudFileUpgrade.label"),
-                           null, null, {})) {
-        openLinkExternally(url);
+      if (displayError) {
+        const prompt = Services.prompt;
+        let url = this.cloudProvider.providerUrlForError(aStatusCode);
+        let flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_OK;
+        if (url)
+          flags += prompt.BUTTON_POS_1 * prompt.BUTTON_TITLE_IS_STRING;
+        if (prompt.confirmEx(window, title, msg, flags, null,
+                             bundle.getString("errorCloudFileUpgrade.label"),
+                             null, null, {})) {
+          openLinkExternally(url);
+        }
       }
 
       if (attachmentItem) {
         // Remove the loading throbber.
         attachmentItem.image = null;
+        attachmentItem.setAttribute("tooltiptext", attachmentItem.attachment.url);
+        attachmentItem.uploading = false;
+        attachmentItem.attachment.sendViaCloud = false;
+        delete attachmentItem.cloudProvider;
       }
     }
 
     gNumUploadingAttachments--;
     updateSendCommands();
   },
 
   QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIRequestObserver,
@@ -1197,33 +1199,43 @@ function attachToCloud(aProvider)
 
     let files = [f for (f in fixIterator(fp.files,
                                          Components.interfaces.nsILocalFile))];
     let attachments = [FileToAttachment(f) for each (f in files)];
 
     let i = 0;
     let items = AddAttachments(attachments, function(aItem) {
       let listener = new uploadListener(attachments[i], files[i], aProvider);
-      aProvider.uploadFile(files[i], listener);
+      try {
+        aProvider.uploadFile(files[i], listener);
+      }
+      catch (ex) {
+        listener.onStopRequest(null, null, ex.result);
+      }
       i++;
     });
 
     SetLastAttachDirectory(files[files.length-1]);
   }
 }
 
 /**
  * Convert an array of attachments to cloud attachments.
  *
  * @param aItems an array of <attachmentitem>s containing the attachments in
  *        question
  * @param aProvider the cloud provider to upload the files to
  */
 function convertListItemsToCloudAttachment(aItems, aProvider)
 {
+  // If we want to display an offline error message, we should do it here.
+  // No sense in doing the delete and upload and having them fail.
+  if (Services.io.offline)
+    return;
+
   let fileHandler = Services.io.getProtocolHandler("file")
                             .QueryInterface(Components.interfaces.nsIFileProtocolHandler);
   let convertedAttachments = Components.classes["@mozilla.org/array;1"]
                                        .createInstance(Components.interfaces.nsIMutableArray);
 
   for (let [,item] in Iterator(aItems)) {
     let url = item.attachment.url;
 
@@ -1234,24 +1246,30 @@ function convertListItemsToCloudAttachme
     }
 
     let file = fileHandler.getFileFromURLSpec(url);
     if (item.cloudProvider) {
       item.cloudProvider.deleteFile(
         file, new deletionListener(item.attachment, item.cloudProvider));
     }
 
-    aProvider.uploadFile(file,
-                         new uploadListener(item.attachment, file, aProvider));
-    convertedAttachments.appendElement(item.attachment, false);
+    try {
+      let listener = new uploadListener(item.attachment, file,
+                                        aProvider);
+      aProvider.uploadFile(file, listener);
+      convertedAttachments.appendElement(item.attachment, false);
+    }
+    catch (ex) {
+      listener.onStopRequest(null, null, ex.result);
+    }
   }
-
-  Services.obs.notifyObservers(convertedAttachments,
-                               "mail:attachmentsConverted",
-                               aProvider.accountKey);
+  if (convertedAttachments.length)
+    Services.obs.notifyObservers(convertedAttachments,
+                                 "mail:attachmentsConverted",
+                                 aProvider.accountKey);
 }
 
 /**
  * Convert the selected attachments to cloud attachments.
  *
  * @param aProvider the cloud provider to upload the files to
  */
 function convertSelectedToCloudAttachment(aProvider)
--- a/mail/components/compose/content/bigFileObserver.js
+++ b/mail/components/compose/content/bigFileObserver.js
@@ -7,17 +7,18 @@ Components.utils.import("resource:///mod
 
 var gBigFileObserver = {
   bigFiles: [],
   sessionHidden: false,
 
   get hidden() {
     return this.sessionHidden ||
            !Services.prefs.getBoolPref("mail.cloud_files.enabled") ||
-           !Services.prefs.getBoolPref("mail.compose.big_attachments.notify");
+           !Services.prefs.getBoolPref("mail.compose.big_attachments.notify") ||
+           Services.io.offline;
   },
   hide: function(aPermanent) {
     if (aPermanent)
       Services.prefs.setBoolPref("mail.compose.big_attachments.notify", false);
     else
       this.sessionHidden = true;
   },
 
--- a/mail/test/mozmill/shared-modules/test-cloudfile-helpers.js
+++ b/mail/test/mozmill/shared-modules/test-cloudfile-helpers.js
@@ -18,16 +18,23 @@ const kMockID = "mock";
 
 const kDefaults = {
   type: kMockID,
   displayName: "Mock Storage",
   iconClass: "chrome://messenger/skin/icons/dropbox.png",
   accountKey: null,
   settingsURL: "",
   managementURL: "",
+  // These are taken from nsIMsgCloudFileProvider
+  authErr: 0x8055001e,
+  offlineErr: 0x80550014,
+  uploadErr: 0x8055311a,
+  uploadWouldExceedQuota: 0x8055311b,
+  uploadExceedsFileLimit: 0x8055311c,
+  uploadCanceled: 0x8055311d,
 };
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 var cfh, fdh, gMockCloudfileComponent;
 
 function setupModule(module) {