/** * Key/Value store for files. Files stored using this API are encrypted automatically * using Sencha Web Application Manager's security infrastructure. * * var files = Ext.space.SecureFiles.get('secrets'); * * files.get('myKey').then(function(contents){ * // do something with the content of the file. * }); * * files is an instance of Ext.space.files.Collection. See Ext.space.files.Collection for * a complete list of file operations. * * This module also allows you to run queries across all of an application's Collections: * * Ext.space.SecureFiles.query({ name: "*.txt" }).then(function(files) { * // got 'em * }); * * @aside guide secure_file_api * */Ext.define("Ext.space.SecureFiles", { singleton: true, /** * @private * @type {Object} */ collections: null, /** * @private * @type {Array} */ dummyCollections: null, /** * @private */ constructor: function() { this.collections = {}; this.dummyCollections = []; }, /** * Create a function that caches query results in a dummy collection. * * @private * @return {Function} Function that takes a query result and caches it in a dummy * collection (necessary to run queries without collections * and still allow various file operations to work correctly) */ _makeDummyCacher: function() { var collection = new Ext.space.files.Collection(); this.dummyCollections.push(collection); return collection._cache.bind(collection); }, /** * Remove a file from any loaded collections. * * When a file gets overwritten by some operation, the object in memory needs to * be removed from any collections we've loaded; this function traverses through * every collection this module is tracking and does just that. * * @private * @param {String} key File key */ _removeFileFromLoadedCollections: function(key) { var collections = this.collections, k = key.toString(); Object.keys(collections).forEach(function(path) { if (collections[path].files[k]) { delete collections[path].files[k]; } }); this.dummyCollections.forEach(function(collection) { if (collection.files[k]) { delete collection.files[k]; } }); }, /** * Get a collection by name. Collections are automatically created if they do not * exist, and multiple requests for collections with the same name will all * return the same collection object. * * @param {String} name The name of the collection to get. * @return {Ext.space.files.Collection} the secure collection. */ get: function(name) { if (!this.collections.hasOwnProperty(name)) { this.collections[name] = new Ext.space.files.Collection(name); } return this.collections[name]; }, /** * Create a file from its file Key. * The collection the file belongs to is updated * If the File's Collection is not in memory then it is automatically created. * * @param {String} key The key of the file on the file system. * * *@private */ getFile: function(key){ var _self = this; var result = new Ext.space.Promise(); Ext.space.Communicator.send({ command: "Files#getFile", key: key, callbacks: { onSuccess: function(meta) { var collection = _self.get(meta.path); var file = collection._cache(meta); result.fulfill(file); }, onError: function(error) { result.reject(error); } } }); return result; }, /** * Query the application's file system for files that match the given criteria. * * The `query` is a dictionary with data against which to match files. The fields * supported are: * * * `name`: "exactName.txt", "*.txt", etc... * * `type`: MIME type ("text/plain", etc...) * * `createdBefore`: Date object * * `createdAfter`: Date object * * `modifiedBefore`: Date object * * `modifiedAfter`: Date object * * The query will combine the criteria specified and produce an array of matching * files. If you omit the query completely, the query operation will return an * array of all files in the collection. * * The `options` is a dictionary describing how you want the results to be presented: * * * `fetch`: "count" or "data" (the default), to return a simple count, or the * complete results, respectively * * `sortField`: name of the field on which to sort * * `sortDirection`: "asc" or "desc" * * @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.createdAfter (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) { var result = new Ext.space.Promise(); var qry = {path: "*"}; // default to querying the app's entire file system // fetching just a count of results, or the results themselves? var fetch = (options && options.fetch && options.fetch.toLowerCase() === "count") ? "count" : "data"; if (query) { // copy the rest of the query as is if (query.name) { qry.name = query.name; } if (query.type) { qry.type = query.type; } if (query.hasOwnProperty("path")) { qry.path = query.path; } // convert Date objects to epoch seconds if (query.createdBefore) { qry.createdBefore = query.createdBefore.getTime() / 1000; } if (query.createdAfter) { qry.createdAfter = query.createdAfter.getTime() / 1000; } if (query.modifiedBefore) { qry.modifiedBefore = query.modifiedBefore.getTime() / 1000; } if (query.modifiedAfter) { qry.modifiedAfter = query.modifiedAfter.getTime() / 1000; } } var args = { command: "Files#queryFiles", query: qry, fetch: fetch, callbacks: { onSuccess: function(matches) { var cacheResult; // if we're fetching a count, return it directly; if we're fetching // data on behalf of a collection, return it directly as well, to // allow the collection to decide how to process it; if we're // querying across all of an application's collections, then put // things in the dummy collection and return what gets stored. if (fetch === "count" || (options && options.collection)) { result.fulfill(matches); } else { cacheResult = Ext.space.SecureFiles._makeDummyCacher(); result.fulfill(matches.map(function(match) { return cacheResult(match); })); } }, onError: function(error) { result.reject(error); } } }; if (options) { if (options.sortField) { args.sortField = options.sortField; } if (options.sortDirection) { args.sortDirection = options.sortDirection.toLowerCase(); } } Ext.space.Communicator.send(args); return result; }, /** * Compress the provided files into an archive. * * Ext.space.SecureFiles.compress({ * files: arrayOfFileObjects, * archiveName: "somefiles.zip" * }).then(function(file) { * // do something with the archive file * }); * * // or specify more options: * Ext.space.SecureFiles.compress({ * files: arrayOfFileObjects, * archiveName: "somefiles.blob", * path: "myArchivePath", * type: "zip" * }).then(function(file) { * // do something with the archive file * }); * * @param {Object} args Options object * @param {Array} args.files Array of Ext.space.files.File objects to compress into an archive * @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", "rar"); * 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.files || args.files.length === 0) { result.reject("Missing files; cannot create a compressed archive"); } else if (!args.archiveName) { result.reject("Missing compressed archive name"); } else { var getEncoding = this._getCompressionEncodingFromName.bind(this); var cmd = { command: "Compression#compress", keys: args.files.map(function(file) { return file.key; }), archiveName: args.archiveName, path: args.path || "", encoding: args.type || getEncoding(args.archiveName) || "zip", callbacks: { onSuccess: function(meta) { result.fulfill(Ext.space.SecureFiles.get(meta.path)._cache(meta)); }, onError: function(error) { result.reject(error); } } }; Ext.space.Communicator.send(cmd); } return result; }, /** * Parse the most likely compression encoding from the given filename. * * @private * @param {String} name File name to parse * @return {String} Compression encoding, or "" if unknown */ _getCompressionEncodingFromName: function(name) { var idx = name.lastIndexOf("."); var ext = name.substr(idx+1).toLowerCase(); if (idx >= 0) { if (ext == "gz" || ext == "gzip") { return "gzip"; } else if (ext == "zip") { return "zip"; } else if (ext == "rar") { return "rar"; } else if (ext == "bz2" || ext == "bzip2") { return "bzip2"; } } return ""; } });