Compare commits

...

17 Commits

Author SHA1 Message Date
zadam
535dcb6d12 release 0.42.7 2020-06-08 10:43:12 +02:00
zadam
4426362799 cleanup sqlite to make the distributed archives smaller 2020-06-08 10:42:40 +02:00
zadam
2c609e8136 promoted attributes widget is now auto-updating, fixes #700 2020-06-08 00:29:52 +02:00
zadam
11b73b79ed refresh promoted attributes when change detected 2020-06-07 23:57:10 +02:00
zadam
e70c862e72 fix import 2020-06-07 23:55:55 +02:00
zadam
b3e66d5a83 fixed command line anonymization 2020-06-07 10:45:41 +02:00
zadam
e8cd821e57 futrther improvements to anonymization 2020-06-07 10:20:48 +02:00
zadam
be7ac74235 better fallback for resolving filenames of binary attachments 2020-06-05 10:40:35 +02:00
zadam
58fa0832f6 fix focusing title after creating a note 2020-06-04 21:44:34 +02:00
zadam
1502b9ce66 prevent attribute inheritance cycle via template, closes #1077 2020-06-04 12:27:41 +02:00
zadam
7307ca385f release 0.42.6 2020-06-03 14:30:07 +02:00
zadam
c1fd9825aa fix backup 2020-06-03 12:16:16 +02:00
zadam
9de7d3fc53 fix unloading protected session after clicking on a button, closes #1078 2020-06-03 11:47:30 +02:00
zadam
3c5db844ba fix tree focusing issues 2020-06-03 11:06:45 +02:00
zadam
e7330c1104 more anonymization 2020-06-03 09:55:05 +02:00
zadam
ec4586b164 fix reference link implementation, closes #1069 2020-06-02 23:54:33 +02:00
zadam
91e5f24798 fix db anonymization 2020-06-02 23:13:55 +02:00
34 changed files with 336 additions and 232 deletions

View File

