fix handling of being offline, and going offline in the middle of uploading cloud files, r=mconley, bug 744004
authorDavid Bienvenu <bienvenu@nventure.com>
Mon, 16 Apr 2012 08:32:54 -0700
changeset 9908 f7f5eeb7b12a37e4c3dc3971dae512760d39f523
parent 9907 74679c8cb51db675fa57057b14a841495f501ed5
child 9909 7df52c7b6e4ae5e83d2caa7fff9d49303feca0e5
push idunknown
push userunknown
push dateunknown
reviewersmconley, bug
bugs744004
fix handling of being offline, and going offline in the middle of uploading cloud files, r=mconley, 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
@@ -442,18 +442,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.
       }
     },
 
@@ -682,17 +683,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;
         }
@@ -1048,31 +1050,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");
@@ -1087,38 +1080,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,
@@ -1181,33 +1183,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;
 
@@ -1218,24 +1230,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,22 @@ const kMockID = "mock";
 
 const kDefaults = {
   type: kMockID,
   displayName: "Mock Storage",
   iconClass: "chrome://messenger/skin/icons/dropbox.png",
   accountKey: null,
   settingsURL: "",
   managementURL: "",
+  authErr: Ci.nsIMsgCloudFileProvider.authErr,
+  offlineErr: Ci.nsIMsgCloudFileProvider.offlineErr,
+  uploadErr: Ci.nsIMsgCloudFileProvider.uploadErr,
+  uploadWouldExceedQuota: Ci.nsIMsgCloudFileProvider.uploadWouldExceedQuota,
+  uploadExceedsFileLimit: Ci.nsIMsgCloudFileProvider.uploadExceedsFileLimit,
+  uploadCanceled: Ci.nsIMsgCloudFileProvider.uploadCanceled,
 };
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 var cfh, fdh, gMockCloudfileComponent;
 
 function setupModule(module) {