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)
|
||||
};
|
||||
|
||||
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) {
|
||||
if (!Array.isArray(data)) { return void cb("EINVAL"); }
|
||||
var args = data[1];
|
||||
|
@ -733,6 +773,8 @@ var commands = {
|
|||
SET_LAST_EVICTION: setLastEviction,
|
||||
GET_WORKER_PROFILES: getWorkerProfiles,
|
||||
GET_USER_TOTAL_SIZE: getUserTotalSize,
|
||||
|
||||
REMOVE_DOCUMENT: removeDocument,
|
||||
};
|
||||
|
||||
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) {
|
||||
if (!stream) { return; }
|
||||
try {
|
||||
|
@ -683,6 +739,7 @@ var unarchiveChannel = function (env, channelName, cb) {
|
|||
// so unlike 'archiveChannel' we won't overwrite.
|
||||
// Fse.move will call back with EEXIST in such a situation
|
||||
|
||||
var ENOENT = false;
|
||||
nThen(function (w) {
|
||||
// if either metadata or a file exist in prod, abort
|
||||
channelExists(channelPath, w(function (err, exists) {
|
||||
|
@ -702,7 +759,7 @@ var unarchiveChannel = function (env, channelName, cb) {
|
|||
}
|
||||
if (exists) {
|
||||
w.abort();
|
||||
return CB("UNARCHIVE_METADATA_CONFLICT");
|
||||
return CB("UNARCHIVE_METADATA_CONFLICT"); // XXX
|
||||
}
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
|
@ -710,6 +767,10 @@ var unarchiveChannel = function (env, channelName, cb) {
|
|||
var archiveChannelPath = mkArchivePath(env, channelName);
|
||||
// restore the archived channel
|
||||
Fse.move(archiveChannelPath, channelPath, w(function (err) {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
ENOENT = true;
|
||||
return;
|
||||
}
|
||||
if (err) {
|
||||
w.abort();
|
||||
return void CB(err);
|
||||
|
@ -723,6 +784,10 @@ var unarchiveChannel = function (env, channelName, cb) {
|
|||
Fse.move(archiveMetadataPath, metadataPath, w(function (err) {
|
||||
// if there's nothing to move, you're done.
|
||||
if (err && err.code === 'ENOENT') {
|
||||
if (ENOENT) {
|
||||
// nothing was deleted? the client probably wants to know about that.
|
||||
return void cb("ENOENT");
|
||||
}
|
||||
return CB();
|
||||
}
|
||||
// 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
|
||||
isChannelAvailable: function (channelName, cb) {
|
||||
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
|
||||
// construct the path
|
||||
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));
|
||||
});
|
||||
isChannelAvailable(env, channelName, cb);
|
||||
},
|
||||
// check if a channel exists in the archive
|
||||
isChannelArchived: function (channelName, cb) {
|
||||
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
|
||||
// construct the path
|
||||
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));
|
||||
});
|
||||
isChannelArchived(env, channelName, cb);
|
||||
},
|
||||
// move a channel from the database to the archive, along with its metadata
|
||||
archiveChannel: function (channelName, cb) {
|
||||
|
|
|
@ -21,8 +21,10 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.alert.alert-info.cp-admin-bigger-alert {
|
||||
font-size: 16px;
|
||||
.alert.alert-info, .alert.alert-danger {
|
||||
&.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
|
||||
var archiveDocumentButton = danger(Messages.admin_archiveButton, function () {
|
||||
justifyArchivalDialog('', result => {
|
||||
|
@ -809,9 +874,7 @@ define([
|
|||
archiveDocumentButton,
|
||||
h('small', Messages.admin_archiveHint),
|
||||
]));
|
||||
}
|
||||
|
||||
if (data.archived && !data.live) {
|
||||
} else if (data.archived) {
|
||||
var restoreDocumentButton = primary(Messages.admin_unarchiveButton, function () {
|
||||
justifyRestorationDialog('', reason => {
|
||||
sframeCommand("RESTORE_ARCHIVED_DOCUMENT", {
|
||||
|
|
Loading…
Reference in a new issue