mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	fixed backup and anonymization with better-sqlite3
This commit is contained in:
		| @@ -5,13 +5,13 @@ const log = require('../../services/log'); | |||||||
| const backupService = require('../../services/backup'); | const backupService = require('../../services/backup'); | ||||||
| const consistencyChecksService = require('../../services/consistency_checks'); | const consistencyChecksService = require('../../services/consistency_checks'); | ||||||
|  |  | ||||||
| function anonymize() { | async function anonymize() { | ||||||
|     return backupService.anonymize(); |     return await backupService.anonymize(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function backupDatabase() { | async function backupDatabase() { | ||||||
|     return { |     return { | ||||||
|         backupFile: backupService.backupNow("now") |         backupFile: await backupService.backupNow("now") | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -97,9 +97,14 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio | |||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             if (resultHandler) { |             if (resultHandler) { | ||||||
|  |                 if (result && result.then) { | ||||||
|  |                     result.then(actualResult => resultHandler(req, res, actualResult)) | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|                     resultHandler(req, res, result); |                     resultHandler(req, res, result); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |         } | ||||||
|         catch (e) { |         catch (e) { | ||||||
|             log.error(`${method} ${path} threw exception: ` + e.stack); |             log.error(`${method} ${path} threw exception: ` + e.stack); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ const syncMutexService = require('./sync_mutex'); | |||||||
| const attributeService = require('./attributes'); | const attributeService = require('./attributes'); | ||||||
| const cls = require('./cls'); | const cls = require('./cls'); | ||||||
| const utils = require('./utils'); | const utils = require('./utils'); | ||||||
|  | const Database = require('better-sqlite3'); | ||||||
|  |  | ||||||
| function regularBackup() { | function regularBackup() { | ||||||
|     periodBackup('lastDailyBackupDate', 'daily', 24 * 3600); |     periodBackup('lastDailyBackupDate', 'daily', 24 * 3600); | ||||||
| @@ -32,7 +33,7 @@ function periodBackup(optionName, fileName, periodInSeconds) { | |||||||
|  |  | ||||||
| const COPY_ATTEMPT_COUNT = 50; | const COPY_ATTEMPT_COUNT = 50; | ||||||
|  |  | ||||||
| function copyFile(backupFile) { | async function copyFile(backupFile) { | ||||||
|     const sql = require('./sql'); |     const sql = require('./sql'); | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
| @@ -40,79 +41,54 @@ function copyFile(backupFile) { | |||||||
|     } catch (e) { |     } catch (e) { | ||||||
|     } // unlink throws exception if the file did not exist |     } // unlink throws exception if the file did not exist | ||||||
|  |  | ||||||
|     let success = false; |     await sql.dbConnection.backup(backupFile); | ||||||
|     let attemptCount = 0 |  | ||||||
|  |  | ||||||
|     for (; attemptCount < COPY_ATTEMPT_COUNT && !success; attemptCount++) { |  | ||||||
|         try { |  | ||||||
|             sql.executeWithoutTransaction(`VACUUM INTO '${backupFile}'`); |  | ||||||
|  |  | ||||||
|             success = true; |  | ||||||
|         } catch (e) { |  | ||||||
|             log.info(`Copy DB attempt ${attemptCount + 1} failed with "${e.message}", retrying...`); |  | ||||||
|         } |  | ||||||
|         // 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 |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return attemptCount !== COPY_ATTEMPT_COUNT; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| async function backupNow(name) { | async function backupNow(name) { | ||||||
|     // 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 | ||||||
|     return await syncMutexService.doExclusively(() => { |     return await syncMutexService.doExclusively(async () => { | ||||||
|         const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`; |         const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`; | ||||||
|  |  | ||||||
|         const success = copyFile(backupFile); |         await copyFile(backupFile); | ||||||
|  |  | ||||||
|         if (success) { |  | ||||||
|         log.info("Created backup at " + backupFile); |         log.info("Created backup at " + backupFile); | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             log.error(`Creating backup ${backupFile} failed`); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return backupFile; |         return backupFile; | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function anonymize() { | async function anonymize() { | ||||||
|     if (!fs.existsSync(dataDir.ANONYMIZED_DB_DIR)) { |     if (!fs.existsSync(dataDir.ANONYMIZED_DB_DIR)) { | ||||||
|         fs.mkdirSync(dataDir.ANONYMIZED_DB_DIR, 0o700); |         fs.mkdirSync(dataDir.ANONYMIZED_DB_DIR, 0o700); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const anonymizedFile = dataDir.ANONYMIZED_DB_DIR + "/" + "anonymized-" + dateUtils.getDateTimeForFile() + ".db"; |     const anonymizedFile = dataDir.ANONYMIZED_DB_DIR + "/" + "anonymized-" + dateUtils.getDateTimeForFile() + ".db"; | ||||||
|  |  | ||||||
|     const success = copyFile(anonymizedFile); |     await copyFile(anonymizedFile); | ||||||
|  |  | ||||||
|     if (!success) { |     const db = new Database(anonymizedFile); | ||||||
|         return { success: false }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const db = sqlite.open({ |     db.prepare("UPDATE api_tokens SET token = 'API token value'").run(); | ||||||
|         filename: anonymizedFile, |     db.prepare("UPDATE notes SET title = 'title'").run(); | ||||||
|         driver: sqlite3.Database |     db.prepare("UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL").run(); | ||||||
|     }); |     db.prepare("UPDATE note_revisions SET title = 'title'").run(); | ||||||
|  |     db.prepare("UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL").run(); | ||||||
|     db.run("UPDATE api_tokens SET token = 'API token value'"); |  | ||||||
|     db.run("UPDATE notes SET title = 'title'"); |  | ||||||
|     db.run("UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL"); |  | ||||||
|     db.run("UPDATE note_revisions SET title = 'title'"); |  | ||||||
|     db.run("UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL"); |  | ||||||
|  |  | ||||||
|     // we want to delete all non-builtin attributes because they can contain sensitive names and values |     // we want to delete all non-builtin attributes because they can contain sensitive names and values | ||||||
|     // on the other hand builtin/system attrs should not contain any sensitive info |     // on the other hand builtin/system attrs should not contain any sensitive info | ||||||
|     const builtinAttrs = attributeService.getBuiltinAttributeNames().map(name => "'" + utils.sanitizeSql(name) + "'").join(', '); |     const builtinAttrs = attributeService | ||||||
|  |         .getBuiltinAttributeNames() | ||||||
|  |         .map(name => "'" + utils.sanitizeSql(name) + "'").join(', '); | ||||||
|  |  | ||||||
|     db.run(`UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN(${builtinAttrs})`); |     db.prepare(`UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN(${builtinAttrs})`).run(); | ||||||
|     db.run(`UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN (${builtinAttrs})`); |     db.prepare(`UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN (${builtinAttrs})`).run(); | ||||||
|     db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL"); |     db.prepare("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL").run(); | ||||||
|     db.run(`UPDATE options SET value = 'anonymized' WHERE name IN  |     db.prepare(`UPDATE options SET value = 'anonymized' WHERE name IN  | ||||||
|                     ('documentId', 'documentSecret', 'encryptedDataKey',  |                     ('documentId', 'documentSecret', 'encryptedDataKey',  | ||||||
|                      'passwordVerificationHash', 'passwordVerificationSalt',  |                      'passwordVerificationHash', 'passwordVerificationSalt',  | ||||||
|                      'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy')  |                      'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy')  | ||||||
|                       AND value != ''`); |                       AND value != ''`).run(); | ||||||
|     db.run("VACUUM"); |     db.prepare("VACUUM").run(); | ||||||
|  |  | ||||||
|     db.close(); |     db.close(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ const commonmark = require('commonmark'); | |||||||
| const TaskContext = require('../task_context.js'); | const TaskContext = require('../task_context.js'); | ||||||
| const protectedSessionService = require('../protected_session'); | const protectedSessionService = require('../protected_session'); | ||||||
| const mimeService = require("./mime"); | const mimeService = require("./mime"); | ||||||
|  | const sql = require("../sql"); | ||||||
| const treeService = require("../tree"); | const treeService = require("../tree"); | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -166,6 +167,7 @@ function importTar(taskContext, fileBuffer, importRootNote) { | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         sql.transactional(() => { | ||||||
|             ({note} = noteService.createNewNote({ |             ({note} = noteService.createNewNote({ | ||||||
|                 parentNoteId: parentNoteId, |                 parentNoteId: parentNoteId, | ||||||
|                 title: noteTitle, |                 title: noteTitle, | ||||||
| @@ -179,6 +181,7 @@ function importTar(taskContext, fileBuffer, importRootNote) { | |||||||
|             })); |             })); | ||||||
|  |  | ||||||
|             saveAttributes(note, noteMeta); |             saveAttributes(note, noteMeta); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|         if (!firstNote) { |         if (!firstNote) { | ||||||
|             firstNote = note; |             firstNote = note; | ||||||
|   | |||||||
| @@ -67,9 +67,7 @@ function getOptions() { | |||||||
| } | } | ||||||
|  |  | ||||||
| function getOptionsMap() { | function getOptionsMap() { | ||||||
|     const options = getOptions(); |     return require('./sql').getMap("SELECT name, value FROM options ORDER BY name"); | ||||||
|  |  | ||||||
|     return utils.toObject(options, opt => [opt.name, opt.value]); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   | |||||||
| @@ -131,7 +131,6 @@ function updateEntity(entity) { | |||||||
|                     eventService.emit(eventService.ENTITY_CREATED, eventPayload); |                     eventService.emit(eventService.ENTITY_CREATED, eventPayload); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 // it seems to be better to handle deletion and update separately |  | ||||||
|                 eventService.emit(entity.isDeleted ? eventService.ENTITY_DELETED : eventService.ENTITY_CHANGED, eventPayload); |                 eventService.emit(entity.isDeleted ? eventService.ENTITY_DELETED : eventService.ENTITY_CHANGED, eventPayload); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ const utils = require('./utils'); | |||||||
| const dateUtils = require('./date_utils'); | const dateUtils = require('./date_utils'); | ||||||
| const log = require('./log'); | const log = require('./log'); | ||||||
| const sql = require('./sql'); | const sql = require('./sql'); | ||||||
| const sqlInit = require('./sql_init'); |  | ||||||
| const cls = require('./cls'); | const cls = require('./cls'); | ||||||
|  |  | ||||||
| function saveSourceId(sourceId) { | function saveSourceId(sourceId) { | ||||||
| @@ -49,8 +48,10 @@ const currentSourceId = createSourceId(); | |||||||
|  |  | ||||||
| // very ugly | // very ugly | ||||||
| setTimeout(() => { | setTimeout(() => { | ||||||
|  |     const sqlInit = require('./sql_init'); | ||||||
|  |  | ||||||
|     sqlInit.dbReady.then(cls.wrap(() => saveSourceId(currentSourceId))); |     sqlInit.dbReady.then(cls.wrap(() => saveSourceId(currentSourceId))); | ||||||
| }, 1000); | }, 5000); | ||||||
|  |  | ||||||
| function getCurrentSourceId() { | function getCurrentSourceId() { | ||||||
|     return currentSourceId; |     return currentSourceId; | ||||||
|   | |||||||
| @@ -253,6 +253,7 @@ function transactional(func) { | |||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|  |     dbConnection, | ||||||
|     insert, |     insert, | ||||||
|     replace, |     replace, | ||||||
|     getValue, |     getValue, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user