(function(){ // utility function for returning a particular field from the item parameter, or if // item is a string, the item itself; if neither is valid, return undefined function extract(item, field) { if (item[field]) { return item[field]; } else if (typeof item == "string" || typeof item == "number") { return item; } // intentionally don't return anything } // utility function for creating a promise and wiring up callbacks if they were // provided in the args object (i.e., "callback style invocation"). function promisize(args) { var promise = new Ext.space.Promise(); if (args && (args.onComplete || args.onError)) { promise.then( typeof args.onComplete == "function" ? args.onComplete : undefined, typeof args.onError == "function" ? args.onError : undefined ); } return promise;} // utility function to filter out the weird empty items that sometimes come back from // the native side, that look like they're still in progress, but they're invalid; function downloadIsReal(item) { return item.isComplete || (!!item.totalBytes && !!item.fileName);} /** * Promise-based API for downloading files via URL. * * To download a file: * * // list some details when the download completes * function onComplete(file, download) { * Ext.space.Logger.log("Download finished: " + download.url); * Ext.space.Logger.log("Download size: " + download.totalBytes); * Ext.space.Logger.log("Saved to: " + download.fileName); * Ext.space.Logger.log("File path: " + file.path); * Ext.space.Logger.log("File name: " + file.name); * Ext.space.Logger.log("File size on disk: " + file.size); * } * * Ext.space.Downloads.download({ url: "http://www.sencha.com/" }).then(onComplete); * * To send custom request headers, add a `headers` property to the invocation: * * // specify custom request headers * Ext.space.Downloads.download({ * url: "http://www.sencha.com/", * headers: {"x-example-app": "testing123"} * }).then(...); * * The `download` objects involved here are instances of Ext.space.files.Download; * the `file` object is an Ext.space.files.File. * * To get a list of all downloads currently in progress, plus up to the ten most * recently completed downloads: * * Ext.space.Downloads.getDownloads().then(function(downloads) { * downloads.forEach(function(download) { * Ext.space.Logger.log(download.fileName); * }); * }); * * If you have a download object and want to fetch the latest information about it, * you can get the progress of a single download at a time: * * download.getProgress().then(function(updatedDownload) { * Ext.space.Logger.log(updatedDownload.bytesDownloaded + " bytes downloaded"); * }); * * Alternatively, you can wire up a progress event callback on the download: * * download.on("progress", function(updatedDownload) { * // gets called every time new progress is available * }); * * To cancel a download in progress: * * download.cancel().then(function() { * Ext.space.Logger.log("Canceled!"); * }); * * @aside guide file_locker * */Ext.define("Ext.space.Downloads", { singleton: true, /** * @private * Cache of the download information returned by the native bridge */ downloads: null, /** * @private * Whether or not the download manager has registered callbacks with the native bridge DownloadManager#watchDownloads */ watching: false, /** * @private */ constructor: function() { this.downloads = {}; }, /** * Download a file. * * @param {Object} args Upload parameters * @param {String} args.url The URL of the download. * @param {Object} args.headers Optional HTTP headers to be sent with the request. * @return {Ext.space.files.Download} Download object that will be filled with data as it becomes available */ download: function(args) { var url, manager = this, download = new Ext.space.files.Download(); // backwards compatibility with old versions of this interface, which took // event callbacks in the main args object if (args && (args.onComplete || args.onError)) { download.then( typeof args.onComplete == "function" ? args.onComplete : undefined, typeof args.onError == "function" ? args.onError: undefined ); } if (args) { url = extract(args, "url"); if (url) { if (args.onProgress) { download.on("progress", args.onProgress); } var cmd = { command: "DownloadManager#download", url: url, callbacks: { onStart: function(id) { Ext.space.Logger.debug("download start: ", id, url); if (id) { // cache a reference to the Download object, so we can // continue to update it over time manager.downloads[id] = download; } manager.watchDownloads(); }, onSuccess: function(id) { Ext.space.Logger.debug("download success: ", id); manager.getProgress(id).then(function(obj) { Ext.space.SecureFiles.getFile(obj.fileKey).then(function(file) { Ext.space.Logger.debug("Resolving successful download: ", id, file, obj); obj.done.fulfill(file, obj); }); }); }, onError: function(error) { download.done.reject(error); } } }; if (args.headers) { cmd.headers = args.headers; } Ext.space.Communicator.send(cmd); } } if (!args || !url) { download.done.reject("Missing URL"); } return download; }, /** * Retrieve the current status of all active downloads, plus the most recently * completed downloads. * * @param {Object} args (optional) Object with .onComplete and/or .onError * callback(s) to run when the download finishes * @return {Ext.space.Promise} Promise which will receive an array of * Ext.space.files.Download objects */ getDownloads: function(args) { var promise = promisize(args); var manager = this; function makeDownload(item) { var id = item.downloadId; if (manager.downloads[id]) { return manager.downloads[id]._updateWith(item); } else { manager.downloads[id] = new Ext.space.files.Download(item); return manager.downloads[id]; } } Ext.space.Communicator.send({ command: "DownloadManager#getDownloads", callbacks: { onSuccess: function(responses) { Ext.space.Logger.debug("getDownloads: ", responses); if (Object.prototype.toString.call(responses) === "[object Array]") { // resolve with an array of Download objects promise.fulfill(responses.filter(downloadIsReal).map(makeDownload)); manager.watchDownloads(); } else { // what happened? promise.reject("Malformed (non-Array) response from the native bridge"); } }, onError: function(error) { promise.reject(error); } } }); return promise; }, /** * Check a download's progress (normally done via download.getProgress()). * * @private * @param {String|Object} args Download ID of the download to check, or an object * containing a .downloadId property containing such. * @return {Ext.space.Promise} Promise which will receive an up-to-date copy of the * Ext.space.files.Download */ getProgress: function(args) { var id, promise, match, manager = this; if (args) { promise = promisize(args); id = typeof args == "number" ? args : extract(args, "downloadId"); if (id && manager.downloads[id]) { if (manager.downloads[id].isComplete) { // if it's cached and complete, return it promise.fulfill(manager.downloads[id]); } else { // if it's cached and incomplete, get it from getDownloads this.getDownloads().then(function(downloads) { downloads.some(function(download) { if (download.downloadId === id) { match = download; return true; } }); if (match) { promise.fulfill(match); } else { promise.reject("Download " + id + " not found"); } }, function(error) { promise.reject(error); }); } } } if (!args || !id) { if (!promise) { promise = new Ext.space.Promise(); } promise.reject("Missing download ID"); } else if (!manager.downloads[id]) { promise.reject("Download " + id + " not found"); } return promise; }, /** * Cancel a download (normally done via download.cancel()). * * @private * @param {String|Object} args Download ID of the download to check, or an object * containing a .downloadId property containing such. * @return {Ext.space.Promise} Promise which will resolve when the download is canceled. If * the download is already done or canceled, it will reject. */ cancel: function(args) { var id, promise = new Ext.space.Promise(), manager = this; if (args) { promise = promisize(args); id = extract(args, "downloadId"); if (id) { Ext.space.Communicator.send({ command: "DownloadManager#cancel", downloadId: id, callbacks: { onSuccess: function() { manager.downloads[id].done.reject("Canceled"); promise.fulfill(true); }, onError: function(error) { promise.reject(error); } } }); } } if (!args || !id) { promise.reject("Missing download ID"); } return promise; }, /** * Watch for updates coming in from the native bridge, to keep the internal * cache up to date * * @private */ watchDownloads: function() { var manager = this, cache = this.downloads, activeCount = 0; function processItem(item) { var id = item.downloadId, alreadyComplete = !(id in cache) || (cache[id].isComplete && !cache[id].isVolatile), justCompleted = !alreadyComplete && item.isComplete && !item.isVolatile; // count the downloads still in progress so we know when to unwatch if (item.isVolatile) { activeCount++; } // create or update the cached download object if (cache[id]) { cache[id]._updateWith(item); } else { cache[id] = new Ext.space.files.Download(item); } // resolve the original promise with the final data if (justCompleted) { Ext.space.SecureFiles.getFile(cache[id].fileKey).then(function(file) { Ext.space.Logger.debug("Resolving watched download: ", id, file, cache[id]); cache[id].done.fulfill(file, cache[id]); }); } } if (!manager.watching) { manager.watching = true; Ext.space.Communicator.send({ command: "DownloadManager#watchDownloads", callbacks: { onSuccess: function(responses) { Ext.space.Logger.debug("watchDownloads: ", responses.length, responses); activeCount = 0; if (Object.prototype.toString.call(responses) === "[object Array]") { responses.forEach(processItem); if (!activeCount) { manager.unwatchDownloads(); } } Ext.space.Logger.debug("watchDownloads activeCount: ", activeCount); }, onError: function(error) { Ext.space.Logger.debug("watchDownloads encountered an error; unwatching. Error: ", error); manager.unwatchDownloads(); } } }); } }, /** * Discontinue watching for download updates from the native bridge * * @private */ unwatchDownloads: function() { if (this.watching) { Ext.space.Communicator.send({ command: "DownloadManager#unwatchDownloads" }); this.watching = false; } }}); }());