(function() { // utility function to reject the given promise with the first parameter passed to // the callback; intended as a shorthand for creating bridge call error handlers that // simply bail out when something goes wrong function reject(promise) { return function(error) { promise.reject(error); };} // utility function to add a wildcard before a filename's extension, e.g., transform // 'index.html' => 'index*.html', 'index.foo.whatever.html' => 'index.foo.whatever*.html' function wildcardify(name) { var parts = name.split("."); if (parts.length == 1) { parts[0] += "*"; } else { parts[parts.length-2] += "*"; } return parts.join(".");} // utility function that takes a name like foo.something.html and inserts `idx` // before the extension, returning, e.g., 'foo.something.2.html' function indexifyName(name, idx) { var parts = name.split("."); return parts.slice(0, parts.length-1) .concat(idx) .concat(parts[parts.length-1]) .join(".");} /** * Key/Value store for files. A collection represents a flat grouping of files in an * application's file system, and it allows you to do basic CRUD operations on the * files contained therein. Typically you don't instantiate a collection yourself; * use Ext.space.SecureFiles.get() to create one. * * The `file` objects used in this API are instances of Ext.space.files.File. * * To create a collection: * * var myCollection = Ext.space.SecureFiles.get("secrets"); * * To retrieve file contents: * * myCollection.get("someFile.txt").then(function(contents) { * // got 'em * }); * * To write file contents: * * myCollection.set("someFile.txt", "The new contents").then(function(file) { * // `file` is the Ext.space.files.File that was written * }); * * ...and more. See the individual methods for full documentation. * */Ext.define("Ext.space.files.Collection", { /** * @private * @type {String} * Root virtual path for the files in this collection */ name: null, /** * @private * @type {Object} * Hash of files descriptors in this collection, by file key */ files: null, /** * @private */ constructor: function(name) { // store the collection name and create the file cache this.name = name; this.files = {}; }, /** * Cache a file descriptor object in our local catalog. * * @private * @param {Object} obj File descriptor object (as a property bag) * @return {Ext.space.files.File} File object */ _cache: function(obj) { var file = this._makeFile(obj); var id = file.key; if (this.files[id]) { // update the cached file this.files[id]._updateWith(obj); } else { // cache a new one this.files[id] = file; } return this.files[id]; }, /** * Bulk load an array of file descriptors loaded from the bridge into our cache. * * @private * @param {Array} results Results of a bridge query * @return {Array} Array of the Ext.space.files.File objects that got cached */ _cacheResults: function(results) { return results.map(this._cache.bind(this)); }, /** * Transform a property bag file descriptor object into a real Ext.space.files.File. * * @private * @param {Object} obj File descriptor object (as a property bag) * @return {Ext.space.files.File} File object */ _makeFile: function(obj) { var file = new Ext.space.files.File(obj); file.collection = this; return file; }, /** * Retrieve an item from the local catalog, by name or File object. * * @private * @param {String} fileOrName File name as a string, or Ext.space.files.File object * @return {Ext.space.files.File} File descriptor object */ _getItemByName: function(fileOrName) { var match; Object.keys(this.files).some(function(id) { if (this.files[id].name === (typeof fileOrName === "string" ? fileOrName : fileOrName.name)) { match = this.files[id]; return true; } }, this); return match; }, /** * Query the collection for files matching the given criteria. See the main * Ext.space.SecureFiles.query() documentation for query definitions. * * @private * @param {Object} query (optional) Query object * @param {Object} options (optional) Query options * @return {Ext.space.Promise} Promise that resolves with the Ext.space.files.File * objects that match the criteria. */ _query: function(query, options) { var qry = {path: this.name}; if (query) { // copy the query as is if (query.name) { qry.name = query.name; } if (query.type) { qry.type = query.type; } if (query.createdBefore) { qry.createdBefore = query.createdBefore; } if (query.createdAfter) { qry.createdAfter = query.createdAfter; } if (query.modifiedBefore) { qry.modifiedBefore = query.modifiedBefore; } if (query.modifiedAfter) { qry.modifiedAfter = query.modifiedAfter; } } if (options) { options.collection = this; } else { options = {collection: this}; } return Ext.space.SecureFiles.query(qry, options); }, /** * Load a file descriptor from the filesystem, by name. * * @private * @param {String} name File name * @return {Ext.space.Promise} Promise that resolves with the file's cached catalog object; * if the file isn't found, the promise rejects. */ _loadFile: function(name) { // Note that this method exhibits behavior slightly different than the native // bridge's #queryFiles operation being used here. The bridge considers a // zero-length result to still be a success as long as the operation doesn't // encounter an error of some sort. Since this method is simply attempting // to load metadata for a file, if the query itself succeeds but the file // doesn't exist, we reject the promise. That allows consuming code to, e.g., // go back and create a file, then retry whatever it was doing. var result = new Ext.space.Promise(); var collection = this; this.query({ name: name }).then(function(items) { // there really only should be a single match here; what // should we do if there's more than one? if (items.length) { result.fulfill(items[0]); } else { result.reject("File not found: " + collection.name + " :: " + name); } }); return result; }, /** * Retrieve the contents of a file by key. * * @private * @param {String} key File key * @param {Ext.space.files.File} file (optional) File object, to pass file descriptor data through into the promise * @return {Ext.space.Promise} Promise that resolves with the file's contents, plus possibly the file descriptor data */ loadContents: function(key, file) { var result = new Ext.space.Promise(); Ext.space.Communicator.send({ command: "Files#getFileContents", key: key, callbacks: { onSuccess: function(contents) { result.fulfill(contents, file); }, onError: reject(result) } }); return result; }, /** * Create a file by name, with optional type, path, and contents. * * @private * @param {String} name File name * @param {Object} props (optional) Hash with extra data in .type and/or .contents * @return {Ext.space.Promise} Promise that resolves with the Ext.space.files.File object created */ _createFile: function(name, props) { var result = new Ext.space.Promise(); var collection = this; var args = { command: "Files#createFile", path: this.name, name: name, callbacks: { onSuccess: function(fileData) { result.fulfill(collection._cache(fileData)); }, onError: reject(result) } }; // add the optional parameters if (props) { if (props.type) { args.type = props.type; } if (props.contents) { args.fileData = props.contents; } } Ext.space.Communicator.send(args); return result; }, /** * Remove a file's cached object from this collection (the actual file on disk * remains untouched). * * @private * @param {Ext.space.files.File} file File object */ _disownFile: function(file) { // we don't need the cached object to be the same instance as what got passed // in here as a parameter, so we just check that there's a cached item with // the correct file key if (file.collection === this && this.files[file.key]) { delete this.files[file.key]; } }, /** * Move the given file into this collection, renaming it if a name is provided. * * @private * @param {Ext.space.files.File} file File object * @param {String} name (optional) New name for the file * @return {Ext.space.Promise} Promise that resolves when the file is done moving */ _ownFile: function(file, name) { var result = new Ext.space.Promise(); var args = { command: "Files#renameFile", key: file.key, newPath: this.name, overwrite: true, callbacks: { onSuccess: function(overwrittenKey) { // remove from the original collection object file.collection._disownFile(file); // check whether we need to unwire any loaded file objects in // any collections due to a file disappearing if (overwrittenKey) { Ext.space.SecureFiles._removeFileFromLoadedCollections(overwrittenKey); } // wire up the new associations/properties file.collection = this; file.path = this.path; if (name) { file.name = name; } this.files[file.key] = file; result.fulfill(file); }.bind(this), onError: reject(result) } }; if (name) { args.newName = name; } Ext.space.Communicator.send(args); return result; }, /** * Move the given file into this collection, renaming the file if the collection * already contains a file with the same name. * * @private * @param {Ext.space.files.File} file File object * @return {Ext.space.Promise} Promise that resolves when the file is done moving */ _ownFileSafe: function(file) { var result = new Ext.space.Promise(); // look for an existing file with the same name in the collection this.query({ name: wildcardify(file.name) }).then(function(files) { var fileNames, testName, chosenName, done = false, idx = 0; if (!files) { // didn't get even an empty array of files, which shouldn't happen result.reject("Error moving the downloaded file into the collection."); } else { // can we use the original name as is? fileNames = files.map(function(f) { return f.name; }); if (fileNames.indexOf(file.name) == -1) { chosenName = file.name; done = true; } // generate names and check them against the queried results // to avoid conflicts while (!done) { testName = indexifyName(file.name, ++idx); if (fileNames.indexOf(testName) == -1) { chosenName = testName; done = true; } } // take ownership of the file and pass it into our returned promise this._ownFile(file, chosenName).connect(result); } }.bind(this), function(error) { result.reject("Error querying the collection for conflicting files."); }); return result; }, /** * Launch the native viewer for a file by key. * * @private * @param {String} key File key * @return {Ext.space.Promise} Promise that resolves when the viewer is launched */ viewFile: function(key) { var result = new Ext.space.Promise(); Ext.space.Communicator.send({ command: "Files#viewFile", key: key, callbacks: { onSuccess: function() { result.fulfill(); }, onError: reject(result) } }); return result; }, /** * Remove a file from disk. * * @private * @param {String} key File key * @return {Ext.space.Promise} Promise that resolves when the file is removed */ removeFile: function(key) { var result = new Ext.space.Promise(); var collection = this; var file = this.files[key]; Ext.space.Communicator.send({ command: "Files#removeFile", key: key, callbacks: { onSuccess: function() { // remove the cached item from the collection's internal catalog Object.keys(collection.files).some(function(id) { if (id.toString() === file.key.toString()) { delete collection.files[id]; Ext.space.SecureFiles._removeFileFromLoadedCollections(id); return true; } }); result.fulfill(true); }, onError: reject(result) } }); return result; }, /** * Write the contents of a file by key. * * @private * @param {String} key File key * @param {String} contents Contents to write * @return {Ext.space.Promise} Promise that resolves with the File object having been written */ writeContents: function(key, contents) { // note: only call this method if you're sure the file already exists; // otherwise the bridge will invoke the onError handler var result = new Ext.space.Promise(); var collection = this; Ext.space.Communicator.send({ command: "Files#setFileContents", key: key, fileData: contents, callbacks: { onSuccess: function() { result.fulfill(collection.files[key]); }, onError: reject(result) } }); return result; }, /** * Get the file contents for a name. * * var files = Ext.space.SecureFiles.get('secrets'); * * files.get('myFile').then(function(contents){ * // do something with the contents of the file. * }); * * @param {String} name File name for which to retrieve contents * @return {Ext.space.Promise} Promise that resolves when the contents are available * */ get: function(name) { var result, item = this._getItemByName(name); var collection = this; function loadContents(catalogItem) { return collection.loadContents(catalogItem.key, catalogItem); } if (item && item.key) { // we have the key, let's go straight to loading the contents result = loadContents(item); } else { // couldn't find the key in cache (weird, why?), so re-query for the file result = this._loadFile(name).then(loadContents, function(error) { result.reject(error); }); } return result; }, /** * Write the given contents to a file. * * var files = Ext.space.SecureFiles.get('secrets'); * * files.set('myFile', 'the contents go here').then(function(file) { * // can do something with `file` here * }); * * @param {String|Object} name File name to which to write contents, or an object * with properties specifying the name and MIME type * of the file, e.g., `{name: "foo", type: "text/plain"}`. * Note that the type will only be stored if the file * is being created; if the file already exists, any * provided type will be ignored * @param {String} contents Contents to write * @return {Ext.space.Promise} Promise that resolves when the file is written */ set: function(name, contents) { var result, item, type, collection = this; if (typeof name === "object") { type = name.type; name = name.name; } item = this._getItemByName(name); function writeContents(catalogItem) { return collection.writeContents(catalogItem.key, contents); } function createWithContents() { // if the file doesn't exist, create it with the given contents collection._createFile(name, {contents: contents, type: type}).then(function(file) { result.fulfill(file); }); } if (item && item.key) { // we have the key, let's go straight to writing result = writeContents(item); } else { // couldn't find the key in cache (weird, why?), so re-query for the file this._loadFile(name).then(writeContents, createWithContents); result = new Ext.space.Promise(); } return result; }, /** * Query the collection for files matching the given criteria. See the main * Ext.space.SecureFiles.query() documentation for query definitions. * * @param {Object} query (optional) Query object * @param {String} query.name (optional) exactName.txt", "*.txt", etc... * @param {String} query.type MIME type ("text/plain", etc...) * @param {Date} query.createdBefore (optional) Date object * @param {Date} query.modifiedBefore (optional) Date object * @param {Date} query.modifiedAfter (optional) Date object * @param {String} options (optional) modifies how the results will be returned * @param {String} options.fetch (optional) "count" or "data" (the default), to return a simple count, or the complete results, respectively * @param {String} options.sortField (optional) name of the field on which to sort * @param {String} options.sortDirection (optional) "asc" or "desc" * @return {Ext.space.Promise} Promise that resolves with the Ext.space.files.File * objects that match the criteria. */ query: function(query, options) { return this._query(query, options).then(this._cacheResults.bind(this)); }, /** * Delete all of the files in this collection. * * @return {Ext.space.Promise} Promise that resolves when the files are deleted */ clear: function() { var collection = this; function removeFile(key) { return collection.removeFile(key); } return this.query().then(function(items) { return Ext.space.Promise.whenComplete(Object.keys(this.files).map(removeFile)).then(function() { return; }); }.bind(this)); }, /** * Returns a count of the total number of files in the collection. * * var secrets = Ext.space.SecureFiles.get('secrets'); * * secrets.count().then(function(count) { * // done * }); * * @return {Ext.space.Promise} Promise that resolves with the number of files in the collection */ count: function() { return this._query(null, { fetch: "count" }); }, /** * Checks to see if the given file exists. * * var secrets = Ext.space.SecureFiles.get('secrets'); * * secrets.has('myFile').then(function(hasKey) { * // check hasKey * }); * * @param {String} name Name of the file for which to search * @return {Ext.space.Promise} Promise that resolves with a boolean indicating presence of the file */ has: function(name) { var result = new Ext.space.Promise(); this._loadFile(name).then(function() { result.fulfill(true); }, function(error) { result.fulfill(false); }); return result; }, /** * Deletes the file (if present). * * var secrets = Ext.space.SecureFiles.get('secrets'); * * secrets.remove('myFile').then(function(done) { * // done * }); * * @param {String} name Name of the file to delete * @return {Ext.space.Promise} Promise that resolves when the file is deleted * */ remove: function(name) { var result, item = this._getItemByName(name); var collection = this; function removeFile(catalogItem) { return collection.removeFile(catalogItem.key); } if (item && item.key) { // we have the key, let's go straight to removing it result = removeFile(item); } else { // load it to get the key; if it's not found, act as though we succeeded result = this._loadFile(name).then(removeFile, function(error) { result.fulfill(true); }); } return result; }, /** * Launches the viewer for a file. * * var secrets = Ext.space.SecureFiles.get('secrets'); * * secrets.view('myFile').then(function() { * // launched * }); * * @param {String} name Name of the file to view * @return {Ext.space.Promise} Promise that resolves when the file viewer is launched * */ view: function(name) { var result, item = this._getItemByName(name); var collection = this; function viewFile(catalogItem) { return collection.viewFile(catalogItem.key); } if (item && item.key) { // we have the key, let's go straight to removing it result = viewFile(item); } else { // load it to get the key result = this._loadFile(name).then(viewFile, reject(result)); } return result; }, /** * Generate a list of all the names of the files in the collection, in no * particular order. * * var secrets = Ext.space.SecureFiles.get('secrets'); * * secrets.keys().then(function(keys) { * // array of file names * }); * * @return {Ext.space.Promise} Promise that will resolve when all of the keys have been collected. * */ keys: function() { return this.query().then(function(items) { return Object.keys(this.files).map(function(id) { return this.files[id].name; }, this); }.bind(this)); }, /** * Iterates over all the files in a collection * * var secrets = Ext.space.SecureFiles.get('secrets'); * * secrets.forEach(function(file) {...}).then(function() { * // done * }); * * @param {Function} callback Function to call once for each file in the collection. * As with Array.prototype.forEach, it receives three * parameters: an Ext.space.files.File object, its index * in the array being iterated, and the array of files * itself. Note however that the order of elements in * this array are NOT guaranteed in any way. * @param {Object} thisArg (optional) Value to use for `this` when executing the callback. * @return {Ext.space.Promise} Promise that resolves with an array of the File objects * operated on, after the callback has been run across the * entire collection. */ forEach: function(callback, thisArg) { var args = arguments; return this.query().then(function(items) { items.forEach.apply(items, args); return items; }); }, /** * Downloads a file from the given URL into this collection. * * If you pass overwrite: true in the args parameter, the file will be overwritten * if the name conflicts with a file that already exists. * * var secrets = Ext.space.SecureFiles.get('secrets'); * * // saves the file as 'file.html' from the URL * secrets.download({ url: 'http://example.com/some/file.html' }).then(function(file) { * // do something with the file * }); * * // overwites file.html * secrets.download({ * url: 'http://example.com/some/file.html', * overwrite: true * }).then(function(file) { * // do something with the file * }); * * @param {Object} args Download parameters * @param {String} args.url The URL of the file to be downloaded * @param {Boolean} args.overwrite true/false (default false) to determine what to do in the case of a name collision. * @return {Ext.space.Promise} Promise that resolves with the File object for the file * downloaded */ download: function(args) { var result = new Ext.space.Promise(); var collection = this; var overwrite = !!(args && args.overwrite); if (!(args && args.url)) { result.reject("Missing URL"); } else { Ext.space.Downloads.download({ url: args.url }).then(function(file, download) { collection[overwrite ? "_ownFile" : "_ownFileSafe"](file).connect(result); }, function(error) { result.reject("Download error: " + error); }); } return result; }, /** * Compress the entire collection into an archive. * * var collection = Ext.space.SecureFiles.get("somepath"); * collection.compress({ archiveName: "somefiles.zip" }).then(function(file) { * // do something with the archive file * }); * * // or specify more options: * collection.compress({ * archiveName: "somefiles.blob", * path: "myArchivePath", * type: "zip" * }).then(function(file) { * // do something with the archive file * }); * * @param {Object} args Options object * @param {String} args.archiveName Name of the archive file to create * @param {String} args.path (optional) Path into which to save the archive; defaults to "" * @param {String} args.type (optional) Compression type ("zip", "zip", "bzip2", "7zip"); * if this is omitted, the system will attempt to determine * the compression type from the archiveName, and if it * cannot be determined, defaults to "zip". * @return {Ext.space.Promise} Promise that resolves with the Ext.space.files.File * object for the new archive. */ compress: function(args) { var result = new Ext.space.Promise(); if (!args) { result.reject("Missing compression arguments"); } else if (!args.archiveName) { result.reject("Missing compressed archive name"); } else { this.query().then(function(items) { var opts = { files: items, archiveName: args.archiveName, path: args.path, type: args.type }; Ext.space.SecureFiles.compress(opts).connect(result); }); } return result; } }); })();