Source: services/_BaseUploadService.js

/**
 * Copyright (C) 2005-2016 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * <p>This service can be used to control the uploading of content as well as
 * the updating the content of existing nodes on an Alfresco Repository.</p>
 * 
 * @module alfresco/services/_BaseUploadService
 * @extends module:alfresco/services/BaseService
 * @mixes module:alfresco/core/CoreXhr
 * @mixes module:alfresco/services/_UploadHistoryServiceMixin
 * @author Martin Doyle
 * @since 1.0.52
 */
define(["alfresco/core/CoreXhr", 
        "alfresco/core/topics", 
        "alfresco/services/_UploadHistoryServiceMixin", 
        "alfresco/services/BaseService", 
        "dojo/_base/array", 
        "dojo/_base/declare", 
        "dojo/_base/lang", 
        "dojo/Deferred", 
        "dojo/on", 
        "dojo/promise/all", 
        "service/constants/Default"], 
        function(CoreXhr, topics, _UploadHistoryServiceMixin, BaseService, array, declare, lang, Deferred, on, all, AlfConstants) {

   // Declare and return the class
   return declare([BaseService, CoreXhr, _UploadHistoryServiceMixin], {

      /**
       * The File object (referenced in other JSDoc comments)
       *
       * @instance
       * @typedef {object} File
       * @property {num} progress The current upload-progress as a percentage
       * @property {string} fileName The name of the file
       * @property {string} nodeRef The nodeRef that this file was uploaded to (if successful)
       * @property {object} request The request object used to upload the file
       * @property {num} state The state as per [FileUploadService.state]{@link module:alfresco/services/FileUploadService#state}
       * @property {object} uploadData Information pertaining to the upload itself
       *                               (see [constructUploadData]{@link module:alfresco/services/FileUploadService#constructUploadData})
       * @property {object} _dfd An internal deferred object that is auto-generated and
       *                         will resolve once the upload has finished (successfully
       *                         or unsuccessfully)
       */

      /**
       * An array of the i18n files to use with this widget.
       * 
       * @instance
       * @type {object[]}
       * @default [{i18nFile: "./i18n/_BaseUploadService.properties"}]
       */
      i18nRequirements: [{
         i18nFile: "./i18n/_BaseUploadService.properties"
      }],

      /**
       * The Alfresco repository features multiple upload REST APIs. Up to and including version 5.1 of Alfresco
       * supports the "version zero" REST API (also known as Share Services API) for Upload. In addition post 5.1
       * there is a "version one" Public Upload API also. Set this value to 1 to use the new version one API, else
       * the classic v0 API will be used and Share Services AMP must be applied to the repository.
       *
       * @instance
       * @type {number}
       * @default
       * @since 1.0.55
       */
      apiVersion: 0,

      /**
       * Stores references and state for each file that is in the file list. The fileId is used as
       * the key and the value is a [File object]{@link module:alfresco/services/_BaseUploadService#File}.
       *
       * @instance
       * @type {object}
       * @default
       */
      fileStore: null,

      /**
       * The maximum quantity of simultaneous uploads.
       *
       * @instance
       * @type {number}
       * @default
       */
      maxSimultaneousUploads: 1,

      /**
       * The user is browsing and adding files to the file list
       *
       * @instance
       * @type {number}
       * @default
       */
      STATE_BROWSING: 1,

      /**
       * File(s) is been added
       *
       * @instance
       * @type {number}
       * @default
       */
      STATE_ADDED: 2,

      /**
       * File(s) is being uploaded to the server
       *
       * @instance
       * @type {number}
       * @default
       */
      STATE_UPLOADING: 3,

      /**
       * All files are processed and have either failed or been successfully
       * uploaded to the server.
       *
       * @instance
       * @type {number}
       * @default
       */
      STATE_FINISHED: 4,

      /**
       * File failed to upload.
       *
       * @instance
       * @type {number}
       * @default
       */
      STATE_FAILURE: 5,

      /**
       * File was successfully STATE_SUCCESS.
       *
       * @instance
       * @type {number}
       * @default
       */
      STATE_SUCCESS: 6,

      /**
       * This state-variable is used to track the current overall progress, and is equal to
       * the number of "in progress" uploads. This is incremented by the number of files in
       * an upload-request, and reset once all of the files have either been uploaded or
       * failed to upload.
       *
       * @instance
       * @type {number}
       * @default
       */
      totalNewUploads: 0,

      /**
       * The widget that displays the uploads' progress.
       *
       * @instance
       * @type {object}
       * @default
       */
      uploadDisplayWidget: null,

      /**
       * This is the default title for the uploads container.
       *
       * @instance
       * @type {string}
       * @default
       */
      uploadsContainerTitle: "uploads-container.title",

      /**
       * This is the default title for the uploads container.
       *
       * @instance
       * @type {string}
       * @default
       */
      uploadsContainerTitleComplete: "uploads-container.title-complete",

      /**
       * This is the topic on which to publish updates to the title container.
       *
       * @instance
       * @type {string}
       * @default
       */
      uploadsContainerTitleUpdateTopic: null,

      /**
       * The topic that this service will listen to, to initiate a file upload.
       * 
       * @instance
       * @type {string}
       * @listens module:alfresco/core/topics#UPLOAD_REQUEST
       * @default
       */
      uploadTopic: topics.UPLOAD_REQUEST,

      /**
       * The location of the upload endpoint, used when using an
       * [apiVersion]{@link module:alfresco/services/_BaseUploadService#apiVersion} of 0.
       *
       * @instance
       * @type {string}
       * @default
       * @since 1.0.55
       */
      uploadURL: "api/upload",

      /**
       * The widget definition that displays the uploads' progress. This should
       * be a single widget that implements the interface defined by
       * [_UploadsDisplayMixin]{@link module:alfresco/services/_UploadsDisplayMixin}.
       *
       * @instance
       * @type {object[]}
       * @default
       */
      widgetsForUploadDisplay: null,

      /**
       * An internal counter for the currently uploading files.
       *
       * @instance
       * @type {number}
       * @default
       */
      _numUploadsInProgress: 0,

      /**
       * If a service needs to act upon its post-mixed-in state before registering subscriptions then
       * this is where it should be done. It is comparable to postMixInProperties in a widget in the
       * class lifecycle.
       *
       * @instance
       * @override
       */
      initService: function alfresco_services__BaseUploadService__initService() {
         this.inherited(arguments);
         this.reset();
      },

      /**
       * Notifies the uploads-display widget that a file could not be uploaded.
       *
       * @instance
       * @param {object} file The invalid file
       */
      addInvalidFile: function alfresco_services__BaseUploadService__addInvalidFile(file, reason) {
         var reasonMessage = reason || this.message("upload.error.reason-unknown");
         this.uploadDisplayWidget.addFailedFile(file.name, {
            reason: reasonMessage
         });
      },

      /**
       * Handle cancelled upload requests.
       *
       * @instance
       * @param {string} fileId The unique id of the file being uploaded
       * @param {object} evt The cancellation event
       */
      cancelListener: function alfresco_services__BaseUploadService__cancelListener(/*jshint unused:false*/ fileId, /*jshint unused:false*/ evt) {
         this.failureListener.apply(this, arguments); // Defer directly to the failure listener
      },

      /**
       * Constructs the upload payload object to be added to the fileStore object for each file. 
       * The object constructed is designed to work with the Alfresco REST service for uploading
       * documents. This function can be overridden to support different APIs
       *
       * @instance
       * @param {object} file The file being uploaded
       * @param {object} fileName The name of the file being uploaded
       * @param {object} targetData Information about where and how to upload the data
       */
      constructUploadData: function alfresco_services__BaseUploadService__constructUploadData(file, fileName, targetData) {

         // This is to work around the fact that pickers always return an array, even in
         // single item mode - that needs to be better resolved at some point
         var destination = targetData.destination;
         if (destination.constructor === Array) {
            destination = destination[0];
         }
         this.updateUploadHistory(destination);

         // Create object using information defined in payload
         return {
            filedata: file,
            filename: fileName,
            destination: destination,
            siteId: targetData.siteId,
            containerId: targetData.containerId,
            uploaddirectory: targetData.uploadDirectory || file.relativePath || null,
            majorVersion: targetData.majorVersion ? targetData.majorVersion : "true",
            updateNodeRef: targetData.updateNodeRef,
            description: targetData.description,
            overwrite: targetData.overwrite,
            thumbnails: targetData.thumbnails,
            username: targetData.username
         };
      },

      /**
       * Handle an error occurring during the upload.
       *
       * @instance
       * @param {string} fileId The unique id of the file being uploaded
       * @param {object} evt The failure event
       */
      failureListener: function alfresco_services__BaseUploadService__failureListener(fileId, evt) {
         var fileInfo = this.fileStore[fileId];
         if (fileInfo) {
            // Make sure we only update the UI once
            if (fileInfo.state !== this.STATE_FAILURE) {
               this.processUploadFailure(fileId, evt);
            }
         }
      },

      /**
       * Cancel the specified upload.
       *
       * @instance
       * @param {object} payload The publication payload
       * @since 1.0.56
       */
      onUploadCancelRequest: function alfresco_services__BaseUploadService__onUploadCancelRequest(payload) {
         var uploadId = payload && payload.uploadId,
            fileInfo = this.fileStore[uploadId];
         if (fileInfo) {
            try {
               if (fileInfo.progress === 0) {
                  fileInfo.request = { // Manually force status of 0 to notify of cancellation
                     status: 0
                  };
                  this.failureListener(uploadId); // Manually call the failure listener for this file
               } else {
                  fileInfo.request.abort();
               }
            } catch (e) {
               this.alfLog("info", "Unable to cancel upload: ", fileInfo, e);
            }
         }
      },

      /**
       * Handler that's called after an upload has finished (in any state)
       *
       * @instance
       * @param {string} fileId The unique id of the file being uploaded
       */
      onUploadFinished: function alfresco_services__BaseUploadService__onUploadFinished( /*jshint unused:false*/ fileId) {

         // Resolve the deferred object on the file (used for firing requested topic when uploads complete)
         var fileInfo = this.fileStore[fileId],
            dfd = fileInfo && lang.getObject("uploadData.filedata._dfd", false, fileInfo);
         dfd && dfd.resolve();

         // Update the progress information in the UI
         this.updateAggregateProgress();

         // Decrement the in-progress counter and see if there are more to upload
         this._numUploadsInProgress--;
         this.spawnFileUploads();
      },

      /**
       * The main handler for an upload request.
       * 
       * @instance
       * @param {object} payload The publication payload
       */
      onUploadRequest: function alfresco_services__BaseUploadService__onUploadRequest(payload) {

         // A files reference will take precedence over actual files in the payload
         if (lang.exists("filesRefs", payload)) {
            var files = this.alfGetData(payload.filesRefs);
            if (files) {
               payload.files = files;
            }
         }

         // Make sure we have enough information to continue
         if (payload.files && payload.targetData) {

            // Make sure the upload display widget is present
            this.showUploadsWidget().then(lang.hitch(this, function() {

               // Validate the files
               var filesToUpload = this.validateFiles(payload.files);

               // After the files are uploaded, call any final actions
               var filePromises = array.map(filesToUpload, function(file) {
                  return (file._dfd = new Deferred()).promise;
               });
               all(filePromises).then(lang.hitch(this, function() {
                  this.onUploadsBatchComplete(payload);
               }));

               // Update the total number of in-progress files
               this.totalNewUploads += filesToUpload.length;

               // Start the uploads
               this.startFileUploads(filesToUpload, payload.targetData);

               // Update the total progress
               this.updateAggregateProgress();
            }));

         } else {
            this.alfLog("warn", "A request was received to upload files but either no 'files' attribute or no 'targetData' attribute was defined", payload, this);
         }
      },

      /**
       * This listener will be called each time a batch of uploads (grouped by upload request) completes.
       *
       * @instance
       * @param {object} payload The payload containing the original upload request
       */
      onUploadsBatchComplete: function alfresco_services__BaseUploadService__onUploadsBatchComplete(payload) {
         if (payload.alfResponseTopic) {
            this.alfPublish(payload.alfResponseTopic, {
               responseScope: payload.alfResponseScope
            }, true);
         }
      },

      /**
       * Handles the closing of the uploads container.
       *
       * @instance
       */
      onUploadsContainerClosed: function alfresco_services__BaseUploadService__onUploadsContainerClosed() {
         this.reset();
      },

      /**
       * Called when an upload "load" event fires. This merely means that the
       * server has responded though, so it could still be an error.
       *
       * @instance
       * @param {object} fileId The unique identifier of the file
       * @param {object} evt The completion event
       */
      processUploadCompletion: function alfresco_services__BaseUploadService__processUploadCompletion(fileId, evt) {

         // Check the response code
         var fileInfo = this.fileStore[fileId],
            responseCode = fileInfo.request.status,
            successful = responseCode >= 200 && responseCode < 300;

         // Handle according to success
         if (successful) {

            // Get the response and update the file-info object
            var response = JSON.parse(fileInfo.request.responseText);
            switch (this.apiVersion)
            {
               case 0:
               {
                  fileInfo.nodeRef = response.nodeRef;
                  fileInfo.fileName = response.fileName;
                  fileInfo.state = this.STATE_SUCCESS;
                  break;
               }
               case 1:
               {
                  fileInfo.nodeRef = "workspace://SpacesStore/" + response.id;
                  fileInfo.fileName = response.name;
                  fileInfo.state = this.STATE_SUCCESS;
                  break;
               }
               default:
                  this.alfLog("error", "Unknown Upload API version specified: " + this.apiVersion);
            }

            // Notify uploads-display widget of completion
            this.uploadDisplayWidget.handleCompletedUpload(fileId, evt, fileInfo.request);

            // Execute post-upload actions
            this.onUploadFinished(fileId);
         }
         else {
            this.processUploadFailure(fileId, evt);
         }
      },

      /**
       * Called if a request fails or completes with a non-success status code.
       *
       * @instance 
       * @param {object} fileId The unique identifier of the file
       * @param {object} evt The completion event
       */
      processUploadFailure: function alfresco_services__BaseUploadService__processUploadFailure(fileId, evt) {
         var fileInfo = this.fileStore[fileId];
         if (fileInfo) {
            fileInfo.state = this.STATE_FAILURE;
            this.uploadDisplayWidget.handleFailedUpload(fileId, evt, fileInfo.request);
            this.onUploadFinished(fileId);
         }
      },

      /**
       * This function can be called when creating the upload display. It ensures that the root widget is correctly
       * configured to be assigned to the [widgetsForUploadDisplay]{@link module:alfresco/services/_BaseUploadService#widgetsForUploadDisplay}
       * reference. Care should be taken When overriding the 
       * [showUploadsWidget]{@link module:alfresco/services/_BaseUploadService#showUploadsWidget} to ensure that any model
       * is correctly setup by calling this function.
       * 
       * @return {object[]} The object model for rendering the upload display
       * @instance 1.0.57
       */
      processWidgetsForUploadDisplay: function alfresco_services__BaseUploadService__processWidgetsForUploadDisplay() {
         var widgets = lang.clone(this.widgetsForUploadDisplay);
         if (widgets && widgets.constructor === Array && widgets.length === 1) {
            lang.mixin(widgets[0], {
               assignTo: "uploadDisplayWidget",
               assignToScope: this
            });
         } 
         else {
            this.alfLog("error", "Must define a widget for displaying upload progress in property 'widgetsForUploadDisplay'");
         }
         return widgets;
      },

      /**
       * Register this service's subscriptions.
       * 
       * @instance
       * @override
       * @listens module:alfresco/core/topics#UPLOAD_REQUEST
       */
      registerSubscriptions: function alfresco_services_FileUploadService__registerSubscriptions() {
         this.alfSubscribe(topics.UPLOAD_REQUEST, lang.hitch(this, this.onUploadRequest));
         this.alfSubscribe(topics.CANCEL_INPROGRESS_UPLOAD, lang.hitch(this, this.onUploadCancelRequest));
      },

      /**
       * <p>Reset the state of the service.</p>
       *
       * <p><strong>NOTE:</strong> This does not cancel any in-progress uploads.</p>
       *
       * @instance
       */
      reset: function alfresco_services__BaseUploadService__reset() {
         this.fileStore = {};
         this.uploadDisplayWidget && this.uploadDisplayWidget.reset();
      },

      /**
       * This function is called when all uploads have completed and resets the
       * [totalNewUploads counter]{@link module:alfresco/services/_BaseUploadService#totalNewUploads}
       * to zero.
       * 
       * @instance
       * @since 1.0.54
       * @extendable
       */
      resetTotalUploads: function alfresco_services__BaseUploadService__resetTotalUploads() {
         this.totalNewUploads = 0;
      },

      /**
       * Ensure the uploads display widget is available
       *
       * @instance
       * @returns {object} A promise, that will resolve when the widget is ready to accept upload information.
       */
      showUploadsWidget: function alfresco_services__BaseUploadService__showUploadsWidget() {
         throw new Error("Method not overridden in extending class");
      },

      /**
       * Check to see whether there are any waiting uploads that can be started (up to the
       * [maxSimultaneousUploads]{@link module:alfresco/services/FileUploadService#maxSimultaneousUploads}).
       *
       * @instance
       */
      spawnFileUploads: function alfresco_services__BaseUploadService__spawnFileUploads() {
         array.forEach(Object.keys(this.fileStore), function(fileId) {
            var fileInfo = this.fileStore[fileId];
            if (fileInfo.state === this.STATE_ADDED) {
               this.startFileUpload(fileInfo);
            }
         }, this);
      },

      /**
       * Starts the actual upload for a file
       *
       * @instance
       * @param {object} Contains info about the file and its request.
       */
      startFileUpload: function alfresco_services__BaseUploadService__startFileUpload(fileInfo) {
         /*jshint maxstatements:false,maxcomplexity:false*/

         // Ensure we only upload the maximum allowed at a time
         if (this._numUploadsInProgress === this.maxSimultaneousUploads) {
            return;
         }

         // Increment uploads counter
         this._numUploadsInProgress++;

         // Mark file as being uploaded
         fileInfo.state = this.STATE_UPLOADING;

         // Setup variables
         var formData = new FormData(),
            uploadData = fileInfo.uploadData,
            url;

         // resolve final API URL and Form structure based on configuration and apiVersion setting
         switch (this.apiVersion)
         {
            case 0:
            {
               // Set-up the API URL
               url = AlfConstants.PROXY_URI + this.uploadURL;
               if (this.isCsrfFilterEnabled()) {
                  url += "?" + this.getCsrfParameter() + "=" + encodeURIComponent(this.getCsrfToken());
               }
               
               // Set-up the form data object
               formData.append("filedata", uploadData.filedata);
               formData.append("filename", uploadData.filename);
               formData.append("destination", uploadData.destination);
               formData.append("siteId", uploadData.siteId);
               formData.append("containerId", uploadData.containerId);
               formData.append("uploaddirectory", uploadData.uploaddirectory);
               formData.append("majorVersion", uploadData.majorVersion ? uploadData.majorVersion : "false");
               formData.append("username", uploadData.username);
               formData.append("overwrite", uploadData.overwrite);
               formData.append("thumbnails", uploadData.thumbnails);
               if (uploadData.updateNodeRef) {
                  formData.append("updateNodeRef", uploadData.updateNodeRef);
               }
               if (uploadData.description) {
                  formData.append("description", uploadData.description);
               }
               
               break;
            }
            
            case 1:
            {
               // Set-up the API URL
               url = AlfConstants.PROXY_URI + "public/alfresco/versions/1/nodes/{nodeId}/children";
               // extract node id only from expected NodeRef
               url = lang.replace(url, {
                  nodeId: uploadData.destination.split("/")[3]
               });
               if (this.isCsrfFilterEnabled()) {
                  url += "?" + this.getCsrfParameter() + "=" + encodeURIComponent(this.getCsrfToken());
               }
               
               // Set-up the form data object
               formData.append("fileData", uploadData.filedata);
               formData.append("fileName", uploadData.filename);
               formData.append("autoRename", !uploadData.overwrite);
               if (uploadData.thumbnails) {
                  formData.append("renditions", uploadData.thumbnails);
               }
               if (uploadData.uploaddirectory) {
                  formData.append("relativePath", uploadData.uploaddirectory);
               }
               
               break;
            }
            
            default:
               this.alfLog("error", "Unknown Upload API version specified: " + this.apiVersion);
         }
         
         // Open and send the request
         if (url)
         {
            fileInfo.request.open("POST", url, true);
            fileInfo.request.send(formData);
         }
      },

      /**
       * Create the file-upload requests.
       *
       * @instance
       * @param {object[]} filesToUpload The files to be uploaded
       * @param {object} targetData The data that identifies where to upload the files to.
       */
      startFileUploads: function alfresco_services__BaseUploadService__startFileUploads(filesToUpload, targetData) {

         // Recursively add files to the queue
         var nextFile;
         while ((nextFile = filesToUpload.shift())) {

            // Ensure a unique file ID
            var fileId = Date.now();
            while (this.fileStore.hasOwnProperty(fileId)) {
               fileId = Date.now();
            }

            // Add the data to the upload property of XMLHttpRequest so that we can determine which file each
            // progress update relates to (the event argument passed in the progress function does not contain
            // file name details)
            var request = new XMLHttpRequest();
            request.upload._fileData = fileId;

            // Add the event listener functions to the upload properties of the XMLHttpRequest object
            on(request.upload, "progress", lang.hitch(this, this.uploadProgressListener, fileId));
            on(request.upload, "load", lang.hitch(this, this.successListener, fileId));
            on(request.upload, "error", lang.hitch(this, this.failureListener, fileId));
            on(request.upload, "abort", lang.hitch(this, this.cancelListener, fileId));

            // Construct an object containing the data required for file upload
            // Note that we use .name and NOT .fileName which is non-standard and will break FireFox 7
            var fileName = nextFile.name,
               uploadData = this.constructUploadData(nextFile, fileName, targetData);

            // Add the upload data to the file store
            this.fileStore[fileId] = {
               state: this.STATE_ADDED,
               fileName: fileName,
               uploadData: uploadData,
               request: request,
               progress: 0
            };

            // Update the display widget with the details of the file that will be uploaded
            this.uploadDisplayWidget.addInProgressFile(fileId, nextFile);
         }

         // Start uploads
         this.spawnFileUploads();
      },

      /**
       * Handler for the upload request completing.
       * 
       * @instance
       * @param {string} fileId The unique id of the file being uploaded
       * @param {object} evt The success event
       */
      successListener: function alfresco_services__BaseUploadService__successListener(fileId, evt) {
         var fileInfo = this.fileStore[fileId];
         if (fileInfo) {
            // NOTE: There is an occasional timing issue where the upload completion event fires before the
            // readyState is correctly updated. This means that we can't check the upload actually completed
            // successfully, if this occurs then we'll attach a function to the onreadystatechange extension
            // point and things to catch up before we check everything was ok.
            if (fileInfo.request.readyState !== 4) {
               this.uploadDisplayWidget.updateUploadProgress(fileId, 100);
               fileInfo.request.onreadystatechange = lang.hitch(this, function() {
                  if (fileInfo.request.readyState === 4) {
                     this.processUploadCompletion(fileId, evt);
                  }
               });
            } else {
               this.processUploadCompletion(fileId, evt);
            }
         }
      },

      /**
       * Calculates the overall progress of all the uploads and calls the display widget with the data.
       *
       * @instance
       */
      updateAggregateProgress: function alfresco_services__BaseUploadService__updateAggregateProgress() {

         // Setup variables
         var fileIds = Object.keys(this.fileStore),
            totalPercent = this.totalNewUploads * 100,
            cumulativeProgress = 0,
            inProgressFiles = 0;

         // Run through all uploads, calculating total and current progress
         array.forEach(fileIds, function(fileId) {
            var fileInfo = this.fileStore[fileId];
            if (fileInfo.state === this.STATE_ADDED || fileInfo.state === this.STATE_UPLOADING) {
               cumulativeProgress += fileInfo.progress;
               inProgressFiles++;
            }
         }, this);

         // Add completed files to the cumulative total
         cumulativeProgress += (this.totalNewUploads - inProgressFiles) * 100;

         // Calculate total percentage and send to widget
         // NOTE: If no in-progress files, or race-condition causes zero total percent, then
         // just call it 100, because it will mean that essentially there are no pending uploads
         var currentProgressPercent = (inProgressFiles && totalPercent) ? Math.floor(cumulativeProgress / totalPercent * 100) : 100;
         this.uploadDisplayWidget.updateAggregateProgress(currentProgressPercent / 100);

         // If no longer have uploads pending, update the total-completed variable
         if (currentProgressPercent === 100) {
            this.resetTotalUploads();
         }

         // Update the container title with the aggregate progress if required
         if (this.uploadsContainerTitleUpdateTopic) {
            var title = this.message(this.uploadsContainerTitle, currentProgressPercent);
            if (currentProgressPercent === 100) {
               title = this.message(this.uploadsContainerTitleComplete);
            }
            this.alfServicePublish(this.uploadsContainerTitleUpdateTopic, {
               title: title
            });
         }
      },

      /**
       * This function listens for upload progress events retured from the XMLHttpRequest object and
       * adjusts the display to give a visual indication of how the upload for the related file is
       * progressing.
       * 
       * @instance
       * @param {string} fileId The unique id of the file being uploaded
       * @param {object} evt See [ProgressEvent on MDN]{@link https://developer.mozilla.org/en-US/docs/Web/API/ProgressEvent}
       */
      uploadProgressListener: function alfresco_services__BaseUploadService__uploadProgressListener(fileId, evt) {
         var fileInfo = this.fileStore[fileId];
         if (fileInfo && evt.lengthComputable) {
            var progress = Math.min(Math.round(evt.loaded / evt.total * 100), 100);
            this.uploadDisplayWidget.updateUploadProgress(fileId, progress);
            fileInfo.progress = progress;
            this.updateAggregateProgress();
         } else {
            this.alfLog("warn", "Unable to update upload progress for file (evt,file)", evt, fileInfo);
         }
      },

      /**
       * Validate a single file, throwing an exception if it fails.
       *
       * @instance
       * @param {object} file The file
       */
      validateFile: function alfresco_services__BaseUploadService__validateFile(file) {
         if (file.size === 0) {
            throw new Error(this.message("upload.error.empty-file"));
         }
      },

      /**
       * Validate the supplied collection of files, sending a notification of any invalid ones, and returning the valid ones.
       *
       * @instance
       * @param {object[]} files The files
       * @returns {object[]} The valid files
       */
      validateFiles: function alfresco_services__BaseUploadService__validateFiles(files) {
         return array.filter(files, function(file) {
            try {
               this.validateFile(file);
               return true;
            } catch (e) {
               this.addInvalidFile(file, e.message);
            }
         }, this);
      }
   });
});