var LibraryCLOUDFS = { $CLOUDFS__deps: ['$FS', '$MEMFS', '$PATH'], $CLOUDFS__postset: "var CLOUD_PROVIDERS; if (!CLOUD_PROVIDERS) CLOUD_PROVIDERS = (typeof CLOUD_PROVIDERS !== 'undefined' ? CLOUD_PROVIDERS : null) || {};", $CLOUDFS: { mount: function(mount) { var provider = CLOUDFS.fetchProvider(mount); if (provider) { mount.opts.provider = provider; if (!mount.opts.scope) { // backwards compat mount.opts.scope = mount.opts.cloud.applicationtoken; } mount.opts.cloud.scope = mount.opts.scope; Module.print('Cloud provider vendor: ' + provider.vendor); if (!provider.isAvailable(mount.opts.cloud)) { mount.opts.disabled = true; Module.print("WARNING: Cloud not available. Disabling Cloud Sync"); } } else { mount.opts.disabled = true; Module.print("WARNING: Cloud provider not available. Disabling Cloud Sync"); } return MEMFS.mount.apply(null, arguments); }, syncfs: function(mount, populate, callback) { if (mount.opts.disabled) { return callback(new Error("Syncing Disabled")); } if (populate) { if (!mount.opts.disabled) { CLOUDFS.syncfs_load_from_cloud(mount, function (err) { if (err) return callback(err); CLOUDFS.syncfs_load_from_idb(mount, callback); }); } else { CLOUDFS.syncfs_load_from_idb(mount, callback); } } else { CLOUDFS.syncfs_save_to_idb(mount, function(err) { if (err) return callback(err); if (!mount.opts.disabled) { CLOUDFS.syncfs_save_to_cloud(mount, callback); } }); } }, syncfs_load_from_cloud: function(mount, callback) { CLOUDFS.getRemoteSet(mount, function(err, remote) { if (err) return callback(err); CLOUDFS.getIDBSet(mount, function(err, idb) { if (err) return callback(err); CLOUDFS.reconcile(mount, remote, idb, callback); }); }); }, syncfs_load_from_idb: function(mount, callback) { CLOUDFS.getIDBSet(mount, function(err, idb) { if (err) return callback(err); CLOUDFS.getLocalSet(mount, function(err, local) { if (err) return callback(err); CLOUDFS.reconcile(mount, idb, local, callback); }); }); }, syncfs_save_to_idb: function(mount, callback) { CLOUDFS.getLocalSet(mount, function(err, local) { if (err) return callback(err); CLOUDFS.getIDBSet(mount, function(err, idb) { if (err) return callback(err); CLOUDFS.reconcile(mount, local, idb, callback); }); }); }, syncfs_save_to_cloud: function(mount, callback) { CLOUDFS.getIDBSet(mount, function(err, idb) { if (err) return callback(err); CLOUDFS.getRemoteSet(mount, function(err, remote) { if (err) return callback(err); CLOUDFS.reconcile(mount, idb, remote, callback); }); }); }, // handling the diffing of "haves" and "have nots" reconcile: function(mount, src, dst, callback) { var total = 0; var create = []; Object.keys(src.entries).forEach(function (key) { var e = src.entries[key]; var e2 = dst.entries[key]; if (!e2 || e.timestamp > e2.timestamp) { create.push(key); total++; } }); var remove = []; Object.keys(dst.entries).forEach(function (key) { var e = dst.entries[key]; var e2 = src.entries[key]; if (!e2) { remove.push(key); total++; } }); if (!total) { return callback(null); } var completed = 0; function done(err) { if (err) { if (!done.errored) { done.errored = true; return callback(err); } return; } if (++completed >= total) { return callback(null); } }; var srcFunc = CLOUDFS.getFuncSet(src.type), dstFunc = CLOUDFS.getFuncSet(dst.type); var srcCtx = srcFunc.start(mount, src, done), dstCtx = dstFunc.start(mount, dst, done); // sort paths in ascending order so directory entries are created // before the files inside them create.sort().forEach(function (path) { var pathinfo = src.entries[path]; srcFunc.load(mount, srcCtx, pathinfo, function(err, entry) { if (err) return done(err); dstFunc.store(mount, dstCtx, pathinfo, entry, done); }); }); // sort paths in descending order so files are deleted before their // parent directories remove.sort().reverse().forEach(function(path) { dstFunc.remove(mount, dstCtx, dst.entries[path], done); }); }, // Utility functions validateProvider: function(provider_name) { var provider = CLOUD_PROVIDERS[provider_name]; if (provider === undefined) return false; var requiredMethods = ['allFiles', 'read', 'write', 'rm','isAvailable']; return requiredMethods.every(function(method) { return (method in provider); }); }, fetchProvider: function(mount) { if (mount.opts.provider === undefined || CLOUD_PROVIDERS[mount.opts.provider] === undefined) { return false; } if (CLOUDFS.validateProvider( mount.opts.provider ) ) { return CLOUD_PROVIDERS[mount.opts.provider]; } else { return false; } }, populateDirs: function(entries, f, toAbsolute) { if (f.path.indexOf('/') !== -1) { // we have folders.. stuff them in the list var parts = f.path.split('/'), prefix = ''; // remove the "file" from the end parts.pop(); // remove the empty directory from the beginning if (parts[0] == '') parts.shift(); parts.forEach(function(e) { var p = prefix.length ? PATH.join2(prefix, e) : e, abs = toAbsolute(p); if (!(abs in entries)) { entries[abs] = { path: p, type: 'dir', timestamp: f.timestamp }; } prefix = p; }); } }, getFuncSet: function(type) { if (type == 'local') { return { start: function() {}, load: CLOUDFS.loadLocalEntry, store: CLOUDFS.storeLocalEntry, remove: CLOUDFS.removeLocalEntry }; } else if (type == 'remote') { return { start: function() {}, load: CLOUDFS.loadRemoteEntry, store: CLOUDFS.storeRemoteEntry, remove: CLOUDFS.removeRemoteEntry }; } else if (type == 'idb') { return { start: CLOUDFS.startIDBEntry, load: CLOUDFS.loadIDBEntry, store: CLOUDFS.storeIDBEntry, remove: CLOUDFS.removeIDBEntry }; } }, // Indexed DB Utility functions db: null, indexedDB: function() { if (typeof indexedDB !== 'undefined') return indexedDB; var ret = null; if (typeof window === 'object') ret = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; assert(ret, 'CLOUDFS used, but indexedDB not supported'); return ret; }, DB_VERSION: 1, DB_NAME: 'CLOUDFS', DB_STORE_NAME: 'FILE_DATA', getDB: function(callback) { // check the cache first var db = CLOUDFS.db; if (db) { return callback(null, db); } var req; try { req = CLOUDFS.indexedDB().open(CLOUDFS.DB_NAME, CLOUDFS.DB_VERSION); } catch (e) { return callback(e); } req.onupgradeneeded = function(e) { var db = e.target.result; var transaction = e.target.transaction; var fileStore; if (db.objectStoreNames.contains(CLOUDFS.DB_STORE_NAME)) { fileStore = transaction.objectStore(CLOUDFS.DB_STORE_NAME); } else { fileStore = db.createObjectStore(CLOUDFS.DB_STORE_NAME);//, {keyPath: 'key'}); } fileStore.createIndex('scope','scope',{ unique: false }); }; req.onsuccess = function() { db = req.result; // add to the cache CLOUDFS.db = db; callback(null, db); }; req.onerror = function() { callback(this.error); }; }, // Getting list of entities getLocalSet: function(mount, callback) { function isRealDir(p) { return p !== '.' && p !== '..'; }; function toAbsolute(root) { return function(p) { return PATH.join2(root, p); }; }; function checkPath(path) { for (var i = 0, l = mount.opts.filters.length; i < l; ++i) { var f = mount.opts.filters[i]; if (typeof f == 'string') { if (path.lastIndexOf(f, 0) == 0) { return true; } } if (typeof f == 'function') { if (f(path)) { return true; } } if (f instanceof RegExp) { if (f.test(path)) { return true; } } } return false; }; var entries = {}, shouldFilter = false, check = FS.readdir(mount.mountpoint).filter(isRealDir); if (mount.opts.filters && mount.opts.filters.length) { shouldFilter = true; } while (check.length) { var path = check.pop(), stat, keep = true, is_dir = false, abs_path = PATH.join2(mount.mountpoint, path); try { stat = FS.stat(abs_path); } catch (e) { return callback(e); } if (FS.isDir(stat.mode)) { check.push.apply(check, FS.readdir(abs_path).filter(isRealDir).map(toAbsolute(path))); is_dir = true; } else if (shouldFilter) { keep = checkPath(path); } if (keep) { entries[abs_path] = { timestamp: stat.mtime, path: path, type: is_dir ? 'dir' : 'file' }; } } return callback(null, { type: 'local', entries: entries }); }, getIDBSet: function(mount, callback) { var entries = {}, toAbsolute = function(p) { return PATH.join2(mount.mountpoint, p); }; CLOUDFS.getDB(function(err, db) { if (err) return callback(err); var transaction = db.transaction([CLOUDFS.DB_STORE_NAME], 'readonly'); transaction.onerror = function() { callback(this.error); }; var store = transaction.objectStore(CLOUDFS.DB_STORE_NAME); var index = store.index('scope'); index.openCursor(IDBKeyRange.only(mount.opts.scope)).onsuccess = function(event) { var cursor = event.target.result; if (!cursor) { return callback(null, { type: 'idb', db: db, entries: entries }); } entries[PATH.join2(mount.mountpoint, cursor.value.path)] = { path: cursor.value.path, type: FS.isDir(cursor.value.mode) ? 'dir' : 'file', timestamp: cursor.value.timestamp }; cursor.continue(); }; }); }, getRemoteSet: function(mount, callback) { mount.opts.provider.allFiles(mount.opts.cloud, function(data) { var entries = {}, toAbsolute = function(p) { return PATH.join2(mount.mountpoint, p); }; for(var k in data) { var f = data[k]; CLOUDFS.populateDirs(entries, f, toAbsolute); var p = toAbsolute(f.path); entries[p] = { url: f.url, path: f.path.trim('/'), type: 'file', timestamp: f.timestamp, size: f.size }; } return callback(null, { type: 'remote', entries: entries } ); }, function(e) { callback(e || new Error('failed request')); }); }, // Local file access loadLocalEntry: function(mount, ctx, pathinfo, callback) { var stat, node, path = PATH.join2(mount.mountpoint, pathinfo.path); try { var lookup = FS.lookupPath(path); node = lookup.node; stat = FS.stat(path); } catch (e) { return callback(e); } if (FS.isDir(stat.mode)) { return callback(null, { timestamp: stat.mtime, mode: stat.mode }); } else if (FS.isFile(stat.mode)) { // Performance consideration: storing a normal JavaScript array to a IndexedDB is much slower than storing a typed array. // Therefore always convert the file contents to a typed array first before writing the data to IndexedDB. node.contents = MEMFS.getFileDataAsTypedArray(node); return callback(null, { timestamp: stat.mtime, mode: stat.mode, contents: node.contents }); } else { return callback(new Error('node type not supported')); } }, storeLocalEntry: function(mount, ctx, pathinfo, entry, callback) { var path = PATH.join2(mount.mountpoint, pathinfo.path); try { if (FS.isDir(entry.mode)) { try { FS.mkdir(path, entry.mode); } catch(e) { // ignore existing dirs } } else if (FS.isFile(entry.mode)) { FS.writeFile(path, entry.contents, { encoding: 'binary', canOwn: true }); } else { return callback(new Error('node type not supported')); } FS.utime(path, entry.timestamp, entry.timestamp); } catch (e) { return callback(e); } callback(null); }, removeLocalEntry: function(mount, ctx, pathinfo, callback) { var path = PATH.join2(mount.mountpoint, pathinfo.path); try { var lookup = FS.lookupPath(path); var stat = FS.stat(path); if (FS.isDir(stat.mode)) { try { FS.rmdir(path); } catch(e) { // it's ok if we can't remove the local folder.. it could be filtered files are in there } } else if (FS.isFile(stat.mode)) { FS.unlink(path); } } catch (e) { return callback(e); } callback(null); }, // IDB File access buildIDBKey: function(mount, pathinfo) { return (mount.opts.scope || '') + ':' + pathinfo.path; }, startIDBEntry: function(mount, dataset, ondone) { return {db: dataset.db}; }, loadIDBEntry: function(mount, ctx, pathinfo, callback) { var tx = ctx.db.transaction([CLOUDFS.DB_STORE_NAME], 'readwrite'); tx.onerror = function() { callback(this.error); }; var store = tx.objectStore(CLOUDFS.DB_STORE_NAME); var req = store.get(CLOUDFS.buildIDBKey(mount, pathinfo)); req.onsuccess = function(event) { callback(null, event.target.result); }; req.onerror = function() { callback(this.error); }; }, storeIDBEntry: function(mount, ctx, pathinfo, entry, callback) { var tx = ctx.db.transaction([CLOUDFS.DB_STORE_NAME], 'readwrite'); tx.onerror = function() { callback(this.error); }; var store = tx.objectStore(CLOUDFS.DB_STORE_NAME); // keep scope with entry var d = { scope: mount.opts.scope, path: pathinfo.path, mode: entry.mode, timestamp: entry.timestamp }; if (entry.contents) d.contents = entry.contents; var req = store.put(d, CLOUDFS.buildIDBKey(mount, pathinfo)); req.onsuccess = function() { callback(null); }; req.onerror = function() { callback(this.error); }; }, removeIDBEntry: function(mount, ctx, pathinfo, callback) { var tx = ctx.db.transaction([CLOUDFS.DB_STORE_NAME], 'readwrite'); tx.onerror = function() { callback(this.error); }; var store = tx.objectStore(CLOUDFS.DB_STORE_NAME); var req = store.delete(CLOUDFS.buildIDBKey(mount, pathinfo)); req.onsuccess = function() { callback(null); }; req.onerror = function() { callback(this.error); }; }, // Remote file access loadRemoteEntry: function(mount, ctx, pathinfo, callback) { if (pathinfo.type == 'file') { mount.opts.provider.read(mount.opts.cloud, pathinfo.url, function(data) { // ensure data is in Uint8Array var u8data = new Uint8Array(data); callback(null, { contents: u8data, timestamp: pathinfo.timestamp, mode: CLOUDFS._FILE_MODE }); }, function(e) { callback(e); }); } else { callback(null, { timestamp: pathinfo.timestamp, mode: CLOUDFS._DIR_MODE }); } }, storeRemoteEntry: function(mount, ctx, pathinfo, entry, callback) { if (FS.isFile(entry.mode)) { mount.opts.provider.write(mount.opts.cloud, pathinfo, entry.contents, function() { callback(null); }, function(e) { callback(e); }) } }, removeRemoteEntry: function(mount, ctx, pathinfo, callback) { if (pathinfo.type == 'file') { mount.opts.provider.rm(mount.opts.cloud, pathinfo, function() { callback(null); }, function(e) { callback(e); }); } }, _FILE_MODE: {{{ cDefine('S_IFREG') | 0777 }}}, _DIR_MODE: {{{ cDefine('S_IFDIR') | 0777 }}} } }; autoAddDeps(LibraryCLOUDFS, '$CLOUDFS'); mergeInto(LibraryManager.library, LibraryCLOUDFS);