@@ -33,6 +33,9 @@ find $DIR/libraries -name "*.map" -type f -delete
rm -r $DIR/src/public/app
rm -r $DIR/node_modules/sqlite3/build
rm -r $DIR/node_modules/sqlite3/deps
sed -i -e 's/app\/desktop.js/app-dist\/desktop.js/g' $DIR/src/views/desktop.ejs
sed -i -e 's/app\/mobile.js/app-dist\/mobile.js/g' $DIR/src/views/mobile.ejs
sed -i -e 's/app\/setup.js/app-dist\/setup.js/g' $DIR/src/views/setup.ejs
sed -i -e 's/app\/setup.js/app-dist\/setup.js/g' $DIR/src/views/setup.ejs

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.42.2",
"version": "0.42.6",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3345,9 +3345,9 @@
}
},
"electron": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.0.0.tgz",
"integrity": "sha512-JsaSQNPh+XDYkLj8APtVKTtvpb86KIG57W5OOss4TNrn8L3isC9LsCITwfnVmGIXHhvX6oY/weCtN5hAAytjVg==",
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.0.2.tgz",
"integrity": "sha512-+a3KegLvQXVjC3b6yBWwZmtWp3tHf9ut27yORAWHO9JRFtKfNf88fi1UvTPJSW8R0sUH7ZEdzN6A95T22KGtlA==",
"dev": true,
"requires": {
"@electron/get": "^1.0.1",

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.42.5",
"version": "0.42.7",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -78,7 +78,7 @@
"yazl": "^2.5.1"
},
"devDependencies": {
"electron": "9.0.0",
"electron": "9.0.2",
"electron-builder": "22.6.0",
"electron-packager": "14.2.1",
"electron-rebuild": "1.10.1",

View File

@@ -1,7 +1,24 @@
const anonymizationService = require('./services/anonymization');
const backupService = require('./services/backup');
const sqlInit = require('./services/sql_init');
require('./entities/entity_constructor');
anonymizationService.anonymize().then(filePath => {
console.log("Anonymized file has been saved to:", filePath);
sqlInit.dbReady.then(async () => {
try {
console.log("Starting anonymization...");
process.exit(0);
});
const resp = await backupService.anonymize();
if (resp.success) {
console.log("Anonymized file has been saved to: " + resp.anonymizedFilePath);
process.exit(0);
} else {
console.log("Anonymization failed.");
}
}
catch (e) {
console.error(e.message, e.stack);
}
process.exit(1);
});

View File

@@ -105,7 +105,7 @@ class Attribute extends Entity {
// cannot be static!
updatePojo(pojo) {
delete pojo.__note;
delete pojo.__note; // FIXME: probably note necessary anymore
}
createClone(type, name, value) {

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

@@ -152,10 +152,10 @@ function AttributesModel() {
attr.value = treeService.getNoteIdFromNotePath(attr.selectedPath);
}
else if (attr.type === 'label-definition') {
attr.value = attr.labelDefinition;
attr.value = JSON.stringify(attr.labelDefinition);
}
else if (attr.type === 'relation-definition') {
attr.value = attr.relationDefinition;
attr.value = JSON.stringify(attr.relationDefinition);
}
delete attr.labelValue;

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

@@ -170,6 +170,16 @@ class NoteShort {
* @returns {Attribute[]} all note's attributes, including inherited ones
*/
getAttributes(type, name) {
return this.__filterAttrs(this.__getCachedAttributes([]), type, name);
}
__getCachedAttributes(path) {
// notes/clones cannot form tree cycles, it is possible to create attribute inheritance cycle via templates
// when template instance is a parent of template itself
if (path.includes(this.noteId)) {
return [];
}
if (!(this.noteId in noteAttributeCache)) {
const ownedAttributes = this.getOwnedAttributes();
@@ -177,11 +187,13 @@ class NoteShort {
ownedAttributes
];
const newPath = [...path, this.noteId];
for (const templateAttr of ownedAttributes.filter(oa => oa.type === 'relation' && oa.name === 'template')) {
const templateNote = this.treeCache.notes[templateAttr.value];
if (templateNote) {
attrArrs.push(templateNote.getAttributes());
if (templateNote && templateNote.noteId !== this.noteId) {
attrArrs.push(templateNote.__getCachedAttributes(newPath));
}
}
@@ -189,7 +201,7 @@ class NoteShort {
for (const parentNote of this.getParentNotes()) {
// these virtual parent-child relationships are also loaded into frontend tree cache
if (parentNote.type !== 'search') {
attrArrs.push(parentNote.getInheritableAttributes());
attrArrs.push(parentNote.__getInheritableAttributes(newPath));
}
}
}
@@ -197,7 +209,7 @@ class NoteShort {
noteAttributeCache.attributes[this.noteId] = attrArrs.flat();
}
return this.__filterAttrs(noteAttributeCache.attributes[this.noteId], type, name);
return noteAttributeCache.attributes[this.noteId];
}
__filterAttrs(attributes, type, name) {
@@ -212,8 +224,8 @@ class NoteShort {
}
}
getInheritableAttributes() {
const attrs = this.getAttributes();
__getInheritableAttributes(path) {
const attrs = this.__getCachedAttributes(path);
return attrs.filter(attr => attr.isInheritable);
}
@@ -460,4 +472,4 @@ class NoteShort {
}
}
export default NoteShort;
export default NoteShort;

View File

@@ -54,8 +54,9 @@ export default class LoadResults {
this.attributes.push({attributeId, sourceId});
}
getAttributes() {
getAttributes(sourceId = 'none') {
return this.attributes
.filter(row => row.sourceId !== sourceId)
.map(row => this.treeCache.attributes[row.attributeId])
.filter(attr => !!attr);
}

View File

@@ -34,7 +34,7 @@ export default class MainTreeExecutors extends Component {
return;
}
const {note} = await noteCreateService.createNote(activeNote.noteId, {
await noteCreateService.createNote(activeNote.noteId, {
isProtected: activeNote.isProtected,
saveSelection: false
});
@@ -56,4 +56,4 @@ export default class MainTreeExecutors extends Component {
saveSelection: false
});
}
}
}

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

@@ -73,7 +73,7 @@ class TabContext extends Component {
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
if (triggerSwitchEvent) {
this.triggerEvent('tabNoteSwitched', {
await this.triggerEvent('tabNoteSwitched', {
tabContext: this,
notePath: this.notePath
});
@@ -127,4 +127,4 @@ class TabContext extends Component {
}
}
export default TabContext;
export default TabContext;

View File

@@ -203,7 +203,7 @@ export default class TabManager extends Component {
if (activate) {
this.activateTab(tabContext.tabId, false);
this.triggerEvent('tabNoteSwitchedAndActivated', {
await this.triggerEvent('tabNoteSwitchedAndActivated', {
tabContext,
notePath: tabContext.notePath // resolved note path
});

View File

@@ -4,7 +4,6 @@ const TPL = `
<table class="note-info-widget-table">
<style>
.note-info-widget-table {
table-layout: fixed;
width: 100%;
}
@@ -22,22 +21,23 @@ const TPL = `
<tr>
<th>Note ID:</th>
<td colspan="3" class="note-info-note-id"></td>
<td class="note-info-note-id"></td>
</tr>
<tr>
<th>Created:</th>
<td colspan="3" class="note-info-date-created"></td>
<td class="note-info-date-created"></td>
</tr>
<tr>
<th>Modified:</th>
<td colspan="3" class="note-info-date-modified"></td>
<td class="note-info-date-modified"></td>
</tr>
<tr>
<th>Type:</th>
<td class="note-info-type"></td>
<th>MIME:</th>
<td class="note-info-mime"></td>
<td>
<span class="note-info-type"></span>
<span class="note-info-mime"></span>
</td>
</tr>
</table>
`;
@@ -69,9 +69,12 @@ export default class NoteInfoWidget extends CollapsibleWidget {
this.$type.text(note.type);
this.$mime
.text(note.mime)
.attr("title", note.mime);
if (note.mime) {
this.$mime.text('(' + note.mime + ')');
}
else {
this.$mime.empty();
}
}
entitiesReloadedEvent({loadResults}) {

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"],
@@ -651,17 +718,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();
@@ -690,7 +757,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);
@@ -730,8 +802,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) {
@@ -746,7 +818,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;
node.setExpanded(branch.isExpanded, {noEvents:true});
if (node.isExpanded() !== branch.isExpanded) {
node.setExpanded(branch.isExpanded, {noEvents: true});
}
node.renderTitle();
}
@@ -778,8 +854,11 @@ export default class NoteTreeWidget extends TabAwareWidget {
this.toggleInt(this.isEnabled());
const oldActiveNode = this.getActiveNode();
let oldActiveNodeFocused = false;
if (oldActiveNode) {
oldActiveNodeFocused = oldActiveNode.hasFocus();
oldActiveNode.setActive(false);
oldActiveNode.setFocus(false);
}
@@ -792,8 +871,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
await this.expandToNote(this.tabContext.notePath);
}
newActiveNode.setActive(true, {noEvents: true});
newActiveNode.setActive(true, {noEvents: true, noFocus: !oldActiveNodeFocused});
newActiveNode.makeVisible({scrollIntoView: true});
}
}
@@ -822,6 +900,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
async entitiesReloadedEvent({loadResults}) {
const activeNode = this.getActiveNode();
const activeNodeFocused = activeNode && activeNode.hasFocus();
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
@@ -944,19 +1023,23 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
if (node) {
node.setActive(true, {noEvents: true});
node.setActive(true, {noEvents: true, noFocus: true});
}
else {
// this is used when original note has been deleted and we want to move the focus to the note above/below
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) {
await newActiveNode.setFocus(true);
}
}
}
@@ -983,7 +1066,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
if (activeNotePath) {
const node = await this.getNodeFromPath(activeNotePath, true);
await node.setActive(true, {noEvents: true});
await node.setActive(true, {noEvents: true, noFocus: true});
}
}

View File

@@ -229,7 +229,7 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
.prop("title", "Remove this attribute")
.on('click', async () => {
if (valueAttr.attributeId) {
await server.remove("notes/" + this.noteId + "/attributes/" + valueAttr.attributeId);
await server.remove("notes/" + this.noteId + "/attributes/" + valueAttr.attributeId, this.componentId);
}
$tr.remove();
@@ -263,8 +263,19 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
type: $attr.prop("attribute-type"),
name: $attr.prop("attribute-name"),
value: value
});
}, this.componentId);
$attr.prop("attribute-id", result.attributeId);
}
entitiesReloadedEvent({loadResults}) {console.log("loadResults", loadResults);
// relation/label definitions are very often inherited by tree or template,
// it's difficult to detect inheritance so we will
if (loadResults.getAttributes(this.componentId).find(attr =>
attr.noteId === this.noteId
|| ['label-definition', 'relation-definition'].includes(attr.type))) {
this.refresh();
}
}
}

View File

@@ -23,6 +23,7 @@ const mentionSetup = {
row.text = row.name = row.noteTitle;
row.id = '@' + row.text;
row.link = '#' + row.path;
row.notePath = row.path;
}
res(rows);
@@ -256,4 +257,4 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.textEditor.model.insertContent(imageElement, this.textEditor.model.document.selection);
} );
}
}
}

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;
@@ -873,4 +808,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

@@ -125,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
}
}

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');
@@ -220,7 +219,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

@@ -115,13 +115,24 @@ function isAttributeType(type) {
}
function isAttributeDangerous(type, name) {
return BUILTIN_ATTRIBUTES.some(attr =>
attr.type === attr.type &&
return BUILTIN_ATTRIBUTES.some(attr =>
attr.type === attr.type &&
attr.name.toLowerCase() === name.trim().toLowerCase() &&
attr.isDangerous
);
}
function getBuiltinAttributeNames() {
return BUILTIN_ATTRIBUTES
.map(attr => attr.name)
.concat([
'internalLink',
'imageLink',
'includeNoteLink',
'relationMapLink'
]);
}
module.exports = {
getNotesWithLabel,
getNotesWithLabels,
@@ -131,5 +142,6 @@ module.exports = {
createAttribute,
getAttributeNames,
isAttributeType,
isAttributeDangerous
};
isAttributeDangerous,
getBuiltinAttributeNames
};

View File

@@ -7,7 +7,11 @@ const dataDir = require('./data_dir');
const log = require('./log');
const sqlInit = require('./sql_init');
const syncMutexService = require('./sync_mutex');
const attributeService = require('./attributes');
const cls = require('./cls');
const utils = require('./utils');
const sqlite = require('sqlite');
const sqlite3 = require('sqlite3');
async function regularBackup() {
await periodBackup('lastDailyBackupDate', 'daily', 24 * 3600);
@@ -28,46 +32,98 @@ async function periodBackup(optionName, fileName, periodInSeconds) {
}
}
const BACKUP_ATTEMPT_COUNT = 50;
const COPY_ATTEMPT_COUNT = 50;
async function backupNow(name) {
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 < BACKUP_ATTEMPT_COUNT && !success; attemptCount++) {
try {
await sql.executeNoWrap(`VACUUM INTO '${backupFile}'`);
success++;
}
catch (e) {
log.info(`Backup 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
}
if (attemptCount === BACKUP_ATTEMPT_COUNT) {
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' WHERE content IS NOT NULL");
await db.run("UPDATE note_revisions SET title = 'title'");
await 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
// on the other hand builtin/system attrs should not contain any sensitive info
const builtinAttrs = attributeService.getBuiltinAttributeNames().map(name => "'" + utils.sanitizeSql(name) + "'").join(', ');
await db.run(`UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN(${builtinAttrs})`);
await db.run(`UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN (${builtinAttrs})`);
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')
AND value != ''`);
await db.run("VACUUM");
await db.close();
return {
success: true,
anonymizedFilePath: anonymizedFile
};
}
if (!fs.existsSync(dataDir.BACKUP_DIR)) {
fs.mkdirSync(dataDir.BACKUP_DIR, 0o700);
}
@@ -80,5 +136,6 @@ sqlInit.dbReady.then(() => {
});
module.exports = {
backupNow
backupNow,
anonymize
};

View File

@@ -1 +1 @@
module.exports = { buildDate:"2020-05-31T23:33:30+02:00", buildRevision: "8c88ce6f651c3df1cd97925e5b8e3281ca4b1665" };
module.exports = { buildDate:"2020-06-08T10:43:12+02:00", buildRevision: "4426362799448b6228eedf20e7fc179ce4b3f860" };

View File

@@ -47,7 +47,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
return noteIdMap[origNoteId];
}
function getMeta(filePath) {
if (!metaFile) {
return {};
@@ -425,7 +425,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
}
for (const noteId in createdNoteIds) { // now the noteIds are unique
await noteService.scanForLinks(await repository.getNotes(noteId));
await noteService.scanForLinks(await repository.getNote(noteId));
if (!metaFile) {
// if there's no meta file then the notes are created based on the order in that tar file but that
@@ -459,4 +459,4 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
module.exports = {
importTar
};
};

View File

@@ -454,7 +454,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
});
for (const noteId in createdNoteIds) { // now the noteIds are unique
await noteService.scanForLinks(await repository.getNotes(noteId));
await noteService.scanForLinks(await repository.getNote(noteId));
if (!metaFile) {
// if there's no meta file then the notes are created based on the order in that tar file but that
@@ -481,4 +481,4 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
module.exports = {
importZip
};
};

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) {

View File

@@ -205,6 +205,14 @@ function formatDownloadTitle(filename, type, mime) {
}
}
if (mime === 'application/octet-stream') {
// we didn't find any good guess for this one, it will be better to just return
// the current name without fake extension. It's possible that the title still preserves to correct
// extension too
return filename;
}
return filename + '.' + extensions[0];
}
}