allow admins to overwrite live data with archived data when both exist

This commit is contained in:
ansuz 2022-09-13 18:32:50 +05:30
parent 3457bd3ba7
commit 333ba82970
4 changed files with 181 additions and 28 deletions

View File

@ -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) {

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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", {