Merge branch 'staging' of github.com:xwiki-labs/cryptpad into staging
This commit is contained in:
commit
eaa389fcb8
@ -219,6 +219,29 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
inactiveTime: 90, // days
|
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)
|
/* Max Upload Size (bytes)
|
||||||
* this sets the maximum size of any one file uploaded to the server.
|
* this sets the maximum size of any one file uploaded to the server.
|
||||||
* anything larger than this size will be rejected
|
* 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.
|
* CryptPad stores each document in an individual file on your hard drive.
|
||||||
Specify a directory where files should be stored.
|
* Specify a directory where files should be stored.
|
||||||
It will be created automatically if it does not already exist.
|
* It will be created automatically if it does not already exist.
|
||||||
*/
|
*/
|
||||||
filePath: './datastore/',
|
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
|
/* CryptPad allows logged in users to request that particular documents be
|
||||||
* stored by the server indefinitely. This is called 'pinning'.
|
* stored by the server indefinitely. This is called 'pinning'.
|
||||||
* Pin requests are stored in a pin-store. The location of this store is
|
* Pin requests are stored in a pin-store. The location of this store is
|
||||||
|
|||||||
37
package-lock.json
generated
37
package-lock.json
generated
@ -99,26 +99,15 @@
|
|||||||
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
|
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
|
||||||
},
|
},
|
||||||
"chainpad-server": {
|
"chainpad-server": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/chainpad-server/-/chainpad-server-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/chainpad-server/-/chainpad-server-3.0.2.tgz",
|
||||||
"integrity": "sha512-1r53gYvPlrnZg0vf91gP3pqHILfi67oSo3cnj7kcvC4Y/n4t6wS3QCCjXeNArOuZv/sIByuKkeo1929osr1/KA==",
|
"integrity": "sha512-c5aEljVAapDKKs0+Rt2jymKAszm8X4ZeLFNJj1yxflwBqoh0jr8OANYvbfjtNaYFe2Wdflp/1i4gibYX4IMc+g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"nthen": "^0.1.8",
|
"nthen": "^0.1.8",
|
||||||
"pull-stream": "^3.6.9",
|
"pull-stream": "^3.6.9",
|
||||||
"stream-to-pull-stream": "^1.7.3",
|
"stream-to-pull-stream": "^1.7.3",
|
||||||
"tweetnacl": "~0.12.2",
|
"tweetnacl": "~0.12.2",
|
||||||
"ws": "^1.0.1"
|
"ws": "^3.3.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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chalk": {
|
"chalk": {
|
||||||
@ -827,11 +816,6 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
|
||||||
"version": "0.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz",
|
|
||||||
"integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8="
|
|
||||||
},
|
|
||||||
"os-tmpdir": {
|
"os-tmpdir": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||||
@ -1248,9 +1232,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ultron": {
|
"ultron": {
|
||||||
"version": "1.0.2",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
|
||||||
"integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po="
|
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
|
||||||
},
|
},
|
||||||
"uniq": {
|
"uniq": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@ -1298,13 +1282,6 @@
|
|||||||
"async-limiter": "~1.0.0",
|
"async-limiter": "~1.0.0",
|
||||||
"safe-buffer": "~5.1.0",
|
"safe-buffer": "~5.1.0",
|
||||||
"ultron": "~1.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": {
|
"xml2js": {
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
"url": "git://github.com/xwiki-labs/cryptpad.git"
|
"url": "git://github.com/xwiki-labs/cryptpad.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chainpad-server": "~3.0.0",
|
"chainpad-server": "~3.0.2",
|
||||||
"express": "~4.16.0",
|
"express": "~4.16.0",
|
||||||
"fs-extra": "^7.0.0",
|
"fs-extra": "^7.0.0",
|
||||||
"nthen": "~0.1.0",
|
"nthen": "~0.1.0",
|
||||||
|
|||||||
17
rpc.js
17
rpc.js
@ -814,6 +814,7 @@ var clearOwnedChannel = function (Env, channelId, unsafeKey, cb) {
|
|||||||
return void cb('INSUFFICIENT_PERMISSIONS');
|
return void cb('INSUFFICIENT_PERMISSIONS');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME COLDSTORAGE
|
||||||
return void Env.msgStore.clearChannel(channelId, function (e) {
|
return void Env.msgStore.clearChannel(channelId, function (e) {
|
||||||
cb(e);
|
cb(e);
|
||||||
});
|
});
|
||||||
@ -900,6 +901,20 @@ var removeOwnedChannel = function (Env, channelId, unsafeKey, cb) {
|
|||||||
if (metadata.owners.indexOf(unsafeKey) === -1) {
|
if (metadata.owners.indexOf(unsafeKey) === -1) {
|
||||||
return void cb('INSUFFICIENT_PERMISSIONS');
|
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) {
|
return void Env.msgStore.removeChannel(channelId, function (e) {
|
||||||
Log.info('DELETION_CHANNEL_BY_OWNER_RPC', {
|
Log.info('DELETION_CHANNEL_BY_OWNER_RPC', {
|
||||||
unsafeKey: unsafeKey,
|
unsafeKey: unsafeKey,
|
||||||
@ -1430,6 +1445,7 @@ var removeLoginBlock = function (Env, msg, cb) {
|
|||||||
return void cb('E_INVALID_BLOCK_PATH');
|
return void cb('E_INVALID_BLOCK_PATH');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME COLDSTORAGE
|
||||||
Fs.unlink(path, function (err) {
|
Fs.unlink(path, function (err) {
|
||||||
Log.info('DELETION_BLOCK_BY_OWNER_RPC', {
|
Log.info('DELETION_BLOCK_BY_OWNER_RPC', {
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
@ -1645,6 +1661,7 @@ RPC.create = function (
|
|||||||
};
|
};
|
||||||
|
|
||||||
var Env = {
|
var Env = {
|
||||||
|
retainData: config.retainData || false,
|
||||||
defaultStorageLimit: config.defaultStorageLimit,
|
defaultStorageLimit: config.defaultStorageLimit,
|
||||||
maxUploadSize: config.maxUploadSize || (20 * 1024 * 1024),
|
maxUploadSize: config.maxUploadSize || (20 * 1024 * 1024),
|
||||||
Sessions: {},
|
Sessions: {},
|
||||||
|
|||||||
@ -7,6 +7,14 @@ const config = require("../lib/load-config");
|
|||||||
|
|
||||||
if (!config.inactiveTime || typeof(config.inactiveTime) !== "number") { return; }
|
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 inactiveTime = +new Date() - (config.inactiveTime * 24 * 3600 * 1000);
|
||||||
let inactiveConfig = {
|
let inactiveConfig = {
|
||||||
unpinned: true,
|
unpinned: true,
|
||||||
|
|||||||
63
scripts/diagnose-archive-conflicts.js
Normal file
63
scripts/diagnose-archive-conflicts.js
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
|
||||||
169
scripts/evict-inactive.js
Normal file
169
scripts/evict-inactive.js
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
|
||||||
61
scripts/restore-archived.js
Normal file
61
scripts/restore-archived.js
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
|
||||||
180
storage/file.js
180
storage/file.js
@ -5,6 +5,7 @@ var Fs = require("fs");
|
|||||||
var Fse = require("fs-extra");
|
var Fse = require("fs-extra");
|
||||||
var Path = require("path");
|
var Path = require("path");
|
||||||
var nThen = require("nthen");
|
var nThen = require("nthen");
|
||||||
|
var Semaphore = require("saferphore");
|
||||||
const ToPull = require('stream-to-pull-stream');
|
const ToPull = require('stream-to-pull-stream');
|
||||||
const Pull = require('pull-stream');
|
const Pull = require('pull-stream');
|
||||||
|
|
||||||
@ -14,10 +15,18 @@ const isValidChannelId = function (id) {
|
|||||||
/^[a-zA-Z0-9=+-]*$/.test(id);
|
/^[a-zA-Z0-9=+-]*$/.test(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 511 -> octal 777
|
||||||
|
// read, write, execute permissions flag
|
||||||
|
const PERMISSIVE = 511;
|
||||||
|
|
||||||
var mkPath = function (env, channelId) {
|
var mkPath = function (env, channelId) {
|
||||||
return Path.join(env.root, channelId.slice(0, 2), channelId) + '.ndjson';
|
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 getMetadataAtPath = function (Env, path, cb) {
|
||||||
var remainder = '';
|
var remainder = '';
|
||||||
var stream = Fs.createReadStream(path, { encoding: 'utf8' });
|
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);
|
var path = mkPath(env, channelId);
|
||||||
getMetadataAtPath(env, path, function (e, metadata) {
|
getMetadataAtPath(env, path, function (e, metadata) {
|
||||||
if (e) { return cb(new Error(e)); }
|
if (e) { return cb(new Error(e)); }
|
||||||
@ -189,8 +198,7 @@ var checkPath = function (path, callback) {
|
|||||||
callback(err);
|
callback(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 511 -> octal 777
|
Fse.mkdirp(Path.dirname(path), PERMISSIVE, function (err) {
|
||||||
Fse.mkdirp(Path.dirname(path), 511, function (err) {
|
|
||||||
if (err && err.code !== 'EEXIST') {
|
if (err && err.code !== 'EEXIST') {
|
||||||
callback(err);
|
callback(err);
|
||||||
return;
|
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);
|
var filename = mkPath(env, channelName);
|
||||||
Fs.unlink(filename, cb);
|
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 flushUnusedChannels = function (env, cb, frame) {
|
||||||
var currentTime = +new Date();
|
var currentTime = +new Date();
|
||||||
|
|
||||||
@ -413,19 +538,30 @@ module.exports.create = function (
|
|||||||
) {
|
) {
|
||||||
var env = {
|
var env = {
|
||||||
root: conf.filePath || './datastore',
|
root: conf.filePath || './datastore',
|
||||||
|
archiveRoot: conf.archivePath || './data/archive',
|
||||||
|
retainData: conf.retainData,
|
||||||
channels: { },
|
channels: { },
|
||||||
channelExpirationMs: conf.channelExpirationMs || 30000,
|
channelExpirationMs: conf.channelExpirationMs || 30000,
|
||||||
verbose: conf.verbose,
|
verbose: conf.verbose,
|
||||||
openFiles: 0,
|
openFiles: 0,
|
||||||
openFileLimit: conf.openFileLimit || 2048,
|
openFileLimit: conf.openFileLimit || 2048,
|
||||||
};
|
};
|
||||||
// 0x1ff -> 777
|
|
||||||
var it;
|
var it;
|
||||||
Fse.mkdirp(env.root, 0x1ff, function (err) {
|
|
||||||
|
nThen(function (w) {
|
||||||
|
// make sure the store's directory exists
|
||||||
|
Fse.mkdirp(env.root, PERMISSIVE, w(function (err) {
|
||||||
if (err && err.code !== 'EEXIST') {
|
if (err && err.code !== 'EEXIST') {
|
||||||
// TODO: somehow return a nice error
|
|
||||||
throw err;
|
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({
|
cb({
|
||||||
readMessagesBin: (channelName, start, asyncMsgHandler, cb) => {
|
readMessagesBin: (channelName, start, asyncMsgHandler, cb) => {
|
||||||
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
|
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
|
||||||
@ -449,6 +585,10 @@ module.exports.create = function (
|
|||||||
cb(err);
|
cb(err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
removeArchivedChannel: function (channelName, cb) {
|
||||||
|
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
|
||||||
|
removeArchivedChannel(env, channelName, cb);
|
||||||
|
},
|
||||||
closeChannel: function (channelName, cb) {
|
closeChannel: function (channelName, cb) {
|
||||||
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
|
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
|
||||||
closeChannel(env, channelName, cb);
|
closeChannel(env, channelName, cb);
|
||||||
@ -468,6 +608,32 @@ module.exports.create = function (
|
|||||||
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
|
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
|
||||||
clearChannel(env, channelName, cb);
|
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) {
|
log: function (channelName, content, cb) {
|
||||||
message(env, channelName, content, cb);
|
message(env, channelName, content, cb);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -83,6 +83,7 @@ var write = function (env, task, cb) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var remove = function (env, path, cb) {
|
var remove = function (env, path, cb) {
|
||||||
|
// FIXME COLDSTORAGE?
|
||||||
Fs.unlink(path, cb);
|
Fs.unlink(path, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -327,15 +327,6 @@ define([
|
|||||||
account.note = obj.note;
|
account.note = obj.note;
|
||||||
cb(obj);
|
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);
|
loadUniversal(Profile, 'profile', waitFor);
|
||||||
cleanFriendRequests();
|
cleanFriendRequests();
|
||||||
}).nThen(function () {
|
}).nThen(function () {
|
||||||
|
arePinsSynced(function (err, yes) {
|
||||||
|
if (!yes) {
|
||||||
|
resetPins(function (err) {
|
||||||
|
if (err) { return console.error(err); }
|
||||||
|
console.log('RESET DONE');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
var requestLogin = function () {
|
var requestLogin = function () {
|
||||||
broadcast([], "REQUEST_LOGIN");
|
broadcast([], "REQUEST_LOGIN");
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user