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: 0, message:"ok", data: null}`
|
||||||
* `{code: 1, message:"padID does not exist", 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])
|
#### movePad(sourceID, destinationID[, force=false])
|
||||||
* API >= 1.2.8
|
* API >= 1.2.8
|
||||||
|
|
||||||
|
|
|
@ -597,6 +597,21 @@ exports.copyPad = async function(sourceID, destinationID, force)
|
||||||
await pad.copy(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,
|
movePad(sourceID, destinationID[, force=false]) moves a pad. If force is true,
|
||||||
the destination will be overwritten if it exists.
|
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) {
|
Pad.prototype.copy = async function copy(destinationID, force) {
|
||||||
|
let destGroupID;
|
||||||
let sourceID = this.id;
|
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.
|
// Kick everyone from this pad.
|
||||||
// This was commented due to https://github.com/ether/etherpad-lite/issues/3183.
|
// This was commented due to https://github.com/ether/etherpad-lite/issues/3183.
|
||||||
// Do we really need to kick everyone out?
|
// Do we really need to kick everyone out?
|
||||||
|
@ -375,31 +368,15 @@ Pad.prototype.copy = async function copy(destinationID, force) {
|
||||||
// flush the source pad:
|
// flush the source pad:
|
||||||
this.saveToDatabase();
|
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]
|
try {
|
||||||
let groupExists = await groupManager.doesGroupExist(destGroupID);
|
// if it's a group pad, let's make sure the group exists.
|
||||||
|
destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID);
|
||||||
|
|
||||||
// group does not exist
|
// if force is true and already exists a Pad with the same id, remove that Pad
|
||||||
if (!groupExists) {
|
await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force);
|
||||||
throw new customError("groupID does not exist for destinationID", "apierror");
|
} catch(err) {
|
||||||
}
|
throw err;
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy the 'pad' entry
|
// copy the 'pad' entry
|
||||||
|
@ -427,10 +404,7 @@ Pad.prototype.copy = async function copy(destinationID, force) {
|
||||||
promises.push(p);
|
promises.push(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the new pad to all authors who contributed to the old one
|
this.copyAuthorInfoToDestinationPad(destinationID);
|
||||||
this.getAllAuthors().forEach(authorID => {
|
|
||||||
authorManager.addPad(authorID, destinationID);
|
|
||||||
});
|
|
||||||
|
|
||||||
// wait for the above to complete
|
// wait for the above to complete
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
@ -452,6 +426,110 @@ Pad.prototype.copy = async function copy(destinationID, force) {
|
||||||
return { padID: destinationID };
|
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() {
|
Pad.prototype.remove = async function remove() {
|
||||||
var padID = this.id;
|
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
|
// 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 the versions so it can be used by the new Swagger endpoint
|
||||||
exports.version = version;
|
exports.version = version;
|
||||||
|
|
|
@ -427,6 +427,7 @@ describe('deletePad', function(){
|
||||||
|
|
||||||
var originalPadId = testPadId;
|
var originalPadId = testPadId;
|
||||||
var newPadId = makeid();
|
var newPadId = makeid();
|
||||||
|
var copiedPadId = makeid();
|
||||||
|
|
||||||
describe('createPad', function(){
|
describe('createPad', function(){
|
||||||
it('creates a new Pad with text', function(done) {
|
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
|
-> 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){
|
var endPoint = function(point, version){
|
||||||
version = version || apiVersion;
|
version = version || apiVersion;
|
||||||
return '/api/'+version+'/'+point+'?apikey='+apiKey;
|
return '/api/'+version+'/'+point+'?apikey='+apiKey;
|
||||||
|
|
Loading…
Reference in New Issue