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.
ep_hash_auth_in_core_tests
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

@ -526,6 +526,16 @@ copies a pad with full history and chat. If force is true and the destination pa
* `{code: 0, message:"ok", data: null}`
* `{code: 1, message:"padID does not exist", data: null}`
#### copyPadWithoutHistory(sourceID, destinationID[, force=false])
* API >= 1.2.15
copies a pad without copying the history and chat. If force is true and the destination pad exists, it will be overwritten.
Note that all the revisions will be lost! In most of the cases one should use `copyPad` API instead.
*Example returns:*
* `{code: 0, message:"ok", data: null}`
* `{code: 1, message:"padID does not exist", data: null}`
#### movePad(sourceID, destinationID[, force=false])
* API >= 1.2.8

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;

View File

@ -427,6 +427,7 @@ describe('deletePad', function(){
var originalPadId = testPadId;
var newPadId = makeid();
var copiedPadId = makeid();
describe('createPad', function(){
it('creates a new Pad with text', function(done) {
@ -681,12 +682,126 @@ describe('createPad', function(){
});
})
describe('copyPad', function(){
it('copies the content of a existent pad', function(done) {
api.get(endPoint('copyPad')+"&sourceID="+testPadId+"&destinationID="+copiedPadId+"&force=true")
.expect(function(res){
if(res.body.code !== 0) throw new Error("Copy Pad Failed")
})
.expect('Content-Type', /json/)
.expect(200, done)
});
})
describe('copyPadWithoutHistory', function(){
var sourcePadId = makeid();
var newPad;
before(function(done) {
createNewPadWithHtml(sourcePadId, ulHtml, done);
});
beforeEach(function() {
newPad = makeid();
})
it('returns a successful response', function(done) {
api.get(endPoint('copyPadWithoutHistory')+"&sourceID="+sourcePadId+"&destinationID="+newPad+"&force=false")
.expect(function(res){
if(res.body.code !== 0) throw new Error("Copy Pad Without History Failed")
})
.expect('Content-Type', /json/)
.expect(200, done)
});
// this test validates if the source pad's text and attributes are kept
it('creates a new pad with the same content as the source pad', function(done) {
api.get(endPoint('copyPadWithoutHistory')+"&sourceID="+sourcePadId+"&destinationID="+newPad+"&force=false")
.expect(function(res){
if(res.body.code !== 0) throw new Error("Copy Pad Without History Failed")
})
.end(function() {
api.get(endPoint('getHTML')+"&padID="+newPad)
.expect(function(res){
var receivedHtml = res.body.data.html.replace("<br><br></body>", "</body>").toLowerCase();
if (receivedHtml !== expectedHtml) {
throw new Error(`HTML received from export is not the one we were expecting.
Received:
${receivedHtml}
Expected:
${expectedHtml}
Which is a slightly modified version of the originally imported one:
${ulHtml}`);
}
})
.expect(200, done);
});
});
context('when try copy a pad with a group that does not exist', function() {
var padId = makeid();
var padWithNonExistentGroup = `notExistentGroup$${padId}`
it('throws an error', function(done) {
api.get(endPoint('copyPadWithoutHistory')+"&sourceID="+sourcePadId+"&destinationID="+padWithNonExistentGroup+"&force=true")
.expect(function(res){
// code 1, it means an error has happened
if(res.body.code !== 1) throw new Error("It should report an error")
})
.expect(200, done);
})
});
context('when try copy a pad and destination pad already exist', function() {
var padIdExistent = makeid();
before(function(done) {
createNewPadWithHtml(padIdExistent, ulHtml, done);
});
context('and force is false', function() {
it('throws an error', function(done) {
api.get(endPoint('copyPadWithoutHistory')+"&sourceID="+sourcePadId+"&destinationID="+padIdExistent+"&force=false")
.expect(function(res){
// code 1, it means an error has happened
if(res.body.code !== 1) throw new Error("It should report an error")
})
.expect(200, done);
});
});
context('and force is true', function() {
it('returns a successful response', function(done) {
api.get(endPoint('copyPadWithoutHistory')+"&sourceID="+sourcePadId+"&destinationID="+padIdExistent+"&force=true")
.expect(function(res){
// code 1, it means an error has happened
if(res.body.code !== 0) throw new Error("Copy pad without history with force true failed")
})
.expect(200, done);
});
});
})
})
/*
-> movePadForce Test
*/
var createNewPadWithHtml = function(padId, html, cb) {
api.get(endPoint('createPad')+"&padID="+padId)
.end(function() {
api.post(endPoint('setHTML'))
.send({
"padID": padId,
"html": html,
})
.end(cb);
})
}
var endPoint = function(point, version){
version = version || apiVersion;
return '/api/'+version+'/'+point+'?apikey='+apiKey;