mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
fixed backup and anonymization with better-sqlite3
This commit is contained in:
parent
027afab6b1
commit
969f31dde2
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user