allow admins to overwrite live data with archived data when both exist
This commit is contained in:
parent
3457bd3ba7
commit
333ba82970
4 changed files with 181 additions and 28 deletions
|
@ -200,6 +200,46 @@ var archiveDocument = function (Env, Server, cb, data) {
|
||||||
// Env.blobStore.archive.proof(userSafeKey, blobId, cb)
|
// Env.blobStore.archive.proof(userSafeKey, blobId, cb)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var removeDocument = function (Env, Server, cb, data) {
|
||||||
|
if (!Array.isArray(data)) { return void cb("EINVAL"); }
|
||||||
|
var args = data[1];
|
||||||
|
|
||||||
|
var id, reason;
|
||||||
|
if (typeof(args) === 'string') {
|
||||||
|
id = args;
|
||||||
|
} else if (args && typeof(args) === 'object') {
|
||||||
|
id = args.id;
|
||||||
|
reason = args.reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof(id) !== 'string' || id.length < 32) { return void cb("EINVAL"); }
|
||||||
|
|
||||||
|
switch (id.length) {
|
||||||
|
case 32:
|
||||||
|
return void Env.msgStore.removeChannel(id, Util.both(cb, function (err) {
|
||||||
|
Env.Log.info("REMOVAL_CHANNEL_BY_ADMIN_RPC", {
|
||||||
|
channelId: id,
|
||||||
|
reason: reason,
|
||||||
|
status: err? String(err): "SUCCESS",
|
||||||
|
});
|
||||||
|
Channel.disconnectChannelMembers(Env, Server, id, 'EDELETED', err => {
|
||||||
|
if (err) { } // TODO
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
case 48:
|
||||||
|
return void Env.blobStore.remove.blob(id, Util.both(cb, function (err) {
|
||||||
|
Env.Log.info("REMOVAL_BLOB_BY_ADMIN_RPC", {
|
||||||
|
id: id,
|
||||||
|
reason: reason,
|
||||||
|
status: err? String(err): "SUCCESS",
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
default:
|
||||||
|
return void cb("INVALID_ID_LENGTH");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
var restoreArchivedDocument = function (Env, Server, cb, data) {
|
var restoreArchivedDocument = function (Env, Server, cb, data) {
|
||||||
if (!Array.isArray(data)) { return void cb("EINVAL"); }
|
if (!Array.isArray(data)) { return void cb("EINVAL"); }
|
||||||
var args = data[1];
|
var args = data[1];
|
||||||
|
@ -733,6 +773,8 @@ var commands = {
|
||||||
SET_LAST_EVICTION: setLastEviction,
|
SET_LAST_EVICTION: setLastEviction,
|
||||||
GET_WORKER_PROFILES: getWorkerProfiles,
|
GET_WORKER_PROFILES: getWorkerProfiles,
|
||||||
GET_USER_TOTAL_SIZE: getUserTotalSize,
|
GET_USER_TOTAL_SIZE: getUserTotalSize,
|
||||||
|
|
||||||
|
REMOVE_DOCUMENT: removeDocument,
|
||||||
};
|
};
|
||||||
|
|
||||||
Admin.command = function (Env, safeKey, data, _cb, Server) {
|
Admin.command = function (Env, safeKey, data, _cb, Server) {
|
||||||
|
|
|
@ -85,6 +85,62 @@ var channelExists = function (filepath, cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var isChannelAvailable = function (env, channelName, cb) {
|
||||||
|
// construct the path
|
||||||
|
var filepath = mkPath(env, channelName);
|
||||||
|
var metapath = mkMetadataPath(env, channelName);
|
||||||
|
|
||||||
|
// (ansuz) I'm uncertain whether this task should be unordered or ordered.
|
||||||
|
// there's a round trip to the client (and possibly the user) before they decide
|
||||||
|
// to act on the information of whether there is already content present in this channel.
|
||||||
|
// so it's practically impossible to avoid race conditions where someone else creates
|
||||||
|
// some content before you.
|
||||||
|
// if that's the case, it's basically impossible that you'd generate the same signing key,
|
||||||
|
// and thus historykeeper should reject the signed messages of whoever loses the race.
|
||||||
|
// thus 'unordered' seems appropriate.
|
||||||
|
env.schedule.unordered(channelName, function (next) {
|
||||||
|
var done = Util.once(Util.mkAsync(Util.both(cb, next)));
|
||||||
|
var exists = false;
|
||||||
|
var handler = function (err, _exists) {
|
||||||
|
if (err) { return void done(err); }
|
||||||
|
exists = exists || _exists;
|
||||||
|
};
|
||||||
|
|
||||||
|
nThen(function (w) {
|
||||||
|
channelExists(filepath, w(handler));
|
||||||
|
channelExists(metapath, w(handler));
|
||||||
|
}).nThen(function () {
|
||||||
|
done(void 0, exists);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var isChannelArchived = function (env, channelName, cb) {
|
||||||
|
// construct the path
|
||||||
|
var filepath = mkArchivePath(env, channelName);
|
||||||
|
var metapath = mkArchiveMetadataPath(env, channelName);
|
||||||
|
|
||||||
|
// as with the method above, somebody might remove, restore, or overwrite an archive
|
||||||
|
// in the time that it takes to answer this query and to execute whatever follows.
|
||||||
|
// since it's impossible to win the race every time let's just make this 'unordered'
|
||||||
|
|
||||||
|
env.schedule.unordered(channelName, function (next) {
|
||||||
|
var done = Util.once(Util.mkAsync(Util.both(cb, next)));
|
||||||
|
var exists = false;
|
||||||
|
var handler = function (err, _exists) {
|
||||||
|
if (err) { return void done(err); }
|
||||||
|
exists = exists || _exists;
|
||||||
|
};
|
||||||
|
|
||||||
|
nThen(function (w) {
|
||||||
|
channelExists(filepath, w(handler));
|
||||||
|
channelExists(metapath, w(handler));
|
||||||
|
}).nThen(function () {
|
||||||
|
done(void 0, exists);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const destroyStream = function (stream) {
|
const destroyStream = function (stream) {
|
||||||
if (!stream) { return; }
|
if (!stream) { return; }
|
||||||
try {
|
try {
|
||||||
|
@ -683,6 +739,7 @@ var unarchiveChannel = function (env, channelName, cb) {
|
||||||
// so unlike 'archiveChannel' we won't overwrite.
|
// so unlike 'archiveChannel' we won't overwrite.
|
||||||
// Fse.move will call back with EEXIST in such a situation
|
// Fse.move will call back with EEXIST in such a situation
|
||||||
|
|
||||||
|
var ENOENT = false;
|
||||||
nThen(function (w) {
|
nThen(function (w) {
|
||||||
// if either metadata or a file exist in prod, abort
|
// if either metadata or a file exist in prod, abort
|
||||||
channelExists(channelPath, w(function (err, exists) {
|
channelExists(channelPath, w(function (err, exists) {
|
||||||
|
@ -702,7 +759,7 @@ var unarchiveChannel = function (env, channelName, cb) {
|
||||||
}
|
}
|
||||||
if (exists) {
|
if (exists) {
|
||||||
w.abort();
|
w.abort();
|
||||||
return CB("UNARCHIVE_METADATA_CONFLICT");
|
return CB("UNARCHIVE_METADATA_CONFLICT"); // XXX
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}).nThen(function (w) {
|
}).nThen(function (w) {
|
||||||
|
@ -710,6 +767,10 @@ var unarchiveChannel = function (env, channelName, cb) {
|
||||||
var archiveChannelPath = mkArchivePath(env, channelName);
|
var archiveChannelPath = mkArchivePath(env, channelName);
|
||||||
// restore the archived channel
|
// restore the archived channel
|
||||||
Fse.move(archiveChannelPath, channelPath, w(function (err) {
|
Fse.move(archiveChannelPath, channelPath, w(function (err) {
|
||||||
|
if (err && err.code === 'ENOENT') {
|
||||||
|
ENOENT = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (err) {
|
if (err) {
|
||||||
w.abort();
|
w.abort();
|
||||||
return void CB(err);
|
return void CB(err);
|
||||||
|
@ -723,6 +784,10 @@ var unarchiveChannel = function (env, channelName, cb) {
|
||||||
Fse.move(archiveMetadataPath, metadataPath, w(function (err) {
|
Fse.move(archiveMetadataPath, metadataPath, w(function (err) {
|
||||||
// if there's nothing to move, you're done.
|
// if there's nothing to move, you're done.
|
||||||
if (err && err.code === 'ENOENT') {
|
if (err && err.code === 'ENOENT') {
|
||||||
|
if (ENOENT) {
|
||||||
|
// nothing was deleted? the client probably wants to know about that.
|
||||||
|
return void cb("ENOENT");
|
||||||
|
}
|
||||||
return CB();
|
return CB();
|
||||||
}
|
}
|
||||||
// call back with an error if something goes wrong
|
// call back with an error if something goes wrong
|
||||||
|
@ -1175,31 +1240,12 @@ module.exports.create = function (conf, _cb) {
|
||||||
// check if a channel exists in the database
|
// check if a channel exists in the database
|
||||||
isChannelAvailable: function (channelName, cb) {
|
isChannelAvailable: function (channelName, cb) {
|
||||||
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
|
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
|
||||||
// construct the path
|
isChannelAvailable(env, channelName, cb);
|
||||||
var filepath = mkPath(env, channelName);
|
|
||||||
// (ansuz) I'm uncertain whether this task should be unordered or ordered.
|
|
||||||
// there's a round trip to the client (and possibly the user) before they decide
|
|
||||||
// to act on the information of whether there is already content present in this channel.
|
|
||||||
// so it's practically impossible to avoid race conditions where someone else creates
|
|
||||||
// some content before you.
|
|
||||||
// if that's the case, it's basically impossible that you'd generate the same signing key,
|
|
||||||
// and thus historykeeper should reject the signed messages of whoever loses the race.
|
|
||||||
// thus 'unordered' seems appropriate.
|
|
||||||
schedule.unordered(channelName, function (next) {
|
|
||||||
channelExists(filepath, Util.both(cb, next));
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
// check if a channel exists in the archive
|
// check if a channel exists in the archive
|
||||||
isChannelArchived: function (channelName, cb) {
|
isChannelArchived: function (channelName, cb) {
|
||||||
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
|
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
|
||||||
// construct the path
|
isChannelArchived(env, channelName, cb);
|
||||||
var filepath = mkArchivePath(env, channelName);
|
|
||||||
// as with the method above, somebody might remove, restore, or overwrite an archive
|
|
||||||
// in the time that it takes to answer this query and to execute whatever follows.
|
|
||||||
// since it's impossible to win the race every time let's just make this 'unordered'
|
|
||||||
schedule.unordered(channelName, function (next) {
|
|
||||||
channelExists(filepath, Util.both(cb, next));
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
// move a channel from the database to the archive, along with its metadata
|
// move a channel from the database to the archive, along with its metadata
|
||||||
archiveChannel: function (channelName, cb) {
|
archiveChannel: function (channelName, cb) {
|
||||||
|
|
|
@ -21,8 +21,10 @@
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert.alert-info.cp-admin-bigger-alert {
|
.alert.alert-info, .alert.alert-danger {
|
||||||
font-size: 16px;
|
&.cp-admin-bigger-alert {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -788,7 +788,72 @@ define([
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.live) {
|
if (data.live && data.archived) {
|
||||||
|
let disableButtons;
|
||||||
|
let restoreButton = danger(Messages.admin_unarchiveButton, function () {
|
||||||
|
justifyRestorationDialog('', reason => {
|
||||||
|
nThen(function (w) {
|
||||||
|
sframeCommand('REMOVE_DOCUMENT', {
|
||||||
|
id: data.id,
|
||||||
|
reason: reason,
|
||||||
|
}, w(err => {
|
||||||
|
if (err) {
|
||||||
|
w.abort();
|
||||||
|
return void UI.warn(Messages.error);
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}).nThen(function () {
|
||||||
|
sframeCommand("RESTORE_ARCHIVED_DOCUMENT", {
|
||||||
|
id: data.id,
|
||||||
|
reason: reason,
|
||||||
|
}, (err /*, response */) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return void UI.warn(Messages.error);
|
||||||
|
}
|
||||||
|
UI.log(Messages.restoredFromServer);
|
||||||
|
disableButtons();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let archiveButton = danger(Messages.admin_archiveButton, function () {
|
||||||
|
justifyArchivalDialog('', result => {
|
||||||
|
sframeCommand('ARCHIVE_DOCUMENT', {
|
||||||
|
id: data.id,
|
||||||
|
reason: result,
|
||||||
|
}, (err /*, response */) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return void UI.warn(Messages.error);
|
||||||
|
}
|
||||||
|
UI.log(Messages.archivedFromServer);
|
||||||
|
disableButtons();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
disableButtons = function () {
|
||||||
|
[archiveButton, restoreButton].forEach(el => {
|
||||||
|
disable($(el));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
row(h('span', [
|
||||||
|
Messages.admin_documentConflict,
|
||||||
|
h('br'),
|
||||||
|
h('small', Messages.ui_experimental),
|
||||||
|
]), h('span', [
|
||||||
|
h('div.alert.alert-danger.cp-admin-bigger-alert', [
|
||||||
|
Messages.admin_conflictExplanation,
|
||||||
|
]),
|
||||||
|
h('p', [
|
||||||
|
restoreButton,
|
||||||
|
archiveButton,
|
||||||
|
]),
|
||||||
|
]));
|
||||||
|
} else if (data.live) {
|
||||||
// archive
|
// archive
|
||||||
var archiveDocumentButton = danger(Messages.admin_archiveButton, function () {
|
var archiveDocumentButton = danger(Messages.admin_archiveButton, function () {
|
||||||
justifyArchivalDialog('', result => {
|
justifyArchivalDialog('', result => {
|
||||||
|
@ -809,9 +874,7 @@ define([
|
||||||
archiveDocumentButton,
|
archiveDocumentButton,
|
||||||
h('small', Messages.admin_archiveHint),
|
h('small', Messages.admin_archiveHint),
|
||||||
]));
|
]));
|
||||||
}
|
} else if (data.archived) {
|
||||||
|
|
||||||
if (data.archived && !data.live) {
|
|
||||||
var restoreDocumentButton = primary(Messages.admin_unarchiveButton, function () {
|
var restoreDocumentButton = primary(Messages.admin_unarchiveButton, function () {
|
||||||
justifyRestorationDialog('', reason => {
|
justifyRestorationDialog('', reason => {
|
||||||
sframeCommand("RESTORE_ARCHIVED_DOCUMENT", {
|
sframeCommand("RESTORE_ARCHIVED_DOCUMENT", {
|
||||||
|
|
Loading…
Reference in a new issue