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")); } CLOUDFS.getLocalSet(mount, function(err, local) { if (err) return callback(err); CLOUDFS.getRemoteSet(mount, function(err, remote) { if (err) return callback(err); var src = populate ? remote : local; var dst = populate ? local : remote; CLOUDFS.reconcile(mount, src, dst, 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); } }; // 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]; if (dst.type === 'local') { CLOUDFS.loadRemoteEntry(mount, pathinfo, function (err, entry) { if (err) return done(err); CLOUDFS.storeLocalEntry(path, entry, done); }); } else { CLOUDFS.loadLocalEntry(path, function (err, entry) { if (err) return done(err); CLOUDFS.storeRemoteEntry(mount, pathinfo, entry, done); }); } }); // sort paths in descending order so files are deleted before their // parent directories remove.sort().reverse().forEach(function(path) { if (dst.type === 'local') { CLOUDFS.removeLocalEntry(path, done); } else { CLOUDFS.removeRemoteEntry(mount, 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; }); } }, // 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, 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))); } else if (shouldFilter) { keep = checkPath(path); } if (keep) { entries[abs_path] = { timestamp: stat.mtime, path: path }; } } return callback(null, { type: 'local', entries: entries }); }, 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')); }); }, // Fetching local and remote files loadLocalEntry: function(path, callback) { var stat, node; 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')); } }, loadRemoteEntry: function(mount, pathinfo, callback) { if (pathinfo.type == 'file') { mount.opts.provider.read(mount.opts.cloud, pathinfo.url, function(data) { callback(null, { contents: data, timestamp: pathinfo.timestamp, mode: {{{ cDefine('S_IFREG') | 0777 }}} }); }, function(e) { callback(e); }); } else { callback(null, { timestamp: pathinfo.timestamp, mode: {{{ cDefine('S_IFDIR') | 0777 }}} }); } }, // storing local and remote files storeLocalEntry: function(path, entry, callback) { 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); }, storeRemoteEntry: function(mount, 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); }) } }, // remove local and remote files removeLocalEntry: function(path, callback) { 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); }, removeRemoteEntry: function(mount, pathinfo, callback) { if (pathinfo.type == 'file') { mount.opts.provider.rm(mount.opts.cloud, pathinfo, function() { callback(null); }, function(e) { callback(e); }); } } } }; autoAddDeps(LibraryCLOUDFS, '$CLOUDFS'); mergeInto(LibraryManager.library, LibraryCLOUDFS);