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
parent
b80a37173e
commit
8c04fe8775
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue