Feature: Copy Pad without history (#4295)

New feature to copy a pad without copying entire history.  This is useful to perform a low CPU intensive operation while still copying current pad state.
This commit is contained in:
Joas Souza 2020-09-16 15:24:09 -03:00 committed by GitHub
parent b80a37173e
commit 8c04fe8775
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 259 additions and 36 deletions

View file

@ -597,6 +597,21 @@ exports.copyPad = async function(sourceID, destinationID, force)
await pad.copy(destinationID, force);
}
/**
copyPadWithoutHistory(sourceID, destinationID[, force=false]) copies a pad. If force is true,
the destination will be overwritten if it exists.
Example returns:
{code: 0, message:"ok", data: {padID: destinationID}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.copyPadWithoutHistory = async function(sourceID, destinationID, force)
{
let pad = await getPadSafe(sourceID, true);
await pad.copyPadWithoutHistory(destinationID, force);
}
/**
movePad(sourceID, destinationID[, force=false]) moves a pad. If force is true,
the destination will be overwritten if it exists.

View file

@ -357,16 +357,9 @@ Pad.prototype.init = async function init(text) {
}
Pad.prototype.copy = async function copy(destinationID, force) {
let destGroupID;
let sourceID = this.id;
// allow force to be a string
if (typeof force === "string") {
force = (force.toLowerCase() === "true");
} else {
force = !!force;
}
// Kick everyone from this pad.
// This was commented due to https://github.com/ether/etherpad-lite/issues/3183.
// Do we really need to kick everyone out?
@ -375,31 +368,15 @@ Pad.prototype.copy = async function copy(destinationID, force) {
// flush the source pad:
this.saveToDatabase();
// if it's a group pad, let's make sure the group exists.
let destGroupID;
if (destinationID.indexOf("$") >= 0) {
destGroupID = destinationID.split("$")[0]
let groupExists = await groupManager.doesGroupExist(destGroupID);
try {
// if it's a group pad, let's make sure the group exists.
destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID);
// group does not exist
if (!groupExists) {
throw new customError("groupID does not exist for destinationID", "apierror");
}
}
// if the pad exists, we should abort, unless forced.
let exists = await padManager.doesPadExist(destinationID);
if (exists) {
if (!force) {
console.error("erroring out without force");
throw new customError("destinationID already exists", "apierror");
}
// exists and forcing
let pad = await padManager.getPad(destinationID);
await pad.remove();
// if force is true and already exists a Pad with the same id, remove that Pad
await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force);
} catch(err) {
throw err;
}
// copy the 'pad' entry
@ -427,10 +404,7 @@ Pad.prototype.copy = async function copy(destinationID, force) {
promises.push(p);
}
// add the new pad to all authors who contributed to the old one
this.getAllAuthors().forEach(authorID => {
authorManager.addPad(authorID, destinationID);
});
this.copyAuthorInfoToDestinationPad(destinationID);
// wait for the above to complete
await Promise.all(promises);
@ -452,6 +426,110 @@ Pad.prototype.copy = async function copy(destinationID, force) {
return { padID: destinationID };
}
Pad.prototype.checkIfGroupExistAndReturnIt = async function checkIfGroupExistAndReturnIt(destinationID) {
let destGroupID = false;
if (destinationID.indexOf("$") >= 0) {
destGroupID = destinationID.split("$")[0]
let groupExists = await groupManager.doesGroupExist(destGroupID);
// group does not exist
if (!groupExists) {
throw new customError("groupID does not exist for destinationID", "apierror");
}
}
return destGroupID;
}
Pad.prototype.removePadIfForceIsTrueAndAlreadyExist = async function removePadIfForceIsTrueAndAlreadyExist(destinationID, force) {
// if the pad exists, we should abort, unless forced.
let exists = await padManager.doesPadExist(destinationID);
// allow force to be a string
if (typeof force === "string") {
force = (force.toLowerCase() === "true");
} else {
force = !!force;
}
if (exists) {
if (!force) {
console.error("erroring out without force");
throw new customError("destinationID already exists", "apierror");
}
// exists and forcing
let pad = await padManager.getPad(destinationID);
await pad.remove();
}
}
Pad.prototype.copyAuthorInfoToDestinationPad = function copyAuthorInfoToDestinationPad(destinationID) {
// add the new sourcePad to all authors who contributed to the old one
this.getAllAuthors().forEach(authorID => {
authorManager.addPad(authorID, destinationID);
});
}
Pad.prototype.copyPadWithoutHistory = async function copyPadWithoutHistory(destinationID, force) {
let destGroupID;
let sourceID = this.id;
// flush the source pad
this.saveToDatabase();
try {
// if it's a group pad, let's make sure the group exists.
destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID);
// if force is true and already exists a Pad with the same id, remove that Pad
await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force);
} catch(err) {
throw err;
}
let sourcePad = await padManager.getPad(sourceID);
// add the new sourcePad to all authors who contributed to the old one
this.copyAuthorInfoToDestinationPad(destinationID);
// Group pad? Add it to the group's list
if (destGroupID) {
await db.setSub("group:" + destGroupID, ["pads", destinationID], 1);
}
// initialize the pad with a new line to avoid getting the defaultText
let newPad = await padManager.getPad(destinationID, '\n');
let oldAText = this.atext;
let newPool = newPad.pool;
newPool.fromJsonable(sourcePad.pool.toJsonable()); // copy that sourceId pool to the new pad
// based on Changeset.makeSplice
let assem = Changeset.smartOpAssembler();
assem.appendOpWithText('=', '');
Changeset.appendATextToAssembler(oldAText, assem);
assem.endDocument();
// although we have instantiated the newPad with '\n', an additional '\n' is
// added internally, so the pad text on the revision 0 is "\n\n"
let oldLength = 2;
let newLength = assem.getLengthChange();
let newText = oldAText.text;
// create a changeset that removes the previous text and add the newText with
// all atributes present on the source pad
let changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
newPad.appendRevision(changeset);
hooks.callAll('padCopy', { 'originalPad': this, 'destinationID': destinationID });
return { padID: destinationID };
}
Pad.prototype.remove = async function remove() {
var padID = this.id;

View file

@ -142,8 +142,13 @@ version["1.2.14"] = Object.assign({}, version["1.2.13"],
}
);
version["1.2.15"] = Object.assign({}, version["1.2.14"],
{ "copyPadWithoutHistory" : ["sourceID", "destinationID", "force"]
}
);
// set the latest available API version here
exports.latestApiVersion = '1.2.14';
exports.latestApiVersion = '1.2.15';
// exports the versions so it can be used by the new Swagger endpoint
exports.version = version;