safer backup to file using VACUUM INTO + possibility to explicitly ask for backup now

This commit is contained in:
zadam 2020-05-29 21:55:08 +02:00
parent 1911d64c1c
commit 13f9d037dc
5 changed files with 68 additions and 13 deletions

View File

@ -24,6 +24,13 @@ const TPL = `
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and metadata) <p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and metadata)
for sharing online for debugging purposes without fear of leaking your personal data.</p> for sharing online for debugging purposes without fear of leaking your personal data.</p>
<h4>Backup database</h4>
<button id="backup-database-button" class="btn">Backup database</button>
<br/>
<br/>
<h4>Vacuum database</h4> <h4>Vacuum database</h4>
<p>This will rebuild database which will typically result in smaller database file. No data will be actually changed.</p> <p>This will rebuild database which will typically result in smaller database file. No data will be actually changed.</p>
@ -37,6 +44,7 @@ export default class AdvancedOptions {
this.$forceFullSyncButton = $("#force-full-sync-button"); this.$forceFullSyncButton = $("#force-full-sync-button");
this.$fillSyncRowsButton = $("#fill-sync-rows-button"); this.$fillSyncRowsButton = $("#fill-sync-rows-button");
this.$anonymizeButton = $("#anonymize-button"); this.$anonymizeButton = $("#anonymize-button");
this.$backupDatabaseButton = $("#backup-database-button");
this.$vacuumDatabaseButton = $("#vacuum-database-button"); this.$vacuumDatabaseButton = $("#vacuum-database-button");
this.$findAndFixConsistencyIssuesButton = $("#find-and-fix-consistency-issues-button"); this.$findAndFixConsistencyIssuesButton = $("#find-and-fix-consistency-issues-button");
@ -58,16 +66,22 @@ export default class AdvancedOptions {
toastService.showMessage("Created anonymized database"); toastService.showMessage("Created anonymized database");
}); });
this.$backupDatabaseButton.on('click', async () => {
const {backupFile} = await server.post('database/backup-database');
toastService.showMessage("Database has been backed up to " + backupFile, 10000);
});
this.$vacuumDatabaseButton.on('click', async () => { this.$vacuumDatabaseButton.on('click', async () => {
await server.post('cleanup/vacuum-database'); await server.post('database/vacuum-database');
toastService.showMessage("Database has been vacuumed"); toastService.showMessage("Database has been vacuumed");
}); });
this.$findAndFixConsistencyIssuesButton.on('click', async () => { this.$findAndFixConsistencyIssuesButton.on('click', async () => {
await server.post('cleanup/find-and-fix-consistency-issues'); await server.post('database/find-and-fix-consistency-issues');
toastService.showMessage("Consistency issues should be fixed."); toastService.showMessage("Consistency issues should be fixed.");
}); });
} }
} }

View File

@ -2,8 +2,15 @@
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const log = require('../../services/log'); const log = require('../../services/log');
const backupService = require('../../services/backup');
const consistencyChecksService = require('../../services/consistency_checks'); const consistencyChecksService = require('../../services/consistency_checks');
async function backupDatabase() {
return {
backupFile: await backupService.backupNow("now")
};
}
async function vacuumDatabase() { async function vacuumDatabase() {
await sql.execute("VACUUM"); await sql.execute("VACUUM");
@ -15,6 +22,7 @@ async function findAndFixConsistencyIssues() {
} }
module.exports = { module.exports = {
backupDatabase,
vacuumDatabase, vacuumDatabase,
findAndFixConsistencyIssues findAndFixConsistencyIssues
}; };

View File

@ -25,7 +25,7 @@ const importRoute = require('./api/import');
const setupApiRoute = require('./api/setup'); const setupApiRoute = require('./api/setup');
const sqlRoute = require('./api/sql'); const sqlRoute = require('./api/sql');
const anonymizationRoute = require('./api/anonymization'); const anonymizationRoute = require('./api/anonymization');
const cleanupRoute = require('./api/cleanup'); const databaseRoute = require('./api/database');
const imageRoute = require('./api/image'); const imageRoute = require('./api/image');
const attributesRoute = require('./api/attributes'); const attributesRoute = require('./api/attributes');
const scriptRoute = require('./api/script'); const scriptRoute = require('./api/script');
@ -222,10 +222,13 @@ function register(app) {
apiRoute(POST, '/api/sql/execute', sqlRoute.execute); apiRoute(POST, '/api/sql/execute', sqlRoute.execute);
apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize); apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize);
// VACUUM requires execution outside of transaction // backup requires execution outside of transaction
route(POST, '/api/cleanup/vacuum-database', [auth.checkApiAuthOrElectron, csrfMiddleware], cleanupRoute.vacuumDatabase, apiResultHandler, false); route(POST, '/api/database/backup-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.backupDatabase, apiResultHandler, false);
route(POST, '/api/cleanup/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], cleanupRoute.findAndFixConsistencyIssues, apiResultHandler, false); // VACUUM requires execution outside of transaction
route(POST, '/api/database/vacuum-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.vacuumDatabase, apiResultHandler, false);
route(POST, '/api/database/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.findAndFixConsistencyIssues, apiResultHandler, false);
apiRoute(POST, '/api/script/exec', scriptRoute.exec); apiRoute(POST, '/api/script/exec', scriptRoute.exec);
apiRoute(POST, '/api/script/run/:noteId', scriptRoute.run); apiRoute(POST, '/api/script/run/:noteId', scriptRoute.run);
@ -267,4 +270,4 @@ function register(app) {
module.exports = { module.exports = {
register register
}; };

View File

@ -29,13 +29,38 @@ async function periodBackup(optionName, fileName, periodInSeconds) {
} }
async function backupNow(name) { async function backupNow(name) {
const sql = require('./sql');
// we don't want to backup DB in the middle of sync with potentially inconsistent DB state // we don't want to backup DB in the middle of sync with potentially inconsistent DB state
await syncMutexService.doExclusively(async () => { return await syncMutexService.doExclusively(async () => {
const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`; const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`;
fs.copySync(dataDir.DOCUMENT_PATH, backupFile); try {
fs.unlinkSync(backupFile);
}
catch (e) {} // unlink throws exception if the file did not exist
log.info("Created backup at " + backupFile); let success = false;
let attemptCount = 0
for (; attemptCount < 50 && !success; attemptCount++) {
try {
await sql.executeNoWrap(`VACUUM INTO '${backupFile}'`);
success++;
}
catch (e) {}
// we re-try since VACUUM is very picky and it can't run if there's any other query currently running
// which is difficult to guarantee so we just re-try
}
if (attemptCount === 10) {
log.error(`Creating backup ${backupFile} failed`);
}
else {
log.info("Created backup at " + backupFile);
}
return backupFile;
}); });
} }
@ -52,4 +77,4 @@ sqlInit.dbReady.then(() => {
module.exports = { module.exports = {
backupNow backupNow
}; };

View File

@ -153,6 +153,10 @@ async function execute(query, params = []) {
return await wrap(async db => db.run(query, ...params), query); return await wrap(async db => db.run(query, ...params), query);
} }
async function executeNoWrap(query, params = []) {
await dbConnection.run(query, ...params);
}
async function executeMany(query, params) { async function executeMany(query, params) {
// essentially just alias // essentially just alias
await getManyRows(query, params); await getManyRows(query, params);
@ -264,6 +268,7 @@ module.exports = {
getMap, getMap,
getColumn, getColumn,
execute, execute,
executeNoWrap,
executeMany, executeMany,
executeScript, executeScript,
transactional, transactional,