Merge remote-tracking branch 'origin/stable'

# Conflicts:
#	libraries/ckeditor/ckeditor.js
#	libraries/ckeditor/ckeditor.js.map
#	package-lock.json
#	package.json
#	src/public/app/widgets/type_widgets/editable_text.js
This commit is contained in:
zadam 2020-06-03 16:25:45 +02:00
commit 915b1d1a45
24 changed files with 295 additions and 225 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,12 @@
const anonymizationService = require('./services/anonymization');
const backupService = require('./services/backup');
anonymizationService.anonymize().then(filePath => {
console.log("Anonymized file has been saved to:", filePath);
backupService.anonymize().then(resp => {
if (resp.success) {
console.log("Anonymization failed.");
}
else {
console.log("Anonymized file has been saved to: " + resp.anonymizedFilePath);
}
process.exit(0);
});
});

View File

@ -541,12 +541,13 @@ class Note extends Entity {
/**
* @return {Promise<Attribute>}
*/
async addAttribute(type, name, value = "") {
async addAttribute(type, name, value = "", isInheritable = false) {
const attr = new Attribute({
noteId: this.noteId,
type: type,
name: name,
value: value
value: value,
isInheritable: isInheritable
});
await attr.save();
@ -556,12 +557,12 @@ class Note extends Entity {
return attr;
}
async addLabel(name, value = "") {
return await this.addAttribute(LABEL, name, value);
async addLabel(name, value = "", isInheritable = false) {
return await this.addAttribute(LABEL, name, value, isInheritable);
}
async addRelation(name, targetNoteId) {
return await this.addAttribute(RELATION, name, targetNoteId);
async addRelation(name, targetNoteId, isInheritable = false) {
return await this.addAttribute(RELATION, name, targetNoteId, isInheritable);
}
/**

View File

@ -39,13 +39,14 @@ export async function showDialog(noteIds) {
}
async function cloneNotesTo(notePath) {
const targetNoteId = treeService.getNoteIdFromNotePath(notePath);
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath);
const targetBranchId = await treeCache.getBranchId(parentNoteId, noteId);
for (const cloneNoteId of clonedNoteIds) {
await branchService.cloneNoteTo(cloneNoteId, targetNoteId, $clonePrefix.val());
await branchService.cloneNoteTo(cloneNoteId, targetBranchId, $clonePrefix.val());
const clonedNote = await treeCache.getNote(cloneNoteId);
const targetNote = await treeCache.getNote(targetNoteId);
const targetNote = await treeCache.getBranch(targetBranchId).getNote();
toastService.showMessage(`Note "${clonedNote.title}" has been cloned into ${targetNote.title}`);
}

View File

@ -32,10 +32,11 @@ export async function showDialog(branchIds) {
noteAutocompleteService.showRecentNotes($noteAutoComplete);
}
async function moveNotesTo(parentNoteId) {
await branchService.moveToParentNote(movedBranchIds, parentNoteId);
async function moveNotesTo(parentBranchId) {
await branchService.moveToParentNote(movedBranchIds, parentBranchId);
const parentNote = await treeCache.getNote(parentNoteId);
const parentBranch = treeCache.getBranch(parentBranchId);
const parentNote = await parentBranch.getNote();
toastService.showMessage(`Selected notes have been moved into ${parentNote.title}`);
}
@ -46,9 +47,8 @@ $form.on('submit', () => {
if (notePath) {
$dialog.modal('hide');
const noteId = treeService.getNoteIdFromNotePath(notePath);
moveNotesTo(noteId);
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath);
treeCache.getBranchId(parentNoteId, noteId).then(branchId => moveNotesTo(branchId));
}
else {
console.error("No path to move to.");

View File

@ -17,19 +17,18 @@ const TPL = `
<button id="find-and-fix-consistency-issues-button" class="btn">Find and fix consistency issues</button><br/><br/>
<h4>Debugging</h4>
<h4>Anonymize database</h4>
<p>This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and some non-sensitive metadata)
for sharing online for debugging purposes without fear of leaking your personal data.</p>
<button id="anonymize-button" class="btn">Save anonymized database</button><br/><br/>
<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>
<h4>Backup database</h4>
<button id="backup-database-button" class="btn">Backup database</button>
<p>Trilium has automatic backup (daily, weekly, monthly), but you can also trigger backup manually here.</p>
<br/>
<br/>
<button id="backup-database-button" class="btn">Backup database now</button><br/><br/>
<h4>Vacuum database</h4>
@ -61,9 +60,14 @@ export default class AdvancedOptions {
});
this.$anonymizeButton.on('click', async () => {
await server.post('anonymization/anonymize');
const resp = await server.post('database/anonymize');
toastService.showMessage("Created anonymized database");
if (!resp.success) {
toastService.showError("Could not create anonymized database, check backend logs for details");
}
else {
toastService.showMessage(`Created anonymized database in ${resp.anonymizedFilePath}`, 10000);
}
});
this.$backupDatabaseButton.on('click', async () => {

View File

@ -45,7 +45,7 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) {
}
}
async function moveToParentNote(branchIdsToMove, newParentNoteId) {
async function moveToParentNote(branchIdsToMove, newParentBranchId) {
branchIdsToMove = filterRootNote(branchIdsToMove);
for (const branchIdToMove of branchIdsToMove) {
@ -56,7 +56,7 @@ async function moveToParentNote(branchIdsToMove, newParentNoteId) {
continue;
}
const resp = await server.put(`branches/${branchIdToMove}/move-to/${newParentNoteId}`);
const resp = await server.put(`branches/${branchIdToMove}/move-to/${newParentBranchId}`);
if (!resp.success) {
alert(resp.message);
@ -198,8 +198,8 @@ ws.subscribeToMessages(async message => {
}
});
async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, {
async function cloneNoteTo(childNoteId, parentBranchId, prefix) {
const resp = await server.put(`notes/${childNoteId}/clone-to/${parentBranchId}`, {
prefix: prefix
});
@ -225,4 +225,4 @@ export default {
moveNodeUpInHierarchy,
cloneNoteAfter,
cloneNoteTo
};
};

View File

@ -33,13 +33,13 @@ async function pasteAfter(afterBranchId) {
}
}
async function pasteInto(parentNoteId) {
async function pasteInto(parentBranchId) {
if (isClipboardEmpty()) {
return;
}
if (clipboardMode === 'cut') {
await branchService.moveToParentNote(clipboardBranchIds, parentNoteId);
await branchService.moveToParentNote(clipboardBranchIds, parentBranchId);
clipboardBranchIds = [];
clipboardMode = null;
@ -50,7 +50,7 @@ async function pasteInto(parentNoteId) {
for (const clipboardBranch of clipboardBranches) {
const clipboardNote = await clipboardBranch.getNote();
await branchService.cloneNoteTo(clipboardNote.noteId, parentNoteId);
await branchService.cloneNoteTo(clipboardNote.noteId, parentBranchId);
}
// copy will keep clipboardBranchIds and clipboardMode so it's possible to paste into multiple places
@ -89,4 +89,4 @@ export default {
cut,
copy,
isClipboardEmpty
}
}

View File

@ -10,7 +10,7 @@ let protectedSessionDeferred = null;
async function leaveProtectedSession() {
if (protectedSessionHolder.isProtectedSessionAvailable()) {
utils.reloadApp();
protectedSessionHolder.resetProtectedSession();
}
}
@ -113,4 +113,4 @@ export default {
enterProtectedSession,
leaveProtectedSession,
setupProtectedSession
};
};

View File

@ -165,6 +165,10 @@ async function consumeSyncData() {
utils.reloadApp();
}
for (const syncRow of nonProcessedSyncRows) {
processedSyncIds.add(syncRow.id);
}
lastProcessedSyncId = Math.max(lastProcessedSyncId, allSyncRows[allSyncRows.length - 1].id);
}

View File

@ -64,6 +64,72 @@ const TPL = `
width: 320px;
border-radius: 10px 0 10px 10px;
}
ul.fancytree-container {
outline: none !important;
background-color: inherit !important;
}
.fancytree-custom-icon {
font-size: 1.3em;
}
span.fancytree-title {
color: inherit !important;
background: inherit !important;
outline: none !important;
}
span.fancytree-node.protected > span.fancytree-custom-icon {
filter: drop-shadow(2px 2px 2px var(--main-text-color));
}
span.fancytree-node.multiple-parents .fancytree-title::after {
content: " *"
}
span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-title {
font-weight: bold;
}
/* first nesting level has lower left padding to avoid extra left padding. Other levels are not affected */
.ui-fancytree > li > ul {
padding-left: 5px;
}
span.fancytree-active .fancytree-title {
font-weight: bold;
border-color: var(--main-border-color) !important;
border-radius: 5px;
}
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
border-style: dashed !important;
}
span.fancytree-focused .fancytree-title, span.fancytree-focused.fancytree-selected .fancytree-title {
color: var(--active-item-text-color) !important;
background-color: var(--active-item-background-color) !important;
border-color: var(--main-background-color) !important; /* invisible border */
border-radius: 5px;
}
span.fancytree-selected .fancytree-title {
color: var(--hover-item-text-color) !important;
background-color: var(--hover-item-background-color) !important;
border-color: var(--main-background-color) !important; /* invisible border */
border-radius: 5px;
font-style: italic;
}
span.fancytree-node:hover span.fancytree-title {
border-color: var(--main-border-color) !important;
border-radius: 5px;
}
span.fancytree-node.archived {
opacity: 0.6;
}
</style>
<button class="btn btn-sm icon-button bx bx-cog tree-settings-button" title="Tree settings"></button>
@ -206,6 +272,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
const treeData = [await this.prepareRootNode()];
this.$tree.fancytree({
titlesTabbable: true,
autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: utils.isMobile() ? ["dnd5", "clones"] : ["hotkeys", "dnd5", "clones"],
@ -265,6 +332,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
const notes = this.getSelectedOrActiveNodes(node).map(node => ({
noteId: node.data.noteId,
branchId: node.data.branchId,
title: node.title
}));
@ -305,17 +373,28 @@ export default class NoteTreeWidget extends TabAwareWidget {
});
}
else {
const jsonStr = dataTransfer.getData("text");
let notes = null;
try {
notes = JSON.parse(jsonStr);
}
catch (e) {
console.error(`Cannot parse ${jsonStr} into notes for drop`);
return;
}
// This function MUST be defined to enable dropping of items on the tree.
// data.hitMode is 'before', 'after', or 'over'.
const selectedBranchIds = this.getSelectedOrActiveNodes().map(node => node.data.branchId);
const selectedBranchIds = notes.map(note => note.branchId);
if (data.hitMode === "before") {
branchService.moveBeforeBranch(selectedBranchIds, node.data.branchId);
} else if (data.hitMode === "after") {
branchService.moveAfterBranch(selectedBranchIds, node.data.branchId);
} else if (data.hitMode === "over") {
branchService.moveToParentNote(selectedBranchIds, node.data.noteId);
branchService.moveToParentNote(selectedBranchIds, node.data.branchId);
} else {
throw new Error("Unknown hitMode=" + data.hitMode);
}
@ -575,13 +654,18 @@ export default class NoteTreeWidget extends TabAwareWidget {
/** @return {FancytreeNode[]} */
getSelectedOrActiveNodes(node = null) {
const notes = this.getSelectedNodes(true);
const nodes = this.getSelectedNodes(true);
if (notes.length === 0) {
notes.push(node ? node : this.getActiveNode());
// the node you start dragging should be included even if not selected
if (node && !nodes.find(n => n.key === node.key)) {
nodes.push(node);
}
return notes;
if (nodes.length === 0) {
nodes.push(this.getActiveNode());
}
return nodes;
}
async setExpandedStatusForSubtree(node, isExpanded) {
@ -638,17 +722,17 @@ export default class NoteTreeWidget extends TabAwareWidget {
const activeContext = appContext.tabManager.getActiveTabContext();
if (activeContext && activeContext.notePath) {
this.tree.setFocus();
this.tree.setFocus(true);
const node = await this.expandToNote(activeContext.notePath);
await node.makeVisible({scrollIntoView: true});
node.setFocus();
node.setFocus(true);
}
}
/** @return {FancytreeNode} */
async getNodeFromPath(notePath, expand = false, expandOpts = {}) {
async getNodeFromPath(notePath, expand = false) {
utils.assertArguments(notePath);
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
@ -677,7 +761,12 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
if (expand) {
await parentNode.setExpanded(true, expandOpts);
await parentNode.setExpanded(true);
// although previous line should set the expanded status, it seems to happen asynchronously
// so we need to make sure it is set properly before calling updateNode which uses this flag
const branch = treeCache.getBranch(parentNode.data.branchId);
branch.isExpanded = true;
}
this.updateNode(parentNode);
@ -717,8 +806,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
/** @return {FancytreeNode} */
async expandToNote(notePath, expandOpts) {
return this.getNodeFromPath(notePath, true, expandOpts);
async expandToNote(notePath) {
return this.getNodeFromPath(notePath, true);
}
updateNode(node) {
@ -733,6 +822,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
node.icon = this.getIcon(note, isFolder);
node.extraClasses = this.getExtraClasses(note);
node.title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
if (node.isExpanded() !== branch.isExpanded) {
node.setExpanded(branch.isExpanded, {noEvents: true});
}
node.renderTitle();
}
@ -808,6 +902,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
async entitiesReloadedEvent({loadResults}) {
const activeNode = this.getActiveNode();
const activeNodeFocused = activeNode ? activeNode.hasFocus() : false;
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
@ -937,12 +1032,16 @@ export default class NoteTreeWidget extends TabAwareWidget {
node = await this.expandToNote(nextNotePath);
if (node) {
this.tree.setFocus();
node.setFocus(true);
await appContext.tabManager.getActiveTabContext().setNote(nextNotePath);
}
}
const newActiveNode = this.getActiveNode();
// return focus if the previously active node was also focused
if (newActiveNode && activeNodeFocused) {
newActiveNode.setFocus(true);
}
}
}
@ -1066,7 +1165,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
const toNode = node.getPrevSibling();
if (toNode !== null) {
branchService.moveToParentNote([node.data.branchId], toNode.data.noteId);
branchService.moveToParentNote([node.data.branchId], toNode.data.branchId);
}
}
@ -1151,7 +1250,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
pasteNotesFromClipboardCommand({node}) {
clipboard.pasteInto(node.data.noteId);
clipboard.pasteInto(node.data.branchId);
}
pasteNotesAfterFromClipboard({node}) {

View File

@ -113,71 +113,6 @@ span.fancytree-node.muted { opacity: 0.6; }
width: 100% !important;
}
ul.fancytree-container {
outline: none !important;
background-color: inherit !important;
}
.fancytree-custom-icon {
font-size: 1.3em;
}
span.fancytree-title {
color: inherit !important;
background: inherit !important;
}
span.fancytree-node.protected > span.fancytree-custom-icon {
filter: drop-shadow(2px 2px 2px var(--main-text-color));
}
span.fancytree-node.multiple-parents .fancytree-title::after {
content: " *"
}
span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-title {
font-weight: bold;
}
/* first nesting level has lower left padding to avoid extra left padding. Other levels are not affected */
.ui-fancytree > li > ul {
padding-left: 5px;
}
span.fancytree-active .fancytree-title {
font-weight: bold;
border-color: var(--main-border-color) !important;
border-radius: 5px;
}
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
border-style: dashed !important;
}
span.fancytree-focused .fancytree-title, span.fancytree-focused.fancytree-selected .fancytree-title {
color: var(--active-item-text-color) !important;
background-color: var(--active-item-background-color) !important;
border-color: var(--main-background-color) !important; /* invisible border */
border-radius: 5px;
}
span.fancytree-selected .fancytree-title {
color: var(--hover-item-text-color) !important;
background-color: var(--hover-item-background-color) !important;
border-color: var(--main-background-color) !important; /* invisible border */
border-radius: 5px;
font-style: italic;
}
span.fancytree-node:hover span.fancytree-title {
border-color: var(--main-border-color) !important;
border-radius: 5px;
}
span.fancytree-node.archived {
opacity: 0.6;
}
.ui-autocomplete {
max-height: 300px;
overflow-y: auto;
@ -864,4 +799,4 @@ body {
.hidden-int, .hidden-ext {
display: none !important;
}
}

View File

@ -1,11 +0,0 @@
"use strict";
const anonymization = require('../../services/anonymization');
async function anonymize() {
await anonymization.anonymize();
}
module.exports = {
anonymize
};

View File

@ -14,25 +14,33 @@ const TaskContext = require('../../services/task_context');
*/
async function moveBranchToParent(req) {
const {branchId, parentNoteId} = req.params;
const {branchId, parentBranchId} = req.params;
const parentBranch = await repository.getBranch(parentBranchId);
const branchToMove = await repository.getBranch(branchId);
if (branchToMove.parentNoteId === parentNoteId) {
if (!parentBranch || !branchToMove) {
return [400, `One or both branches ${branchId}, ${parentBranchId} have not been found`];
}
if (branchToMove.parentNoteId === parentBranch.noteId) {
return { success: true }; // no-op
}
const validationResult = await treeService.validateParentChild(parentNoteId, branchToMove.noteId, branchId);
const validationResult = await treeService.validateParentChild(parentBranch.noteId, branchToMove.noteId, branchId);
if (!validationResult.success) {
return [200, validationResult];
}
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [parentNoteId]);
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [parentBranch.noteId]);
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 10;
const newBranch = branchToMove.createClone(parentNoteId, newNotePos);
newBranch.isExpanded = true;
// expanding so that the new placement of the branch is immediately visible
parentBranch.isExpanded = true;
await parentBranch.save();
const newBranch = branchToMove.createClone(parentBranch.noteId, newNotePos);
await newBranch.save();
branchToMove.isDeleted = true;
@ -117,6 +125,7 @@ async function setExpanded(req) {
if (branchId !== 'root') {
await sql.execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]);
// we don't sync expanded label
// also this does not trigger updates to the frontend, this would trigger too many reloads
}
}
@ -178,4 +187,4 @@ module.exports = {
setExpandedForSubtree,
deleteBranch,
setPrefix
};
};

View File

@ -3,10 +3,10 @@
const cloningService = require('../../services/cloning');
async function cloneNoteToParent(req) {
const {noteId, parentNoteId} = req.params;
const {noteId, parentBranchId} = req.params;
const {prefix} = req.body;
return await cloningService.cloneNoteToParent(noteId, parentNoteId, prefix);
return await cloningService.cloneNoteToParent(noteId, parentBranchId, prefix);
}
async function cloneNoteAfter(req) {
@ -18,4 +18,4 @@ async function cloneNoteAfter(req) {
module.exports = {
cloneNoteToParent,
cloneNoteAfter
};
};

View File

@ -5,6 +5,10 @@ const log = require('../../services/log');
const backupService = require('../../services/backup');
const consistencyChecksService = require('../../services/consistency_checks');
async function anonymize() {
return await backupService.anonymize();
}
async function backupDatabase() {
return {
backupFile: await backupService.backupNow("now")
@ -24,5 +28,6 @@ async function findAndFixConsistencyIssues() {
module.exports = {
backupDatabase,
vacuumDatabase,
findAndFixConsistencyIssues
findAndFixConsistencyIssues,
anonymize
};

View File

@ -24,7 +24,6 @@ const exportRoute = require('./api/export');
const importRoute = require('./api/import');
const setupApiRoute = require('./api/setup');
const sqlRoute = require('./api/sql');
const anonymizationRoute = require('./api/anonymization');
const databaseRoute = require('./api/database');
const imageRoute = require('./api/image');
const attributesRoute = require('./api/attributes');
@ -123,7 +122,7 @@ function register(app) {
apiRoute(POST, '/api/tree/load', treeApiRoute.load);
apiRoute(PUT, '/api/branches/:branchId/set-prefix', branchesApiRoute.setPrefix);
apiRoute(PUT, '/api/branches/:branchId/move-to/:parentNoteId', branchesApiRoute.moveBranchToParent);
apiRoute(PUT, '/api/branches/:branchId/move-to/:parentBranchId', branchesApiRoute.moveBranchToParent);
apiRoute(PUT, '/api/branches/:branchId/move-before/:beforeBranchId', branchesApiRoute.moveBranchBeforeNote);
apiRoute(PUT, '/api/branches/:branchId/move-after/:afterBranchId', branchesApiRoute.moveBranchAfterNote);
apiRoute(PUT, '/api/branches/:branchId/expanded/:expanded', branchesApiRoute.setExpanded);
@ -152,7 +151,7 @@ function register(app) {
apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate);
apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentNoteId', cloningApiRoute.cloneNoteToParent);
apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentBranchId', cloningApiRoute.cloneNoteToParent);
apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter);
route(GET, '/api/notes/:branchId/export/:type/:format/:version/:taskId', [auth.checkApiAuthOrElectron], exportRoute.exportBranch);
@ -221,7 +220,7 @@ function register(app) {
apiRoute(GET, '/api/sql/schema', sqlRoute.getSchema);
apiRoute(POST, '/api/sql/execute', sqlRoute.execute);
apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize);
route(POST, '/api/database/anonymize', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler, false);
// backup requires execution outside of transaction
route(POST, '/api/database/backup-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.backupDatabase, apiResultHandler, false);

View File

@ -1,38 +0,0 @@
"use strict";
const dataDir = require('./data_dir');
const dateUtils = require('./date_utils');
const fs = require('fs-extra');
const sqlite = require('sqlite');
async function anonymize() {
if (!fs.existsSync(dataDir.ANONYMIZED_DB_DIR)) {
fs.mkdirSync(dataDir.ANONYMIZED_DB_DIR, 0o700);
}
const anonymizedFile = dataDir.ANONYMIZED_DB_DIR + "/" + "anonymized-" + dateUtils.getDateTimeForFile() + ".db";
fs.copySync(dataDir.DOCUMENT_PATH, anonymizedFile);
const db = await sqlite.open(anonymizedFile, {Promise});
await db.run("UPDATE notes SET title = 'title'");
await db.run("UPDATE note_contents SET content = 'text'");
await db.run("UPDATE note_revisions SET title = 'title'");
await db.run("UPDATE note_revision_contents SET content = 'title'");
await db.run("UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label'");
await db.run("UPDATE attributes SET name = 'name' WHERE type = 'relation'");
await db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL");
await db.run(`UPDATE options SET value = 'anonymized' WHERE name IN
('documentSecret', 'encryptedDataKey', 'passwordVerificationHash',
'passwordVerificationSalt', 'passwordDerivedKeySalt')`);
await db.run("VACUUM");
await db.close();
return anonymizedFile;
}
module.exports = {
anonymize
};

View File

@ -8,6 +8,8 @@ const log = require('./log');
const sqlInit = require('./sql_init');
const syncMutexService = require('./sync_mutex');
const cls = require('./cls');
const sqlite = require('sqlite');
const sqlite3 = require('sqlite3');
async function regularBackup() {
await periodBackup('lastDailyBackupDate', 'daily', 24 * 3600);
@ -28,53 +30,103 @@ async function periodBackup(optionName, fileName, periodInSeconds) {
}
}
async function backupNow(name) {
const COPY_ATTEMPT_COUNT = 50;
async function copyFile(backupFile) {
const sql = require('./sql');
try {
fs.unlinkSync(backupFile);
} catch (e) {
} // unlink throws exception if the file did not exist
let success = false;
let attemptCount = 0
for (; attemptCount < COPY_ATTEMPT_COUNT && !success; attemptCount++) {
try {
await sql.executeNoWrap(`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) {
// we don't want to backup DB in the middle of sync with potentially inconsistent DB state
return await syncMutexService.doExclusively(async () => {
const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`;
try {
fs.unlinkSync(backupFile);
}
catch (e) {} // unlink throws exception if the file did not exist
const success = await copyFile(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`);
if (success) {
log.info("Created backup at " + backupFile);
}
else {
log.info("Created backup at " + backupFile);
log.error(`Creating backup ${backupFile} failed`);
}
return backupFile;
});
}
async function anonymize() {
if (!fs.existsSync(dataDir.ANONYMIZED_DB_DIR)) {
fs.mkdirSync(dataDir.ANONYMIZED_DB_DIR, 0o700);
}
const anonymizedFile = dataDir.ANONYMIZED_DB_DIR + "/" + "anonymized-" + dateUtils.getDateTimeForFile() + ".db";
const success = await copyFile(anonymizedFile);
if (!success) {
return { success: false };
}
const db = await sqlite.open({
filename: anonymizedFile,
driver: sqlite3.Database
});
await db.run("UPDATE api_tokens SET token = 'API token value'");
await db.run("UPDATE notes SET title = 'title'");
await db.run("UPDATE note_contents SET content = 'text'");
await db.run("UPDATE note_revisions SET title = 'title'");
await db.run("UPDATE note_revision_contents SET content = 'title'");
await db.run("UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label'");
await db.run("UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name != 'template'");
await db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL");
await db.run(`UPDATE options SET value = 'anonymized' WHERE name IN
('documentId', 'documentSecret', 'encryptedDataKey', 'passwordVerificationHash',
'passwordVerificationSalt', 'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy')`);
await db.run("VACUUM");
await db.close();
return {
success: true,
anonymizedFilePath: anonymizedFile
};
}
if (!fs.existsSync(dataDir.BACKUP_DIR)) {
fs.mkdirSync(dataDir.BACKUP_DIR, 0o700);
}
sqlInit.dbReady.then(() => {
setInterval(cls.wrap(regularBackup), 60 * 60 * 1000);
setInterval(cls.wrap(regularBackup), 4 * 60 * 60 * 1000);
// kickoff backup immediately
setTimeout(cls.wrap(regularBackup), 1000);
// kickoff first backup soon after start up
setTimeout(cls.wrap(regularBackup), 5 * 60 * 1000);
});
module.exports = {
backupNow
backupNow,
anonymize
};

View File

@ -1 +1 @@
module.exports = { buildDate:"2020-05-20T08:54:55+02:00", buildRevision: "04c573e212db06e1dd60c74707e40f6912c85aab" };
module.exports = { buildDate:"2020-06-03T14:30:07+02:00", buildRevision: "c1fd9825aa6087b5061cdede5dba3f7f9dc62c31" };

View File

@ -9,12 +9,14 @@ const Branch = require('../entities/branch');
const TaskContext = require("./task_context.js");
const utils = require('./utils');
async function cloneNoteToParent(noteId, parentNoteId, prefix) {
if (await isNoteDeleted(noteId) || await isNoteDeleted(parentNoteId)) {
async function cloneNoteToParent(noteId, parentBranchId, prefix) {
const parentBranch = await repository.getBranch(parentBranchId);
if (await isNoteDeleted(noteId) || await isNoteDeleted(parentBranch.noteId)) {
return { success: false, message: 'Note is deleted.' };
}
const validationResult = await treeService.validateParentChild(parentNoteId, noteId);
const validationResult = await treeService.validateParentChild(parentBranch.noteId, noteId);
if (!validationResult.success) {
return validationResult;
@ -22,12 +24,13 @@ async function cloneNoteToParent(noteId, parentNoteId, prefix) {
const branch = await new Branch({
noteId: noteId,
parentNoteId: parentNoteId,
parentNoteId: parentBranch.noteId,
prefix: prefix,
isExpanded: 0
}).save();
await sql.execute("UPDATE branches SET isExpanded = 1 WHERE noteId = ?", [parentNoteId]);
parentBranch.isExpanded = true; // the new target should be expanded so it immediately shows up to the user
await parentBranch.save();
return { success: true, branchId: branch.branchId };
}
@ -111,4 +114,4 @@ module.exports = {
ensureNoteIsAbsentFromParent,
toggleNoteInParent,
cloneNoteAfter
};
};

View File

@ -32,7 +32,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
{
actionName: "scrollToActiveNote",
defaultShortcuts: ["CommandOrControl+."],
scope: "window" // FIXME - how do we find what note tree should be updated?
scope: "window"
},
{
actionName: "searchNotes",
@ -55,7 +55,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
actionName: "collapseTree",
defaultShortcuts: ["Alt+C"],
description: "Collapses the complete note tree",
scope: "note-tree"
scope: "window"
},
{
actionName: "collapseSubtree",
@ -425,4 +425,4 @@ async function getKeyboardActions() {
module.exports = {
DEFAULT_KEYBOARD_ACTIONS,
getKeyboardActions
};
};

View File

@ -98,7 +98,7 @@ async function createNewNote(params) {
const parentNote = await repository.getNote(params.parentNoteId);
if (!parentNote) {
throw new Error(`Parent note ${params.parentNoteId} not found.`);
throw new Error(`Parent note "${params.parentNoteId}" not found.`);
}
if (!params.title || params.title.trim().length === 0) {
@ -662,7 +662,9 @@ async function scanForLinks(note) {
const content = await note.getContent();
const newContent = await saveLinks(note, content);
await note.setContent(newContent);
if (content !== newContent) {
await note.setContent(newContent);
}
}
catch (e) {
log.error(`Could not scan for links note ${note.noteId}: ${e.message} ${e.stack}`);