/* This is a repair tool. It rebuilds an old pad at a new pad location up to a known "good" revision. */ if (process.argv.length != 4 && process.argv.length != 5) { console.error('Use: node bin/repairPad.js $PADID $REV [$NEWPADID]'); process.exit(1); } const npm = require('../src/node_modules/npm'); const async = require('../src/node_modules/async'); const ueberDB = require('../src/node_modules/ueberdb2'); const padId = process.argv[2]; const newRevHead = process.argv[3]; const newPadId = process.argv[4] || `${padId}-rebuilt`; let db, oldPad, newPad, settings; let AuthorManager, ChangeSet, Pad, PadManager; async.series([ function (callback) { npm.load({}, (err) => { if (err) { console.error(`Could not load NPM: ${err}`); process.exit(1); } else { callback(); } }); }, function (callback) { // Get a handle into the database db = require('../src/node/db/DB'); db.init(callback); }, function (callback) { PadManager = require('../src/node/db/PadManager'); Pad = require('../src/node/db/Pad').Pad; // Get references to the original pad and to a newly created pad // HACK: This is a standalone script, so we want to write everything // out to the database immediately. The only problem with this is // that a driver (like the mysql driver) can hardcode these values. db.db.db.settings = {cache: 0, writeInterval: 0, json: true}; // Validate the newPadId if specified and that a pad with that ID does // not already exist to avoid overwriting it. if (!PadManager.isValidPadId(newPadId)) { console.error('Cannot create a pad with that id as it is invalid'); process.exit(1); } PadManager.doesPadExists(newPadId, (err, exists) => { if (exists) { console.error('Cannot create a pad with that id as it already exists'); process.exit(1); } }); PadManager.getPad(padId, (err, pad) => { oldPad = pad; newPad = new Pad(newPadId); callback(); }); }, function (callback) { // Clone all Chat revisions const chatHead = oldPad.chatHead; for (var i = 0, curHeadNum = 0; i <= chatHead; i++) { db.db.get(`pad:${padId}:chat:${i}`, (err, chat) => { db.db.set(`pad:${newPadId}:chat:${curHeadNum++}`, chat); console.log(`Created: Chat Revision: pad:${newPadId}:chat:${curHeadNum}`); }); } callback(); }, function (callback) { // Rebuild Pad from revisions up to and including the new revision head AuthorManager = require('../src/node/db/AuthorManager'); Changeset = require('ep_etherpad-lite/static/js/Changeset'); // Author attributes are derived from changesets, but there can also be // non-author attributes with specific mappings that changesets depend on // and, AFAICT, cannot be recreated any other way newPad.pool.numToAttrib = oldPad.pool.numToAttrib; for (let curRevNum = 0; curRevNum <= newRevHead; curRevNum++) { db.db.get(`pad:${padId}:revs:${curRevNum}`, (err, rev) => { if (rev.meta) { throw 'The specified revision number could not be found.'; } const newRevNum = ++newPad.head; const newRevId = `pad:${newPad.id}:revs:${newRevNum}`; db.db.set(newRevId, rev); AuthorManager.addPad(rev.meta.author, newPad.id); newPad.atext = Changeset.applyToAText(rev.changeset, newPad.atext, newPad.pool); console.log(`Created: Revision: pad:${newPad.id}:revs:${newRevNum}`); if (newRevNum == newRevHead) { callback(); } }); } }, function (callback) { // Add saved revisions up to the new revision head console.log(newPad.head); const newSavedRevisions = []; for (const i in oldPad.savedRevisions) { savedRev = oldPad.savedRevisions[i]; if (savedRev.revNum <= newRevHead) { newSavedRevisions.push(savedRev); console.log(`Added: Saved Revision: ${savedRev.revNum}`); } } newPad.savedRevisions = newSavedRevisions; callback(); }, function (callback) { // Save the source pad db.db.set(`pad:${newPadId}`, newPad, (err) => { console.log(`Created: Source Pad: pad:${newPadId}`); newPad.saveToDatabase().then(() => callback(), callback); }); }, ], (err) => { if (err) { throw err; } else { console.info('finished'); process.exit(0); } });