mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	safer backup to file using VACUUM INTO + possibility to explicitly ask for backup now
This commit is contained in:
		| @@ -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."); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 | ||||||
| }; | }; | ||||||
| @@ -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 | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -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 | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user