diff --git a/config/config.example.js b/config/config.example.js index b896a0363..eb134591c 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -219,6 +219,29 @@ module.exports = { */ inactiveTime: 90, // days + /* CryptPad can be configured to remove inactive data which has not been pinned. + * Deletion of data is always risky and as an operator you have the choice to + * archive data instead of deleting it outright. Set this value to true if + * you want your server to archive files and false if you want to keep using + * the old behaviour of simply removing files. + * + * WARNING: this is not implemented universally, so at the moment this will + * only apply to the removal of 'channels' due to inactivity. + */ + retainData: true, + + /* As described above, CryptPad offers the ability to archive some data + * instead of deleting it outright. This archived data still takes up space + * and so you'll probably still want to remove these files after a brief period. + * The intent with this feature is to provide a safety net in case of accidental + * deletion. Set this value to the number of days you'd like to retain + * archived data before it's removed permanently. + * + * If 'retainData' is set to false, there will never be any archived data + * to remove. + */ + archiveRetentionTime: 15, + /* Max Upload Size (bytes) * this sets the maximum size of any one file uploaded to the server. * anything larger than this size will be rejected @@ -245,12 +268,21 @@ module.exports = { * ===================== */ /* - CryptPad stores each document in an individual file on your hard drive. - Specify a directory where files should be stored. - It will be created automatically if it does not already exist. - */ + * CryptPad stores each document in an individual file on your hard drive. + * Specify a directory where files should be stored. + * It will be created automatically if it does not already exist. + */ filePath: './datastore/', + /* CryptPad offers the ability to archive data for a configurable period + * before deleting it, allowing a means of recovering data in the event + * that it was deleted accidentally. + * + * To set the location of this archive directory to a custom value, change + * the path below: + */ + archivePath: './data/archive', + /* CryptPad allows logged in users to request that particular documents be * stored by the server indefinitely. This is called 'pinning'. * Pin requests are stored in a pin-store. The location of this store is diff --git a/package-lock.json b/package-lock.json index daafab1d5..30b43f03e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,26 +99,15 @@ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, "chainpad-server": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/chainpad-server/-/chainpad-server-3.0.1.tgz", - "integrity": "sha512-1r53gYvPlrnZg0vf91gP3pqHILfi67oSo3cnj7kcvC4Y/n4t6wS3QCCjXeNArOuZv/sIByuKkeo1929osr1/KA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/chainpad-server/-/chainpad-server-3.0.2.tgz", + "integrity": "sha512-c5aEljVAapDKKs0+Rt2jymKAszm8X4ZeLFNJj1yxflwBqoh0jr8OANYvbfjtNaYFe2Wdflp/1i4gibYX4IMc+g==", "requires": { "nthen": "^0.1.8", "pull-stream": "^3.6.9", "stream-to-pull-stream": "^1.7.3", "tweetnacl": "~0.12.2", - "ws": "^1.0.1" - }, - "dependencies": { - "ws": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", - "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", - "requires": { - "options": ">=0.0.5", - "ultron": "1.0.x" - } - } + "ws": "^3.3.1" } }, "chalk": { @@ -827,11 +816,6 @@ "wrappy": "1" } }, - "options": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", - "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=" - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -1248,9 +1232,9 @@ } }, "ultron": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", - "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" }, "uniq": { "version": "1.0.1", @@ -1298,13 +1282,6 @@ "async-limiter": "~1.0.0", "safe-buffer": "~5.1.0", "ultron": "~1.1.0" - }, - "dependencies": { - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" - } } }, "xml2js": { diff --git a/package.json b/package.json index 12a3dd15c..06c187f11 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "url": "git://github.com/xwiki-labs/cryptpad.git" }, "dependencies": { - "chainpad-server": "~3.0.0", + "chainpad-server": "~3.0.2", "express": "~4.16.0", "fs-extra": "^7.0.0", "nthen": "~0.1.0", diff --git a/rpc.js b/rpc.js index adc869432..a69033cc1 100644 --- a/rpc.js +++ b/rpc.js @@ -814,6 +814,7 @@ var clearOwnedChannel = function (Env, channelId, unsafeKey, cb) { return void cb('INSUFFICIENT_PERMISSIONS'); } + // FIXME COLDSTORAGE return void Env.msgStore.clearChannel(channelId, function (e) { cb(e); }); @@ -900,6 +901,20 @@ var removeOwnedChannel = function (Env, channelId, unsafeKey, cb) { if (metadata.owners.indexOf(unsafeKey) === -1) { return void cb('INSUFFICIENT_PERMISSIONS'); } + + // if the admin has configured data retention... + // temporarily archive the file instead of removing it + if (Env.retainData) { + return void Env.msgStore.archiveChannel(channelId, function (e) { + Log.info('ARCHIVAL_CHANNEL_BY_OWNER_RPC', { + unsafeKey: unsafeKey, + channelId: channelId, + status: e? String(e): 'SUCCESS', + }); + cb(e); + }); + } + return void Env.msgStore.removeChannel(channelId, function (e) { Log.info('DELETION_CHANNEL_BY_OWNER_RPC', { unsafeKey: unsafeKey, @@ -1430,6 +1445,7 @@ var removeLoginBlock = function (Env, msg, cb) { return void cb('E_INVALID_BLOCK_PATH'); } + // FIXME COLDSTORAGE Fs.unlink(path, function (err) { Log.info('DELETION_BLOCK_BY_OWNER_RPC', { publicKey: publicKey, @@ -1645,6 +1661,7 @@ RPC.create = function ( }; var Env = { + retainData: config.retainData || false, defaultStorageLimit: config.defaultStorageLimit, maxUploadSize: config.maxUploadSize || (20 * 1024 * 1024), Sessions: {}, diff --git a/scripts/delete-inactive.js b/scripts/delete-inactive.js index 9dbfb0707..1cbe799d4 100644 --- a/scripts/delete-inactive.js +++ b/scripts/delete-inactive.js @@ -7,6 +7,14 @@ const config = require("../lib/load-config"); if (!config.inactiveTime || typeof(config.inactiveTime) !== "number") { return; } +/* Instead of this script you should probably use + evict-inactive.js which moves things to an archive directory + in case the data that would have been deleted turns out to be important. + it also handles removing that archived data after a set period of time + + it only works for channels at the moment, though, and nothing else. +*/ + let inactiveTime = +new Date() - (config.inactiveTime * 24 * 3600 * 1000); let inactiveConfig = { unpinned: true, diff --git a/scripts/diagnose-archive-conflicts.js b/scripts/diagnose-archive-conflicts.js new file mode 100644 index 000000000..8617150fc --- /dev/null +++ b/scripts/diagnose-archive-conflicts.js @@ -0,0 +1,63 @@ +var nThen = require("nthen"); + +var Store = require("../storage/file"); +var config = require("../lib/load-config"); + +var store; +var Log; +nThen(function (w) { + // load the store which will be used for iterating over channels + // and performing operations like archival and deletion + Store.create(config, w(function (_) { + store = _; + })); + + // load the logging module so that you have a record of which + // files were archived or deleted at what time + var Logger = require("../lib/log"); + Logger.create(config, w(function (_) { + Log = _; + })); +}).nThen(function (w) { + // count the number of files which have been restored in this run + var conflicts = 0; + + var handler = function (err, item, cb) { + if (err) { + Log.error('DIAGNOSE_ARCHIVE_CONFLICTS_ITERATION', err); + return void cb(); + } + + // check if such a file exists on the server + store.isChannelAvailable(item.channel, function (err, available) { + // weird edge case? + if (err) { return void cb(); } + + // the channel doesn't exist in the database + if (!available) { return void cb(); } + + // the channel is available + // that means it's a duplicate of something in the archive + conflicts++; + Log.info('DIAGNOSE_ARCHIVE_CONFLICT_DETECTED', item.channel); + cb(); + }); + }; + + // if you hit an error, log it + // otherwise, when there are no more channels to process + // log some stats about how many were removed + var done = function (err) { + if (err) { + return Log.error('DIAGNOSE_ARCHIVE_CONFLICTS_FINAL_ERROR', err); + } + Log.info('DIAGNOSE_ARCHIVE_CONFLICTS_COUNT', conflicts); + }; + + store.listArchivedChannels(handler, w(done)); +}).nThen(function () { + // the store will keep this script running if you don't shut it down + store.shutdown(); + Log.shutdown(); +}); + diff --git a/scripts/evict-inactive.js b/scripts/evict-inactive.js new file mode 100644 index 000000000..ff9a3c343 --- /dev/null +++ b/scripts/evict-inactive.js @@ -0,0 +1,169 @@ +var nThen = require("nthen"); + +var Store = require("../storage/file"); +var Pinned = require("./pinned"); +var config = require("../lib/load-config"); + +// the administrator should have set an 'inactiveTime' in their config +// if they didn't, just exit. +if (!config.inactiveTime || typeof(config.inactiveTime) !== "number") { return; } + +// files which have not been changed since before this date can be considered inactive +var inactiveTime = +new Date() - (config.inactiveTime * 24 * 3600 * 1000); + +// files which were archived before this date can be considered safe to remove +var retentionTime = +new Date() - (config.archiveRetentionTime * 24 * 3600 * 1000); + +var store; +var pins; +var Log; +nThen(function (w) { + // load the store which will be used for iterating over channels + // and performing operations like archival and deletion + Store.create(config, w(function (_) { + store = _; + })); // load the list of pinned files so you know which files + // should not be archived or deleted + Pinned.load(w(function (err, _) { + if (err) { + w.abort(); + return void console.error(err); + } + pins = _; + }), { + pinPath: config.pinPath, + }); + + // load the logging module so that you have a record of which + // files were archived or deleted at what time + var Logger = require("../lib/log"); + Logger.create(config, w(function (_) { + Log = _; + })); +}).nThen(function (w) { + // this block will iterate over archived channels and remove them + // if they've been in cold storage for longer than your configured archive time + + // if the admin has not set an 'archiveRetentionTime', this block makes no sense + // so just skip it + if (typeof(config.archiveRetentionTime) !== "number") { return; } + + // count the number of files which have been removed in this run + var removed = 0; + + var handler = function (err, item, cb) { + if (err) { + Log.error('EVICT_ARCHIVED_CHANNEL_ITERATION', err); + return void cb(); + } + // don't mess with files that are freshly stored in cold storage + // based on ctime because that's changed when the file is moved... + if (+new Date(item.ctime) > retentionTime) { + return void cb(); + } + + // but if it's been stored for the configured time... + // expire it + store.removeArchivedChannel(item.channel, w(function (err) { + if (err) { + Log.error('EVICT_ARCHIVED_CHANNEL_REMOVAL_ERROR', { + error: err, + channel: item.channel, + }); + return void cb(); + } + Log.info('EVICT_ARCHIVED_CHANNEL_REMOVAL', item.channel); + removed++; + cb(); + })); + }; + + // if you hit an error, log it + // otherwise, when there are no more channels to process + // log some stats about how many were removed + var done = function (err) { + if (err) { + return Log.error('EVICT_ARCHIVED_FINAL_ERROR', err); + } + Log.info('EVICT_ARCHIVED_CHANNELS_REMOVED', removed); + }; + + store.listArchivedChannels(handler, w(done)); +}).nThen(function (w) { + var removed = 0; + var channels = 0; + var archived = 0; + + var handler = function (err, item, cb) { + channels++; + if (err) { + Log.error('EVICT_CHANNEL_ITERATION', err); + return void cb(); + } + // check if the database has any ephemeral channels + // if it does it's because of a bug, and they should be removed + if (item.channel.length === 34) { + return void store.removeChannel(item.channel, w(function (err) { + if (err) { + Log.error('EVICT_EPHEMERAL_CHANNEL_REMOVAL_ERROR', { + error: err, + channel: item.channel, + }); + return void cb(); + } + Log.info('EVICT_EPHEMERAL_CHANNEL_REMOVAL', item.channel); + cb(); + })); + } + + // bail out if the channel was modified recently + if (+new Date(item.mtime) > inactiveTime) { return void cb(); } + + // ignore the channel if it's pinned + if (pins[item.channel]) { return void cb(); } + + // if the server is configured to retain data, archive the channel + if (config.retainData) { + return void store.archiveChannel(item.channel, w(function (err) { + if (err) { + Log.error('EVICT_CHANNEL_ARCHIVAL_ERROR', { + error: err, + channel: item.channel, + }); + return void cb(); + } + Log.info('EVICT_CHANNEL_ARCHIVAL', item.channel); + archived++; + cb(); + })); + } + + // otherwise remove it + store.removeChannel(item.channel, w(function (err) { + if (err) { + Log.error('EVICT_CHANNEL_REMOVAL_ERROR', { + error: err, + channel: item.channel, + }); + return void cb(); + } + Log.info('EVICT_CHANNEL_REMOVAL', item.channel); + removed++; + cb(); + })); + }; + + var done = function () { + if (config.retainData) { + return void Log.info('EVICT_CHANNELS_ARCHIVED', archived); + } + return void Log.info('EVICT_CHANNELS_REMOVED', removed); + }; + + store.listChannels(handler, w(done)); +}).nThen(function () { + // the store will keep this script running if you don't shut it down + store.shutdown(); + Log.shutdown(); +}); + diff --git a/scripts/restore-archived.js b/scripts/restore-archived.js new file mode 100644 index 000000000..a420e35e5 --- /dev/null +++ b/scripts/restore-archived.js @@ -0,0 +1,61 @@ +var nThen = require("nthen"); + +var Store = require("../storage/file"); +var config = require("../lib/load-config"); + +var store; +var Log; +nThen(function (w) { + // load the store which will be used for iterating over channels + // and performing operations like archival and deletion + Store.create(config, w(function (_) { + store = _; + })); + + // load the logging module so that you have a record of which + // files were archived or deleted at what time + var Logger = require("../lib/log"); + Logger.create(config, w(function (_) { + Log = _; + })); +}).nThen(function (w) { + // count the number of files which have been restored in this run + var restored = 0; + + var handler = function (err, item, cb) { + if (err) { + Log.error('RESTORE_ARCHIVED_CHANNEL_ITERATION', err); + return void cb(); + } + + store.restoreArchivedChannel(item.channel, w(function (err) { + if (err) { + Log.error('RESTORE_ARCHIVED_CHANNEL_RESTORATION_ERROR', { + error: err, + channel: item.channel, + }); + return void cb(); + } + Log.info('RESTORE_ARCHIVED_CHANNEL_RESTORATION', item.channel); + restored++; + cb(); + })); + }; + + // if you hit an error, log it + // otherwise, when there are no more channels to process + // log some stats about how many were removed + var done = function (err) { + if (err) { + return Log.error('RESTORE_ARCHIVED_FINAL_ERROR', err); + } + Log.info('RESTORE_ARCHIVED_CHANNELS_RESTORED', restored); + }; + + store.listArchivedChannels(handler, w(done)); +}).nThen(function () { + // the store will keep this script running if you don't shut it down + store.shutdown(); + Log.shutdown(); +}); + diff --git a/storage/file.js b/storage/file.js index ba092a14e..99272fc6f 100644 --- a/storage/file.js +++ b/storage/file.js @@ -5,6 +5,7 @@ var Fs = require("fs"); var Fse = require("fs-extra"); var Path = require("path"); var nThen = require("nthen"); +var Semaphore = require("saferphore"); const ToPull = require('stream-to-pull-stream'); const Pull = require('pull-stream'); @@ -14,10 +15,18 @@ const isValidChannelId = function (id) { /^[a-zA-Z0-9=+-]*$/.test(id); }; +// 511 -> octal 777 +// read, write, execute permissions flag +const PERMISSIVE = 511; + var mkPath = function (env, channelId) { return Path.join(env.root, channelId.slice(0, 2), channelId) + '.ndjson'; }; +var mkArchivePath = function (env, channelId) { + return Path.join(env.archiveRoot, 'datastore', channelId.slice(0, 2), channelId) + '.ndjson'; +}; + var getMetadataAtPath = function (Env, path, cb) { var remainder = ''; var stream = Fs.createReadStream(path, { encoding: 'utf8' }); @@ -68,7 +77,7 @@ var closeChannel = function (env, channelName, cb) { } }; -var clearChannel = function (env, channelId, cb) { // FIXME deletion +var clearChannel = function (env, channelId, cb) { var path = mkPath(env, channelId); getMetadataAtPath(env, path, function (e, metadata) { if (e) { return cb(new Error(e)); } @@ -189,8 +198,7 @@ var checkPath = function (path, callback) { callback(err); return; } - // 511 -> octal 777 - Fse.mkdirp(Path.dirname(path), 511, function (err) { + Fse.mkdirp(Path.dirname(path), PERMISSIVE, function (err) { if (err && err.code !== 'EEXIST') { callback(err); return; @@ -200,11 +208,128 @@ var checkPath = function (path, callback) { }); }; -var removeChannel = function (env, channelName, cb) { // FIXME deletion +var removeChannel = function (env, channelName, cb) { var filename = mkPath(env, channelName); Fs.unlink(filename, cb); }; +// pass in the path so we can reuse the same function for archived files +var channelExists = function (filepath, channelName, cb) { + Fs.stat(filepath, function (err, stat) { + if (err) { + if (err.code === 'ENOENT') { + // no, the file doesn't exist + return void cb(void 0, false); + } + return void cb(err); + } + if (!stat.isFile()) { return void cb("E_NOT_FILE"); } + return void cb(void 0, true); + }); +}; + +var removeArchivedChannel = function (env, channelName, cb) { + var filename = mkArchivePath(env, channelName); + Fs.unlink(filename, cb); +}; + +var listChannels = function (root, handler, cb) { + // do twenty things at a time + var sema = Semaphore.create(20); + + var dirList = []; + + nThen(function (w) { + // the root of your datastore contains nested directories... + Fs.readdir(root, w(function (err, list) { + if (err) { + w.abort(); + // TODO check if we normally return strings or errors + return void cb(err); + } + dirList = list; + })); + }).nThen(function (w) { + // search inside the nested directories + // stream it so you don't put unnecessary data in memory + var wait = w(); + dirList.forEach(function (dir) { + sema.take(function (give) { + var nestedDirPath = Path.join(root, dir); + Fs.readdir(nestedDirPath, w(give(function (err, list) { + if (err) { return void handler(err); } // Is this correct? + + list.forEach(function (item) { + // ignore things that don't match the naming pattern + if (/^\./.test(item) || !/[0-9a-fA-F]{32,}\.ndjson$/.test(item)) { return; } + var filepath = Path.join(nestedDirPath, item); + var channel = filepath.replace(/\.ndjson$/, '').replace(/.*\//, ''); + if ([32, 34].indexOf(channel.length) === -1) { return; } + + // otherwise throw it on the pile + sema.take(function (give) { + var next = w(give()); + Fs.stat(filepath, w(function (err, stats) { + if (err) { + return void handler(err); + } + + handler(void 0, { + channel: channel, + atime: stats.atime, + mtime: stats.mtime, + ctime: stats.ctime, + size: stats.size, + }, next); + })); + }); + }); + }))); + }); + }); + wait(); + }).nThen(function () { + cb(); + }); +}; + +// move a channel's log file from its current location +// to an equivalent location in the cold storage directory +var archiveChannel = function (env, channelName, cb) { + if (!env.retainData) { + return void cb("ARCHIVES_DISABLED"); + } + + // ctime is the most reliable indicator of when a file was archived + // because it is used to indicate changes to the files metadata + // and not its contents + // if we find that this is not reliable in production, we can update it manually + // https://nodejs.org/api/fs.html#fs_fs_utimes_path_atime_mtime_callback + + // check what the channel's path should be (in its current location) + var currentPath = mkPath(env, channelName); + + // construct a parallel path in the new location + var archivePath = mkArchivePath(env, channelName); + + // use Fse.move to move it, Fse makes paths to the directory when you use it. + // https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/move.md + Fse.move(currentPath, archivePath, { overwrite: true }, cb); +}; + +var unarchiveChannel = function (env, channelName, cb) { + // very much like 'archiveChannel' but in the opposite direction + + // the file is currently archived + var currentPath = mkArchivePath(env, channelName); + var unarchivedPath = mkPath(env, channelName); + + // if a file exists in the unarchived path, you probably don't want to clobber its data + // so unlike 'archiveChannel' we won't overwrite. + // Fse.move will call back with EEXIST in such a situation + Fse.move(currentPath, unarchivedPath, cb); +}; + var flushUnusedChannels = function (env, cb, frame) { var currentTime = +new Date(); @@ -413,19 +538,30 @@ module.exports.create = function ( ) { var env = { root: conf.filePath || './datastore', + archiveRoot: conf.archivePath || './data/archive', + retainData: conf.retainData, channels: { }, channelExpirationMs: conf.channelExpirationMs || 30000, verbose: conf.verbose, openFiles: 0, openFileLimit: conf.openFileLimit || 2048, }; - // 0x1ff -> 777 var it; - Fse.mkdirp(env.root, 0x1ff, function (err) { - if (err && err.code !== 'EEXIST') { - // TODO: somehow return a nice error - throw err; - } + + nThen(function (w) { + // make sure the store's directory exists + Fse.mkdirp(env.root, PERMISSIVE, w(function (err) { + if (err && err.code !== 'EEXIST') { + throw err; + } + })); + // make sure the cold storage directory exists + Fse.mkdirp(env.archiveRoot, PERMISSIVE, w(function (err) { + if (err && err.code !== 'EEXIST') { + throw err; + } + })); + }).nThen(function () { cb({ readMessagesBin: (channelName, start, asyncMsgHandler, cb) => { if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } @@ -449,6 +585,10 @@ module.exports.create = function ( cb(err); }); }, + removeArchivedChannel: function (channelName, cb) { + if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } + removeArchivedChannel(env, channelName, cb); + }, closeChannel: function (channelName, cb) { if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } closeChannel(env, channelName, cb); @@ -468,6 +608,32 @@ module.exports.create = function ( if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } clearChannel(env, channelName, cb); }, + listChannels: function (handler, cb) { + listChannels(env.root, handler, cb); + }, + isChannelAvailable: function (channelName, cb) { + if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } + // construct the path + var filepath = mkPath(env, channelName); + channelExists(filepath, channelName, cb); + }, + isChannelArchived: function (channelName, cb) { + if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } + // construct the path + var filepath = mkArchivePath(env, channelName); + channelExists(filepath, channelName, cb); + }, + listArchivedChannels: function (handler, cb) { + listChannels(Path.join(env.archiveRoot, 'datastore'), handler, cb); + }, + archiveChannel: function (channelName, cb) { + if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } + archiveChannel(env, channelName, cb); + }, + restoreArchivedChannel: function (channelName, cb) { + if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } + unarchiveChannel(env, channelName, cb); + }, log: function (channelName, content, cb) { message(env, channelName, content, cb); }, diff --git a/storage/tasks.js b/storage/tasks.js index 0e09d0ad0..22f89d7e2 100644 --- a/storage/tasks.js +++ b/storage/tasks.js @@ -83,6 +83,7 @@ var write = function (env, task, cb) { }; var remove = function (env, path, cb) { + // FIXME COLDSTORAGE? Fs.unlink(path, cb); }; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 6dc9e1f00..a6d50ddf6 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -327,15 +327,6 @@ define([ account.note = obj.note; cb(obj); }); - - arePinsSynced(function (err, yes) { - if (!yes) { - resetPins(function (err) { - if (err) { return console.error(err); } - console.log('RESET DONE'); - }); - } - }); }); }); }; @@ -1690,6 +1681,15 @@ define([ loadUniversal(Profile, 'profile', waitFor); cleanFriendRequests(); }).nThen(function () { + arePinsSynced(function (err, yes) { + if (!yes) { + resetPins(function (err) { + if (err) { return console.error(err); } + console.log('RESET DONE'); + }); + } + }); + var requestLogin = function () { broadcast([], "REQUEST_LOGIN"); };