Compare commits

..

72 Commits

Author SHA1 Message Date
zadam c1092c97b5 release 0.51.0-beta
Publish Docker image / Push Docker image to multiple registries (push) Failing after 49s
2022-04-10 14:13:51 +02:00
zadam a04becc4ec dep upgrades 2022-04-10 14:13:45 +02:00
zadam f7d6bda49d added zooming/panning to mermaid diagrams, closes #2635 2022-04-03 23:14:47 +02:00
zadam b250f0a3bf Merge remote-tracking branch 'origin/stable' 2022-04-03 14:21:28 +02:00
zadam df1d94ec61 ckeditor 33 content styles 2022-03-26 14:10:29 +01:00
zadam e00fcd93a1 added #shareDisallowRobotIndexing label and reworked how the child-image exclusion works 2022-03-22 23:17:47 +01:00
zadam 0a95d0f6f5 ckeditor 33 2022-03-22 22:20:44 +01:00
zadam 569b8898ef release 0.50.3
Publish Docker image / Push Docker image to multiple registries (push) Failing after 41s
2022-03-22 21:30:21 +01:00
zadam 091d6a1cf1 added #shareRaw label 2022-03-22 21:20:47 +01:00
zadam 228564f843 Merge remote-tracking branch 'origin/master' 2022-03-22 21:04:52 +01:00
zadam 17dd6141fb Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	Dockerfile
#	package-lock.json
#	package.json
#	src/services/cloning.js
2022-03-22 21:04:30 +01:00
zadam 2f57d55bea add allow robots.txt tag for /share 2022-03-22 21:02:32 +01:00
zadam 0cd690d980 fix inclusion of share.js in builds, fixes #2738 2022-03-21 23:28:19 +01:00
zadam 316a2aee1a invalidate flat text cache after login to protected session, fixes #2712 2022-03-20 22:23:48 +01:00
Matt 77ce56ba84 Don't show protected notes in shared tree (#2704) 2022-03-07 23:00:07 +01:00
zadam eba824a5b1 added app-info method to etapi #2697 2022-03-07 22:57:48 +01:00
zadam c9e72f8fb9 Merge remote-tracking branch 'origin/master' 2022-02-24 23:22:33 +01:00
zadam 4dd3fd9674 better error when password is not set, #2685 2022-02-24 23:22:20 +01:00
zadam 1690a55f7d Update README.ru.md 2022-02-24 20:38:29 +01:00
zadam dd29fc26e3 Update README-ZH_CN.md 2022-02-24 20:38:01 +01:00
zadam 67b5921d6c Update README.md 2022-02-24 20:37:12 +01:00
zadam 1b7bcc5cc1 Update README.md 2022-02-22 23:18:10 +01:00
zadam ea56bb772a logging improvements 2022-02-20 12:33:50 +01:00
zadam 9f33791922 fix uploading binary data through ETAPI, closes #2667 2022-02-18 22:34:46 +01:00
zadam d940590add raise line-height in share CSS to 1.5, fixes #2671 2022-02-18 22:00:58 +01:00
zadam a009b4cb6d remove debug logging 2022-02-16 22:31:02 +01:00
zadam 781be527ce Merge remote-tracking branch 'origin/master' 2022-02-16 22:16:25 +01:00
zadam f7e5d8f62d fix displaying hidden notes in the note's children list #2664 2022-02-16 22:16:15 +01:00
Nriver d6c0fc734f Fix data validation type for prefix (#2660)
* fix param validation

* fix data validation type for `prefix`
2022-02-15 22:49:49 +01:00
zadam db79b5ff53 fixed blurred selection background color, closes #2652 2022-02-14 22:59:03 +01:00
zadam 18d439dd44 clipper endpoint should also scan and download images as a fallback, #2621 2022-02-14 20:50:50 +01:00
zadam d2d2a6c086 ckeditor 32 2022-02-12 22:53:55 +01:00
zadam 5260689b8e include "dump-db" into released artifacts 2022-02-12 22:51:28 +01:00
zadam 78a2863b78 Merge remote-tracking branch 'origin/master' 2022-02-12 22:20:22 +01:00
zadam 5481375347 DB dump tool feature complete 2022-02-12 22:20:15 +01:00
zadam 4da2d2f516 Create CODE_OF_CONDUCT 2022-02-12 13:17:49 +01:00
zadam 23de8e881d make it clear that Dockerfile needs to be run through a script 2022-02-11 22:05:08 +01:00
zadam 59e8720866 fix importing unknown image types such as .DWG, closes #2628 2022-02-11 22:01:45 +01:00
zadam 67cce5f817 Merge remote-tracking branch 'origin/stable' 2022-02-10 23:40:18 +01:00
zadam 9924727729 Merge remote-tracking branch 'origin/master' 2022-02-10 23:40:10 +01:00
zadam 6c9fc364a3 added "DB dump" tool, WIP 2022-02-10 23:37:25 +01:00
Nriver 40598d2663 fix param validation (#2624)
(cherry picked from commit 1aeb674733)
2022-02-10 19:33:11 +01:00
Nriver 1aeb674733 fix param validation (#2624) 2022-02-10 19:28:42 +01:00
zadam 832cac106e release 0.50.2
Publish Docker image / Push Docker image to multiple registries (push) Failing after 34s
2022-02-09 22:52:36 +01:00
zadam 23daaa2387 fix exporting huge text notes (export would get stuck on HTML pretty printing) 2022-02-09 22:39:37 +01:00
zadam d6016f9b81 introduced mutex to avoid conflicts during tab closing 2022-02-09 21:21:17 +01:00
zadam 1dfde76b95 fix updating becca after undeleting notes 2022-02-08 23:38:54 +01:00
zadam 83f8fac088 fixed background color of input label in CKEditor search & replace, #2254 2022-02-07 23:01:14 +01:00
zadam 0ca9bff61b fix multiple UI bugs, closes #2616 2022-02-07 22:50:28 +01:00
zadam df91192b97 Merge remote-tracking branch 'origin/master' 2022-02-06 14:18:18 +01:00
zadam 97fd550402 add light anonymization option to the existing full anonymization 2022-02-06 13:49:33 +01:00
zadam eb579de199 Update README.md 2022-02-05 17:21:41 +01:00
zadam 5f2984aa57 Update README.md 2022-02-05 17:20:41 +01:00
zadam 98a79f6475 Update FUNDING.yml 2022-02-05 17:17:07 +01:00
zadam c09da2b7eb Update FUNDING.yml 2022-02-05 17:12:37 +01:00
zadam 600f74576d added #sentFromSender label, closes #445 2022-02-05 16:23:30 +01:00
zadam a21c49cba7 added "hideRelations" label to relation map 2022-02-05 13:02:19 +01:00
zadam 91e3dd022a added "move note" search action 2022-02-05 12:06:23 +01:00
zadam 478eca47f4 search string is now a textarea instead of input 2022-02-05 11:27:48 +01:00
zadam 1c4358086d release 0.50.1
Publish Docker image / Push Docker image to multiple registries (push) Failing after 39s
2022-02-02 21:38:21 +01:00
zadam 0917fc8be1 log messages now contain script context if available 2022-02-01 22:25:38 +01:00
zadam 6833959f3b added regular DB optimization 2022-02-01 21:36:52 +01:00
zadam 398376108d added integrity check button into advanced options. 2022-02-01 21:22:43 +01:00
zadam 61aa029582 Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	package-lock.json
2022-02-01 21:04:17 +01:00
zadam 46eaa63625 focus promoted/owned attributes input after activating the ribbon tab, closes #2606 2022-02-01 20:24:58 +01:00
zadam 1b54596c5e tweaks to handling of branch removal 2022-01-31 22:09:39 +01:00
zadam 2feb778d8d Merge remote-tracking branch 'origin/stable' 2022-01-31 21:25:37 +01:00
zadam 2075e89239 improved logging 2022-01-31 21:25:18 +01:00
zadam be44431fde fix setting becca's dateModified after marking as deleted 2022-01-30 13:18:22 +01:00
zadam 99cc4078c6 fix setting becca's utcDateModified after marking as deleted 2022-01-30 13:17:40 +01:00
zadam b8b6b38a20 added links to sharing/ETAPI to README 2022-01-22 12:54:14 +01:00
zadam efd8556129 release 0.50.0-beta 2022-01-19 20:24:54 +01:00
94 changed files with 3996 additions and 1640 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ image:
file: .gitpod.dockerfile
tasks:
- before: nvm install 16.13.2 && nvm use 16.13.2
- before: nvm install 16.14.2 && nvm use 16.14.2
init: npm install
command: npm run start-server
+1
View File
@@ -0,0 +1 @@
Please treat each other with respect and understanding.
+2 -1
View File
@@ -1,4 +1,5 @@
FROM node:16.13.2-alpine
# !!! Don't try to build this Dockerfile directly, run it through bin/build-docker.sh script !!!
FROM node:16.14.2-alpine
# Create app directory
WORKDIR /usr/src/app
+4
View File
@@ -7,6 +7,10 @@ Trilium Notes是一个分层的笔记应用程序,专注于建立大型个人
![](https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png)
Ukraine is currently suffering from Russian aggression, please consider donating to [one of these charities](https://old.reddit.com/r/ukraine/comments/s6g5un/want_to_support_ukraine_heres_a_list_of_charities/).
<img src="https://upload.wikimedia.org/wikipedia/commons/4/49/Flag_of_Ukraine.svg" alt="drawing" width="600"/>
## 特性
* 笔记可以排列成任意深的树。单个笔记可以放在树中的多个位置(请参阅[克隆](https://github.com/zadam/trilium/wiki/Cloning-notes)
+11
View File
@@ -7,6 +7,10 @@ Trilium Notes is a hierarchical note taking application with focus on building l
![](https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png)
Ukraine is currently suffering from Russian aggression, please consider donating to [one of these charities](https://old.reddit.com/r/ukraine/comments/s6g5un/want_to_support_ukraine_heres_a_list_of_charities/).
<img src="https://upload.wikimedia.org/wikipedia/commons/4/49/Flag_of_Ukraine.svg" alt="drawing" width="600"/>
## Features
* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://github.com/zadam/trilium/wiki/Cloning-notes))
@@ -16,9 +20,12 @@ Trilium Notes is a hierarchical note taking application with focus on building l
* Seamless [note versioning](https://github.com/zadam/trilium/wiki/Note-revisions)
* Note [attributes](https://github.com/zadam/trilium/wiki/Attributes) can be used for note organization, querying and advanced [scripting](https://github.com/zadam/trilium/wiki/Scripts)
* [Synchronization](https://github.com/zadam/trilium/wiki/Synchronization) with self-hosted sync server
* there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting)
* [Sharing](https://github.com/zadam/trilium/wiki/Sharing) (publishing) notes to public internet
* Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes) with per-note granularity
* [Relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-map) for visualizing notes and their relations
* [Scripting](https://github.com/zadam/trilium/wiki/Scripts) - see [Advanced showcases](https://github.com/zadam/trilium/wiki/Advanced-showcases)
* [REST API](https://github.com/zadam/trilium/wiki/ETAPI) for automation
* Scales well in both usability and performance upwards of 100 000 notes
* Touch optimized [mobile frontend](https://github.com/zadam/trilium/wiki/Mobile-frontend) for smartphones and tablets
* [Night theme](https://github.com/zadam/trilium/wiki/Themes)
@@ -58,6 +65,10 @@ npm run start-server
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-map)
## Donating
You can donate using GitHub Sponsors, [PayPal](https://paypal.me/za4am) or Bitcoin (bitcoin:bc1qv3svjn40v89mnkre5vyvs2xw6y8phaltl385d2).
## License
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+4
View File
@@ -7,6 +7,10 @@ Trilium Notes – это приложение для заметок с иера
![](https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png)
Ukraine is currently suffering from Russian aggression, please consider donating to [one of these charities](https://old.reddit.com/r/ukraine/comments/s6g5un/want_to_support_ukraine_heres_a_list_of_charities/).
<img src="https://upload.wikimedia.org/wikipedia/commons/4/49/Flag_of_Ukraine.svg" alt="drawing" width="600"/>
## Возможности
* Заметки можно расположить в виде дерева произвольной глубины. Отдельную заметку можно разместить в нескольких местах дерева (см. [клонирование](https://github.com/zadam/trilium/wiki/Cloning-notes))
+2
View File
@@ -29,6 +29,8 @@ rm -r $BUILD_DIR/swiftshader
cp bin/tpl/anonymize-database.sql $BUILD_DIR/
cp -r dump-db $BUILD_DIR/
cp bin/tpl/trilium-portable.sh $BUILD_DIR/
chmod 755 $BUILD_DIR/trilium-portable.sh
+2
View File
@@ -25,6 +25,8 @@ mv "./dist/Trilium Notes-darwin-x64" $BUILD_DIR
cp bin/tpl/anonymize-database.sql $BUILD_DIR/
cp -r dump-db $BUILD_DIR/
echo "Zipping mac x64 electron distribution..."
VERSION=`jq -r ".version" package.json`
+3 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
PKG_DIR=dist/trilium-linux-x64-server
NODE_VERSION=16.13.2
NODE_VERSION=16.14.2
if [ "$1" != "DONTCOPY" ]
then
@@ -30,6 +30,8 @@ chmod 755 $PKG_DIR/trilium.sh
cp bin/tpl/anonymize-database.sql $PKG_DIR/
cp -r dump-db $PKG_DIR/
VERSION=`jq -r ".version" package.json`
cd dist
+2
View File
@@ -27,6 +27,8 @@ rm -r $BUILD_DIR/swiftshader
cp bin/tpl/anonymize-database.sql $BUILD_DIR/
cp -r dump-db $BUILD_DIR/
cp bin/tpl/trilium-{portable,no-cert-check,safe-mode}.{bat,ps1} $BUILD_DIR/
echo "Zipping windows x64 electron distribution..."
+8 -2
View File
@@ -5,7 +5,7 @@ if [[ $# -eq 0 ]] ; then
exit 1
fi
n exec 16.13.2 npm run webpack
n exec 16.14.2 npm run webpack
DIR=$1
@@ -14,6 +14,9 @@ mkdir $DIR
echo "Copying Trilium to build directory $DIR"
cp -r dump-db $DIR/
rm -rf $DIR/dump-db/node_modules
cp -r images $DIR/
cp -r libraries $DIR/
cp -r src $DIR/
@@ -27,7 +30,7 @@ cp -r electron.js $DIR/
cp webpack-* $DIR/
# run in subshell (so we return to original dir)
(cd $DIR && n exec 16.13.2 npm install --only=prod)
(cd $DIR && n exec 16.14.2 npm install --only=prod)
# cleanup of useless files in dependencies
rm -r $DIR/node_modules/image-q/demo
@@ -44,8 +47,11 @@ find $DIR/node_modules -name demo -exec rm -rf {} \;
find $DIR/libraries -name "*.map" -type f -delete
cp $DIR/src/public/app/share.js $DIR/src/public/app-dist/
rm -rf $DIR/src/public/app
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\/share.js/app-dist\/share.js/g' $DIR/src/views/share/*.ejs
+1 -1
View File
@@ -4,4 +4,4 @@ const anonymizationService = require('../src/services/anonymization');
const fs = require('fs');
const path = require('path');
fs.writeFileSync(path.resolve(__dirname, 'tpl', 'anonymize-database.sql'), anonymizationService.getAnonymizationScript());
fs.writeFileSync(path.resolve(__dirname, 'tpl', 'anonymize-database.sql'), anonymizationService.getFullAnonymizationScript());
+4 -4
View File
@@ -1,13 +1,13 @@
UPDATE etapi_tokens SET tokenHash = 'API token hash value';
UPDATE notes SET title = 'title';
UPDATE notes SET title = 'title' WHERE title NOT IN ('root', 'hidden', 'share');
UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL;
UPDATE note_revisions SET title = 'title';
UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL;
UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN('inbox', 'disableVersioning', 'calendarRoot', 'archived', 'excludeFromExport', 'disableInclusion', 'appCss', 'appTheme', 'hidePromotedAttributes', 'readOnly', 'autoReadOnlyDisabled', 'hoistedCssClass', 'cssClass', 'iconClass', 'keyboardShortcut', 'run', 'runOnInstance', 'runAtHour', 'customRequestHandler', 'customResourceProvider', 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'noteRevisionsWidgetDisabled', 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', 'workspaceTabBackgroundColor', 'searchHome', 'hoistedInbox', 'hoistedSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId', 'bookmarked', 'bookmarkFolder', 'sorted', 'top', 'fullContentWidth', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation', 'runOnNoteTitleChange', 'runOnNoteChange', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareFavicon');
UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN ('inbox', 'disableVersioning', 'calendarRoot', 'archived', 'excludeFromExport', 'disableInclusion', 'appCss', 'appTheme', 'hidePromotedAttributes', 'readOnly', 'autoReadOnlyDisabled', 'hoistedCssClass', 'cssClass', 'iconClass', 'keyboardShortcut', 'run', 'runOnInstance', 'runAtHour', 'customRequestHandler', 'customResourceProvider', 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'noteRevisionsWidgetDisabled', 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', 'workspaceTabBackgroundColor', 'searchHome', 'hoistedInbox', 'hoistedSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId', 'bookmarked', 'bookmarkFolder', 'sorted', 'top', 'fullContentWidth', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation', 'runOnNoteTitleChange', 'runOnNoteChange', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareFavicon');
UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL;
UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN('inbox', 'disableVersioning', 'calendarRoot', 'archived', 'excludeFromExport', 'disableInclusion', 'appCss', 'appTheme', 'hidePromotedAttributes', 'readOnly', 'autoReadOnlyDisabled', 'hoistedCssClass', 'cssClass', 'iconClass', 'keyboardShortcut', 'run', 'runOnInstance', 'runAtHour', 'customRequestHandler', 'customResourceProvider', 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'noteRevisionsWidgetDisabled', 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', 'workspaceTabBackgroundColor', 'searchHome', 'hoistedInbox', 'hoistedSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId', 'bookmarked', 'bookmarkFolder', 'sorted', 'top', 'fullContentWidth', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss', 'shareRoot', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation', 'runOnNoteTitleChange', 'runOnNoteChange', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareFavicon');
UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN ('inbox', 'disableVersioning', 'calendarRoot', 'archived', 'excludeFromExport', 'disableInclusion', 'appCss', 'appTheme', 'hidePromotedAttributes', 'readOnly', 'autoReadOnlyDisabled', 'hoistedCssClass', 'cssClass', 'iconClass', 'keyboardShortcut', 'run', 'runOnInstance', 'runAtHour', 'customRequestHandler', 'customResourceProvider', 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'noteRevisionsWidgetDisabled', 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', 'workspaceTabBackgroundColor', 'searchHome', 'hoistedInbox', 'hoistedSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId', 'bookmarked', 'bookmarkFolder', 'sorted', 'top', 'fullContentWidth', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss', 'shareRoot', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation', 'runOnNoteTitleChange', 'runOnNoteChange', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareFavicon');
UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL AND prefix != 'recovered';
UPDATE options SET value = 'anonymized' WHERE name IN
('documentId', 'documentSecret', 'encryptedDataKey',
'passwordVerificationHash', 'passwordVerificationSalt',
+34
View File
@@ -0,0 +1,34 @@
# Trilium Notes DB dump tool
This is a simple tool to dump the content of Trilium's document.db onto filesystem.
It is meant as a last resort solution when the standard mean to access your data (through main Trilium application) fail.
## Installation
This tool requires node.js, testing has been done on 16.14.2, but it will probably work on other versions as well.
```
npm install
```
## Running
See output of `node dump-db.js --help`:
```
dump-db.js <path_to_document> <target_directory>
dump the contents of document.db into the target directory
Positionals:
path_to_document path to the document.db
target_directory path of the directory into which the notes should be dumped
Options:
--help Show help [boolean]
--version Show version number [boolean]
--password Set password to be able to decrypt protected notes.[string]
--include-deleted If set to true, dump also deleted notes.
[boolean] [default: false]
```
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env node
const yargs = require('yargs/yargs')
const { hideBin } = require('yargs/helpers')
const dumpService = require("./inc/dump.js");
yargs(hideBin(process.argv))
.command('$0 <path_to_document> <target_directory>', 'dump the contents of document.db into the target directory', (yargs) => {
return yargs
.positional('path_to_document', { describe: 'path to the document.db' })
.positional('target_directory', { describe: 'path of the directory into which the notes should be dumped' })
}, (argv) => {
try {
dumpService.dumpDocument(argv.path_to_document, argv.target_directory, {
includeDeleted: argv.includeDeleted,
password: argv.password
});
}
catch (e) {
console.error(`Unrecoverable error:`, e);
process.exit(1);
}
})
.option('password', {
type: 'string',
description: 'Set password to be able to decrypt protected notes.'
})
.option('include-deleted', {
type: 'boolean',
default: false,
description: 'If set to true, dump also deleted notes.'
})
.parse();
+43
View File
@@ -0,0 +1,43 @@
const crypto = require("crypto");
const sql = require("./sql.js");
const decryptService = require("./decrypt.js");
function getDataKey(password) {
if (!password) {
return null;
}
try {
const passwordDerivedKey = getPasswordDerivedKey(password);
const encryptedDataKey = getOption('encryptedDataKey');
const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey, 16);
return decryptedDataKey;
}
catch (e) {
throw new Error(`Cannot read data key, the entered password might be wrong. The underlying error: '${e.message}', stack:\n${e.stack}`);
}
}
function getPasswordDerivedKey(password) {
const salt = getOption('passwordDerivedKeySalt');
return getScryptHash(password, salt);
}
function getScryptHash(password, salt) {
const hashed = crypto.scryptSync(password, salt, 32,
{N: 16384, r:8, p:1});
return hashed;
}
function getOption(name) {
return sql.getValue("SELECT value FROM options WHERE name = ?", [name]);
}
module.exports = {
getDataKey
};
+92
View File
@@ -0,0 +1,92 @@
const crypto = require("crypto");
function decryptString(dataKey, cipherText) {
const buffer = decrypt(dataKey, cipherText);
if (buffer === null) {
return null;
}
const str = buffer.toString('utf-8');
if (str === 'false') {
throw new Error("Could not decrypt string.");
}
return str;
}
function decrypt(key, cipherText, ivLength = 13) {
if (cipherText === null) {
return null;
}
if (!key) {
return "[protected]";
}
try {
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), 'base64');
const iv = cipherTextBufferWithIv.slice(0, ivLength);
const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength);
const decipher = crypto.createDecipheriv('aes-128-cbc', pad(key), pad(iv));
const decryptedBytes = Buffer.concat([decipher.update(cipherTextBuffer), decipher.final()]);
const digest = decryptedBytes.slice(0, 4);
const payload = decryptedBytes.slice(4);
const computedDigest = shaArray(payload).slice(0, 4);
if (!arraysIdentical(digest, computedDigest)) {
return false;
}
return payload;
}
catch (e) {
// recovery from https://github.com/zadam/trilium/issues/510
if (e.message && e.message.includes("WRONG_FINAL_BLOCK_LENGTH")) {
log.info("Caught WRONG_FINAL_BLOCK_LENGTH, returning cipherText instead");
return cipherText;
}
else {
throw e;
}
}
}
function pad(data) {
if (data.length > 16) {
data = data.slice(0, 16);
}
else if (data.length < 16) {
const zeros = Array(16 - data.length).fill(0);
data = Buffer.concat([data, Buffer.from(zeros)]);
}
return Buffer.from(data);
}
function arraysIdentical(a, b) {
let i = a.length;
if (i !== b.length) return false;
while (i--) {
if (a[i] !== b[i]) return false;
}
return true;
}
function shaArray(content) {
// we use this as simple checksum and don't rely on its security so SHA-1 is good enough
return crypto.createHash('sha1').update(content).digest();
}
module.exports = {
decrypt,
decryptString
};
+171
View File
@@ -0,0 +1,171 @@
const fs = require("fs");
const sanitize = require("sanitize-filename");
const sql = require("./sql.js");
const decryptService = require("./decrypt.js");
const dataKeyService = require("./data_key.js");
const extensionService = require("./extension.js");
function dumpDocument(documentPath, targetPath, options) {
const stats = {
succeeded: 0,
failed: 0,
protected: 0,
deleted: 0
};
validatePaths(documentPath, targetPath);
sql.openDatabase(documentPath);
const dataKey = dataKeyService.getDataKey(options.password);
const existingPaths = {};
const noteIdToPath = {};
dumpNote(targetPath, 'root');
printDumpResults(stats, options);
function dumpNote(targetPath, noteId) {
console.log(`Reading note '${noteId}'`);
let childTargetPath, note, fileNameWithPath;
try {
note = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
if (note.isDeleted) {
stats.deleted++;
if (!options.includeDeleted) {
console.log(`Note '${noteId}' is deleted and --include-deleted option is not used, skipping.`);
return;
}
}
if (note.isProtected) {
stats.protected++;
note.title = decryptService.decryptString(dataKey, note.title);
}
let safeTitle = sanitize(note.title);
if (safeTitle.length > 20) {
safeTitle = safeTitle.substring(0, 20);
}
childTargetPath = targetPath + '/' + safeTitle;
for (let i = 1; i < 100000 && childTargetPath in existingPaths; i++) {
childTargetPath = targetPath + '/' + safeTitle + '_' + i;
}
existingPaths[childTargetPath] = true;
if (note.noteId in noteIdToPath) {
const message = `Note '${noteId}' has been already dumped to ${noteIdToPath[note.noteId]}`;
console.log(message);
fs.writeFileSync(childTargetPath, message);
return;
}
let {content} = sql.getRow("SELECT content FROM note_contents WHERE noteId = ?", [noteId]);
if (content !== null && note.isProtected && dataKey) {
content = decryptService.decrypt(dataKey, content);
}
if (isContentEmpty(content)) {
console.log(`Note '${noteId}' is empty, skipping.`);
} else {
fileNameWithPath = extensionService.getFileName(note, childTargetPath, safeTitle);
fs.writeFileSync(fileNameWithPath, content);
stats.succeeded++;
console.log(`Dumped note '${noteId}' into ${fileNameWithPath} successfully.`);
}
noteIdToPath[noteId] = childTargetPath;
}
catch (e) {
console.error(`DUMPERROR: Writing '${noteId}' failed with error '${e.message}':\n${e.stack}`);
stats.failed++;
}
const childNoteIds = sql.getColumn("SELECT noteId FROM branches WHERE parentNoteId = ?", [noteId]);
if (childNoteIds.length > 0) {
if (childTargetPath === fileNameWithPath) {
childTargetPath += '_dir';
}
try {
fs.mkdirSync(childTargetPath, {recursive: true});
}
catch (e) {
console.error(`DUMPERROR: Creating directory ${childTargetPath} failed with error '${e.message}'`);
}
for (const childNoteId of childNoteIds) {
dumpNote(childTargetPath, childNoteId);
}
}
}
}
function printDumpResults(stats, options) {
console.log('\n----------------------- STATS -----------------------');
console.log('Successfully dumpted notes: ', stats.succeeded.toString().padStart(5, ' '));
console.log('Protected notes: ', stats.protected.toString().padStart(5, ' '), options.password ? '' : '(skipped)');
console.log('Failed notes: ', stats.failed.toString().padStart(5, ' '));
console.log('Deleted notes: ', stats.deleted.toString().padStart(5, ' '), options.includeDeleted ? "(dumped)" : "(at least, skipped)");
console.log('-----------------------------------------------------');
if (!options.password && stats.protected > 0) {
console.log("\nWARNING: protected notes are present in the document but no password has been provided. Protected notes have not been dumped.");
}
}
function isContentEmpty(content) {
if (!content) {
return true;
}
if (typeof content === "string") {
return !content.trim() || content.trim() === '<p></p>';
}
else if (Buffer.isBuffer(content)) {
return content.length === 0;
}
else {
return false;
}
}
function validatePaths(documentPath, targetPath) {
if (!fs.existsSync(documentPath)) {
console.error(`Path to document '${documentPath}' has not been found. Run with --help to see usage.`);
process.exit(1);
}
if (!fs.existsSync(targetPath)) {
const ret = fs.mkdirSync(targetPath, {recursive: true});
if (!ret) {
console.error(`Target path '${targetPath}' could not be created. Run with --help to see usage.`);
process.exit(1);
}
}
}
module.exports = {
dumpDocument
};
+34
View File
@@ -0,0 +1,34 @@
const path = require("path");
const mimeTypes = require("mime-types");
function getFileName(note, childTargetPath, safeTitle) {
let existingExtension = path.extname(safeTitle).toLowerCase();
let newExtension;
if (note.type === 'text') {
newExtension = 'html';
} else if (note.mime === 'application/x-javascript' || note.mime === 'text/javascript') {
newExtension = 'js';
} else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it
newExtension = null;
} else {
if (note.mime?.toLowerCase()?.trim() === "image/jpg") { // image/jpg is invalid but pretty common
newExtension = 'jpg';
} else {
newExtension = mimeTypes.extension(note.mime) || "dat";
}
}
let fileNameWithPath = childTargetPath;
// if the note is already named with extension (e.g. "jquery"), then it's silly to append exact same extension again
if (newExtension && existingExtension !== "." + newExtension.toLowerCase()) {
fileNameWithPath += "." + newExtension;
}
return fileNameWithPath;
}
module.exports = {
getFileName
};
+17
View File
@@ -0,0 +1,17 @@
const Database = require("better-sqlite3");
let dbConnection;
const openDatabase = (documentPath) => { dbConnection = new Database(documentPath, { readonly: true }) };
const getRow = (query, params = []) => dbConnection.prepare(query).get(params);
const getRows = (query, params = []) => dbConnection.prepare(query).all(params);
const getValue = (query, params = []) => dbConnection.prepare(query).pluck().get(params);
const getColumn = (query, params = []) => dbConnection.prepare(query).pluck().all(params);
module.exports = {
openDatabase,
getRow,
getRows,
getValue,
getColumn
};
+1553
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "dump-db",
"version": "1.0.0",
"description": "Standalone tool to dump contents of Trilium document.db file into a directory tree of notes",
"main": "dump-db.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/zadam/trilium.git"
},
"author": "zadam",
"license": "ISC",
"bugs": {
"url": "https://github.com/zadam/trilium/issues"
},
"homepage": "https://github.com/zadam/trilium/dump-db#readme",
"dependencies": {
"better-sqlite3": "7.5.0",
"mime-types": "2.1.34",
"sanitize-filename": "1.6.3",
"yargs": "17.3.1"
}
}
+203 -198
View File
@@ -4,6 +4,12 @@
display: none;
}
/*
* CKEditor 5 (v33.0.0) content styles.
* Generated on Fri, 11 Mar 2022 14:34:26 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
*/
:root {
--ck-color-image-caption-background: hsl(0, 0%, 97%);
--ck-color-image-caption-text: hsl(0, 0%, 20%);
@@ -22,6 +28,43 @@
--ck-todo-list-checkmark-size: 16px;
}
/* ckeditor5-basic-styles/theme/code.css */
.ck-content code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-tiny {
font-size: .7em;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-small {
font-size: .85em;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-big {
font-size: 1.4em;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-huge {
font-size: 1.8em;
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content blockquote {
overflow: hidden;
padding-right: 1.5em;
padding-left: 1.5em;
margin-left: 0;
margin-right: 0;
font-style: italic;
border-left: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content[dir="rtl"] blockquote {
border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
@@ -48,21 +91,53 @@
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-tiny {
font-size: .7em;
/* ckeditor5-image/theme/image.css */
.ck-content .image {
display: table;
clear: both;
text-align: center;
margin: 0.9em auto;
min-width: 50px;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-small {
font-size: .85em;
/* ckeditor5-image/theme/image.css */
.ck-content .image img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 100%;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-big {
font-size: 1.4em;
/* ckeditor5-image/theme/image.css */
.ck-content .image-inline {
/*
* Normally, the .image-inline would have "display: inline-block" and "img { width: 100% }" (to follow the wrapper while resizing).;
* Unfortunately, together with "srcset", it gets automatically stretched up to the width of the editing root.
* This strange behavior does not happen with inline-flex.
*/
display: inline-flex;
max-width: 100%;
align-items: flex-start;
}
/* ckeditor5-font/theme/fontsize.css */
.ck-content .text-huge {
font-size: 1.8em;
/* ckeditor5-image/theme/image.css */
.ck-content .image-inline picture {
display: flex;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image-inline picture,
.ck-content .image-inline img {
flex-grow: 1;
flex-shrink: 1;
max-width: 100%;
}
/* ckeditor5-image/theme/imagecaption.css */
.ck-content .image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: var(--ck-color-image-caption-text);
background-color: var(--ck-color-image-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-left,
@@ -125,54 +200,6 @@
.ck-content .image-inline.image-style-align-right {
margin-left: var(--ck-inline-image-style-spacing);
}
/* ckeditor5-image/theme/imagecaption.css */
.ck-content .image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: var(--ck-color-image-caption-text);
background-color: var(--ck-color-image-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image {
display: table;
clear: both;
text-align: center;
margin: 0.9em auto;
min-width: 50px;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 100%;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image-inline {
/*
* Normally, the .image-inline would have "display: inline-block" and "img { width: 100% }" (to follow the wrapper while resizing).;
* Unfortunately, together with "srcset", it gets automatically stretched up to the width of the editing root.
* This strange behavior does not happen with inline-flex.
*/
display: inline-flex;
max-width: 100%;
align-items: flex-start;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image-inline picture {
display: flex;
}
/* ckeditor5-image/theme/image.css */
.ck-content .image-inline picture,
.ck-content .image-inline img {
flex-grow: 1;
flex-shrink: 1;
max-width: 100%;
}
/* ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized {
max-width: 100%;
@@ -187,141 +214,9 @@
.ck-content .image.image_resized > figcaption {
display: block;
}
/* ckeditor5-code-block/theme/codeblock.css */
.ck-content pre {
padding: 1em;
color: hsl(0, 0%, 20.8%);
background: hsla(0, 0%, 78%, 0.3);
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
text-align: left;
direction: ltr;
tab-size: 4;
white-space: pre-wrap;
font-style: normal;
min-width: 200px;
}
/* ckeditor5-code-block/theme/codeblock.css */
.ck-content pre code {
background: unset;
padding: 0;
border-radius: 0;
}
/* ckeditor5-horizontal-line/theme/horizontalline.css */
.ck-content hr {
margin: 15px 0;
height: 4px;
background: hsl(0, 0%, 87%);
border: 0;
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content blockquote {
overflow: hidden;
padding-right: 1.5em;
padding-left: 1.5em;
margin-left: 0;
margin-right: 0;
/* ckeditor5-language/theme/language.css */
.ck-content span[lang] {
font-style: italic;
border-left: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content[dir="rtl"] blockquote {
border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-basic-styles/theme/code.css */
.ck-content code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
}
/* ckeditor5-table/theme/table.css */
.ck-content .table {
margin: 0.9em auto;
display: table;
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
height: 100%;
border: 1px double hsl(0, 0%, 70%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table td,
.ck-content .table table th {
min-width: 2em;
padding: .4em;
border: 1px solid hsl(0, 0%, 75%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table th {
font-weight: bold;
background: hsla(0, 0%, 0%, 5%);
}
/* ckeditor5-table/theme/table.css */
.ck-content[dir="rtl"] .table th {
text-align: right;
}
/* ckeditor5-table/theme/table.css */
.ck-content[dir="ltr"] .table th {
text-align: left;
}
/* ckeditor5-table/theme/tablecaption.css */
.ck-content .table > figcaption {
display: table-caption;
caption-side: top;
word-break: break-word;
text-align: center;
color: var(--ck-color-table-caption-text);
background-color: var(--ck-color-table-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
position: relative;
clear: both;
padding: 5px 0;
display: flex;
align-items: center;
justify-content: center;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
content: '';
position: absolute;
border-bottom: 2px dashed hsl(0, 0%, 77%);
width: 100%;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break__label {
position: relative;
z-index: 1;
padding: .3em .6em;
display: block;
text-transform: uppercase;
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
font-size: 0.75em;
font-weight: bold;
color: hsl(0, 0%, 20%);
background: hsl(0, 0%, 100%);
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .media {
clear: both;
margin: 0.9em 0;
display: block;
min-width: 15em;
}
/* ckeditor5-list/theme/todolist.css */
.ck-content .todo-list {
@@ -390,9 +285,120 @@
.ck-content .todo-list .todo-list__label .todo-list__label__description {
vertical-align: middle;
}
/* ckeditor5-language/theme/language.css */
.ck-content span[lang] {
font-style: italic;
/* ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .media {
clear: both;
margin: 0.9em 0;
display: block;
min-width: 15em;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
position: relative;
clear: both;
padding: 5px 0;
display: flex;
align-items: center;
justify-content: center;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
content: '';
position: absolute;
border-bottom: 2px dashed hsl(0, 0%, 77%);
width: 100%;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break__label {
position: relative;
z-index: 1;
padding: .3em .6em;
display: block;
text-transform: uppercase;
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
font-size: 0.75em;
font-weight: bold;
color: hsl(0, 0%, 20%);
background: hsl(0, 0%, 100%);
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* ckeditor5-table/theme/table.css */
.ck-content .table {
margin: 0.9em auto;
display: table;
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
height: 100%;
border: 1px double hsl(0, 0%, 70%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table td,
.ck-content .table table th {
min-width: 2em;
padding: .4em;
border: 1px solid hsl(0, 0%, 75%);
}
/* ckeditor5-table/theme/table.css */
.ck-content .table table th {
font-weight: bold;
background: hsla(0, 0%, 0%, 5%);
}
/* ckeditor5-table/theme/table.css */
.ck-content[dir="rtl"] .table th {
text-align: right;
}
/* ckeditor5-table/theme/table.css */
.ck-content[dir="ltr"] .table th {
text-align: left;
}
/* ckeditor5-table/theme/tablecaption.css */
.ck-content .table > figcaption {
display: table-caption;
caption-side: top;
word-break: break-word;
text-align: center;
color: var(--ck-color-table-caption-text);
background-color: var(--ck-color-table-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* ckeditor5-code-block/theme/codeblock.css */
.ck-content pre {
padding: 1em;
color: hsl(0, 0%, 20.8%);
background: hsla(0, 0%, 78%, 0.3);
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
text-align: left;
direction: ltr;
tab-size: 4;
white-space: pre-wrap;
font-style: normal;
min-width: 200px;
}
/* ckeditor5-code-block/theme/codeblock.css */
.ck-content pre code {
background: unset;
padding: 0;
border-radius: 0;
}
/* ckeditor5-horizontal-line/theme/horizontalline.css */
.ck-content hr {
margin: 15px 0;
height: 4px;
background: hsl(0, 0%, 87%);
border: 0;
}
/* ckeditor5-mention/theme/mention.css */
.ck-content .mention {
@@ -409,4 +415,3 @@
display: none;
}
}
+3 -2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1017 -1222
View File
File diff suppressed because it is too large Load Diff
+21 -21
View File
@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.50.0-beta",
"version": "0.51.0-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -26,40 +26,40 @@
"dependencies": {
"archiver": "5.3.0",
"async-mutex": "0.3.2",
"axios": "0.24.0",
"axios": "0.26.1",
"better-sqlite3": "7.4.5",
"chokidar": "3.5.2",
"chokidar": "3.5.3",
"cls-hooked": "4.2.2",
"commonmark": "0.30.0",
"cookie-parser": "1.4.6",
"csurf": "1.11.0",
"dayjs": "1.10.7",
"dayjs": "1.11.0",
"ejs": "3.1.6",
"electron-debug": "3.2.0",
"electron-dl": "3.3.0",
"electron-dl": "3.3.1",
"electron-find": "1.0.7",
"electron-window-state": "5.0.3",
"@electron/remote": "2.0.1",
"express": "4.17.2",
"@electron/remote": "2.0.8",
"express": "4.17.3",
"express-partial-content": "1.0.2",
"express-rate-limit": "6.1.0",
"express-rate-limit": "6.3.0",
"express-session": "1.17.2",
"fs-extra": "10.0.0",
"helmet": "5.0.1",
"fs-extra": "10.0.1",
"helmet": "5.0.2",
"html": "1.0.0",
"html2plaintext": "2.1.4",
"http-proxy-agent": "5.0.0",
"https-proxy-agent": "5.0.0",
"image-type": "4.1.0",
"ini": "2.0.0",
"ini": "3.0.0",
"is-animated": "2.0.2",
"is-svg": "4.3.2",
"jimp": "0.16.1",
"joplin-turndown-plugin-gfm": "1.0.12",
"jsdom": "19.0.0",
"mime-types": "2.1.34",
"mime-types": "2.1.35",
"multer": "1.4.4",
"node-abi": "3.5.0",
"node-abi": "3.8.0",
"normalize-strings": "1.1.1",
"open": "8.4.0",
"portscanner": "2.2.0",
@@ -67,9 +67,9 @@
"request": "2.88.2",
"rimraf": "3.0.2",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.6.1",
"sanitize-html": "2.7.0",
"sax": "1.2.4",
"semver": "7.3.5",
"semver": "7.3.6",
"serve-favicon": "2.5.0",
"session-file-store": "1.5.0",
"stream-throttle": "0.1.3",
@@ -77,22 +77,22 @@
"tmp": "0.2.1",
"turndown": "7.1.1",
"unescape": "1.0.1",
"ws": "8.4.2",
"ws": "8.5.0",
"yauzl": "2.10.0"
},
"devDependencies": {
"cross-env": "7.0.3",
"electron": "16.0.7",
"electron-builder": "22.14.5",
"electron": "16.2.1",
"electron-builder": "23.0.3",
"electron-packager": "15.4.0",
"electron-rebuild": "3.2.7",
"esm": "3.2.25",
"jasmine": "4.0.2",
"jsdoc": "3.6.7",
"jsdoc": "3.6.10",
"lorem-ipsum": "2.0.4",
"rcedit": "3.0.1",
"webpack": "5.66.0",
"webpack-cli": "4.9.1"
"webpack": "5.72.0",
"webpack-cli": "4.9.2"
},
"optionalDependencies": {
"electron-installer-debian": "3.1.0"
+1
View File
@@ -25,6 +25,7 @@ app.use(helmet({
app.use(express.text({limit: '500mb'}));
app.use(express.json({limit: '500mb'}));
app.use(express.raw({limit: '500mb'}));
app.use(express.urlencoded({extended: false}));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
+14 -3
View File
@@ -6,6 +6,7 @@ const entityChangesService = require('../../services/entity_changes');
const eventService = require("../../services/events");
const dateUtils = require("../../services/date_utils");
const cls = require("../../services/cls");
const log = require("../../services/log");
let becca = null;
@@ -103,15 +104,21 @@ class AbstractEntity {
const entityId = this[this.constructor.primaryKeyName];
const entityName = this.constructor.entityName;
this.utcDateModified = dateUtils.utcNowDateTime();
sql.execute(`UPDATE ${entityName} SET isDeleted = 1, deleteId = ?, utcDateModified = ?
WHERE ${this.constructor.primaryKeyName} = ?`,
[deleteId, dateUtils.utcNowDateTime(), entityId]);
[deleteId, this.utcDateModified, entityId]);
if (this.dateModified) {
this.dateModified = dateUtils.localNowDateTime();
sql.execute(`UPDATE ${entityName} SET dateModified = ? WHERE ${this.constructor.primaryKeyName} = ?`,
[dateUtils.localNowDateTime(), entityId]);
[this.dateModified, entityId]);
}
log.info(`Marking ${entityName} ${entityId} as deleted`);
this.addEntityChange(true);
eventService.emit(eventService.ENTITY_DELETED, { entityName, entityId, entity: this });
@@ -121,9 +128,13 @@ class AbstractEntity {
const entityId = this[this.constructor.primaryKeyName];
const entityName = this.constructor.entityName;
this.utcDateModified = dateUtils.utcNowDateTime();
sql.execute(`UPDATE ${entityName} SET isDeleted = 1, utcDateModified = ?
WHERE ${this.constructor.primaryKeyName} = ?`,
[dateUtils.utcNowDateTime(), entityId]);
[this.utcDateModified, entityId]);
log.info(`Marking ${entityName} ${entityId} as deleted`);
this.addEntityChange(true);
+3 -1
View File
@@ -42,7 +42,9 @@ class EtapiToken extends AbstractEntity {
/** @type {boolean} */
this.isDeleted = !!row.isDeleted;
this.becca.etapiTokens[this.etapiTokenId] = this;
if (this.etapiTokenId) {
this.becca.etapiTokens[this.etapiTokenId] = this;
}
}
init() {
+1
View File
@@ -1127,6 +1127,7 @@ class Note extends AbstractEntity {
if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
try {
this.title = protectedSessionService.decryptString(this.title);
this.flatTextCache = null;
this.isDecrypted = true;
}
+12
View File
@@ -0,0 +1,12 @@
const appInfo = require('../services/app_info');
const eu = require("./etapi_utils.js");
function register(router) {
eu.route(router, 'get', '/etapi/app-info', (req, res, next) => {
res.status(200).json(appInfo);
});
}
module.exports = {
register
};
+60
View File
@@ -589,6 +589,24 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
/app-info:
get:
description: returns information about the running Trilium instance
operationId: getAppInfo
responses:
'200':
description: app info
content:
application/json:
schema:
$ref: '#/components/schemas/AppInfo'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
securitySchemes:
EtapiTokenAuth:
@@ -777,6 +795,48 @@ components:
type: string
pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z'
example: 2021-12-31 19:18:11.939Z
AppInfo:
type: object
required:
- statu
- code
- message
properties:
appVersion:
type: string
description: Trilium version
example: 0.50.2
dbVersion:
type: integer
format: int32
description: DB version
example: 194
syncVersion:
type: integer
format: int32
description: Sync protocol version
example: 25
buildDate:
type: string
format: date-time
description: build date
example: 2022-02-09T22:52:36+01:00
buildRevision:
type: string
description: git build revision
example: 23daaa2387a0655685377f0a541d154aeec2aae8
dataDirectory:
type: string
description: data directory where Trilium stores files
example: /home/user/data
clipperProtocolVersion:
type: string
description: version of the supported Trilium Web Clipper protocol
example: 1.0
utcDateTime:
type: string
description: current UTC date time
example: 2022-03-07T21:54:25.277Z
Error:
type: object
required:
+3 -3
View File
@@ -46,7 +46,7 @@ function register(router) {
'mime': [v.notNull, v.isString],
'content': [v.notNull, v.isString],
'notePosition': [v.notNull, v.isInteger],
'prefix': [v.notNull, v.isInteger],
'prefix': [v.notNull, v.isString],
'isExpanded': [v.notNull, v.isBoolean],
'noteId': [v.notNull, v.isValidEntityId],
'branchId': [v.notNull, v.isValidEntityId],
@@ -162,7 +162,7 @@ function parseBoolean(obj, name) {
return obj[name] === 'true';
}
function parseInteger(obj, name) {
function parseOrderDirection(obj, name) {
if (!(name in obj)) {
return undefined;
}
@@ -176,7 +176,7 @@ function parseInteger(obj, name) {
return integer;
}
function parseOrderDirection(obj, name) {
function parseInteger(obj, name) {
if (!(name in obj)) {
return undefined;
}
+45 -6
View File
@@ -13,16 +13,32 @@ const TPL = `
<br/>
<br/>
<h4>Database integrity check</h4>
<p>This will check that the database is not corrupted on the SQLite level. It might take some time, depending on the DB size.</p>
<button id="check-integrity-button" class="btn">Check database integrity</button><br/><br/>
<h4>Consistency checks</h4>
<button id="find-and-fix-consistency-issues-button" class="btn">Find and fix consistency issues</button><br/><br/>
<h4>Anonymize database</h4>
<h5>Full anonymization</h5>
<p>This action will create a new copy of the database and anonymize 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/>
<button id="anonymize-full-button" class="btn">Save fully anonymized database</button><br/><br/>
<h5>Light anonymization</h5>
<p>This action will create a new copy of the database and do a light anonymization on it - specifically only content of all notes will be removed, but titles and attributes will remaing. Additionally, custom JS frontend/backend script notes and custom widgets will remain. This provides more context to debug the issues.</p>
<p>You can decide yourself if you want to provide fully or lightly anonymized database. Even fully anonymized DB is very useful, however in some cases lightly anonymized database can speed up the process of bug identification and fixing.</p>
<button id="anonymize-light-button" class="btn">Save lightly anonymized database</button><br/><br/>
<h4>Vacuum database</h4>
@@ -36,10 +52,11 @@ export default class AdvancedOptions {
this.$forceFullSyncButton = $("#force-full-sync-button");
this.$fillEntityChangesButton = $("#fill-entity-changes-button");
this.$anonymizeButton = $("#anonymize-button");
this.$backupDatabaseButton = $("#backup-database-button");
this.$anonymizeFullButton = $("#anonymize-full-button");
this.$anonymizeLightButton = $("#anonymize-light-button");
this.$vacuumDatabaseButton = $("#vacuum-database-button");
this.$findAndFixConsistencyIssuesButton = $("#find-and-fix-consistency-issues-button");
this.$checkIntegrityButton = $("#check-integrity-button");
this.$forceFullSyncButton.on('click', async () => {
await server.post('sync/force-full-sync');
@@ -53,14 +70,25 @@ export default class AdvancedOptions {
toastService.showMessage("Sync rows filled successfully");
});
this.$anonymizeButton.on('click', async () => {
const resp = await server.post('database/anonymize');
this.$anonymizeFullButton.on('click', async () => {
const resp = await server.post('database/anonymize/full');
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);
toastService.showMessage(`Created fully anonymized database in ${resp.anonymizedFilePath}`, 10000);
}
});
this.$anonymizeLightButton.on('click', async () => {
const resp = await server.post('database/anonymize/light');
if (!resp.success) {
toastService.showError("Could not create anonymized database, check backend logs for details");
}
else {
toastService.showMessage(`Created lightly anonymized database in ${resp.anonymizedFilePath}`, 10000);
}
});
@@ -75,5 +103,16 @@ export default class AdvancedOptions {
toastService.showMessage("Consistency issues should be fixed.");
});
this.$checkIntegrityButton.on('click', async () => {
const {results} = await server.get('database/check-integrity');
if (results.length === 1 && results[0].integrity_check === "ok") {
toastService.showMessage("Integrity check succeeded - no problems found.");
}
else {
toastService.showMessage("Integrity check failed: " + JSON.stringify(results, null, 2), 15000);
}
});
}
}
+15 -15
View File
@@ -48,46 +48,46 @@ const TPL = `
export default class EtapiOptions {
constructor() {
$("#options-etapi").html(TPL);
$("#create-etapi-token").on("click", async () => {
const promptDialog = await import('../../dialogs/prompt.js');
const tokenName = await promptDialog.ask({
title: "New ETAPI token",
message: "Please enter new token's name",
defaultValue: "new token"
message: "Please enter new token's name",
defaultValue: "new token"
});
if (!tokenName.trim()) {
alert("Token name can't be empty");
return;
}
const {token} = await server.post('etapi-tokens', {tokenName});
await promptDialog.ask({
const {authToken} = await server.post('etapi-tokens', {tokenName});
await promptDialog.ask({
title: "ETAPI token created",
message: 'Copy the created token into clipboard. Trilium stores the token hashed and this is the last time you see it.',
defaultValue: token
message: 'Copy the created token into clipboard. Trilium stores the token hashed and this is the last time you see it.',
defaultValue: authToken
});
this.refreshTokens();
});
this.refreshTokens();
}
async refreshTokens() {
const $noTokensYet = $("#no-tokens-yet");
const $tokensTable = $("#tokens-table");
const tokens = await server.get('etapi-tokens');
$noTokensYet.toggle(tokens.length === 0);
$tokensTable.toggle(tokens.length > 0);
const $tokensTableBody = $tokensTable.find("tbody");
$tokensTableBody.empty();
for (const token of tokens) {
$tokensTableBody.append(
$("<tr>")
@@ -112,7 +112,7 @@ export default class EtapiOptions {
});
await server.patch(`etapi-tokens/${etapiTokenId}`, {name: tokenName});
this.refreshTokens();
}
+4 -3
View File
@@ -13,9 +13,9 @@ let shownCb;
export function ask({ title, message, defaultValue, shown }) {
shownCb = shown;
$("#prompt-title").text(title || "Prompt");
$question = $("<label>")
.prop("for", "prompt-dialog-answer")
.text(message);
@@ -51,7 +51,8 @@ $dialog.on("hidden.bs.modal", () => {
}
});
$form.on('submit', () => {
$form.on('submit', e => {
e.preventDefault();
resolve($answer.val());
$dialog.modal('hide');
+39 -32
View File
@@ -7,11 +7,14 @@ import treeService from "./tree.js";
import utils from "./utils.js";
import NoteContext from "./note_context.js";
import appContext from "./app_context.js";
import Mutex from "../utils/mutex.js";
export default class TabManager extends Component {
constructor() {
super();
this.mutex = new Mutex();
this.activeNtxId = null;
// elements are arrays of note contexts for each tab (one main context + subcontexts [splits])
@@ -292,51 +295,55 @@ export default class TabManager extends Component {
}
async removeNoteContext(ntxId) {
const noteContextToRemove = this.getNoteContextById(ntxId);
// removing note context is async process which can take some time, if users presses CTRL-W quickly, two
// close events could interleave which would then lead to attempting to activate already removed context.
await this.mutex.runExclusively(async () => {
const noteContextToRemove = this.getNoteContextById(ntxId);
if (noteContextToRemove.isMainContext()) {
// forbid removing last main note context
// this was previously allowed (was replaced with empty tab) but this proved to be prone to race conditions
const mainNoteContexts = this.getNoteContexts().filter(nc => nc.isMainContext());
if (noteContextToRemove.isMainContext()) {
// forbid removing last main note context
// this was previously allowed (was replaced with empty tab) but this proved to be prone to race conditions
const mainNoteContexts = this.getNoteContexts().filter(nc => nc.isMainContext());
if (mainNoteContexts.length === 1) {
mainNoteContexts[0].setEmpty();
return;
if (mainNoteContexts.length === 1) {
mainNoteContexts[0].setEmpty();
return;
}
}
}
// close dangling autocompletes after closing the tab
$(".aa-input").autocomplete("close");
// close dangling autocompletes after closing the tab
$(".aa-input").autocomplete("close");
const noteContextsToRemove = noteContextToRemove.getSubContexts();
const ntxIdsToRemove = noteContextsToRemove.map(nc => nc.ntxId);
const noteContextsToRemove = noteContextToRemove.getSubContexts();
const ntxIdsToRemove = noteContextsToRemove.map(nc => nc.ntxId);
await this.triggerEvent('beforeTabRemove', { ntxIds: ntxIdsToRemove });
await this.triggerEvent('beforeTabRemove', { ntxIds: ntxIdsToRemove });
if (!noteContextToRemove.isMainContext()) {
await this.activateNoteContext(noteContextToRemove.getMainContext().ntxId);
}
else if (this.mainNoteContexts.length <= 1) {
await this.openAndActivateEmptyTab();
}
else if (ntxIdsToRemove.includes(this.activeNtxId)) {
const idx = this.mainNoteContexts.findIndex(nc => nc.ntxId === noteContextToRemove.ntxId);
if (idx === this.mainNoteContexts.length - 1) {
await this.activatePreviousTabCommand();
if (!noteContextToRemove.isMainContext()) {
await this.activateNoteContext(noteContextToRemove.getMainContext().ntxId);
}
else {
await this.activateNextTabCommand();
else if (this.mainNoteContexts.length <= 1) {
await this.openAndActivateEmptyTab();
}
}
else if (ntxIdsToRemove.includes(this.activeNtxId)) {
const idx = this.mainNoteContexts.findIndex(nc => nc.ntxId === noteContextToRemove.ntxId);
this.children = this.children.filter(nc => !ntxIdsToRemove.includes(nc.ntxId));
if (idx === this.mainNoteContexts.length - 1) {
await this.activatePreviousTabCommand();
}
else {
await this.activateNextTabCommand();
}
}
this.recentlyClosedTabs.push(noteContextsToRemove);
this.children = this.children.filter(nc => !ntxIdsToRemove.includes(nc.ntxId));
this.triggerEvent('noteContextRemoved', {ntxIds: ntxIdsToRemove});
this.recentlyClosedTabs.push(noteContextsToRemove);
this.tabsUpdate.scheduleUpdate();
this.triggerEvent('noteContextRemoved', {ntxIds: ntxIdsToRemove});
this.tabsUpdate.scheduleUpdate();
});
}
tabReorderEvent({ntxIdsInOrder}) {
+1 -1
View File
@@ -65,7 +65,7 @@ async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logEr
if (!parents.length) {
if (logErrors) {
ws.logError(`No parents found for ${childNoteId} (${child.title}) for path ${notePath}`);
ws.logError(`No parents found for note ${childNoteId} (${child.title}) for path ${notePath}`);
}
return;
+28
View File
@@ -0,0 +1,28 @@
export default class Mutex {
constructor() {
this.current = Promise.resolve();
}
/** @returns {Promise} */
lock() {
let resolveFun;
const subPromise = new Promise(resolve => resolveFun = () => resolve());
// Caller gets a promise that resolves when the current outstanding lock resolves
const newPromise = this.current.then(() => resolveFun);
// Don't allow the next request until the new promise is done
this.current = subPromise;
// Return the new promise
return newPromise;
};
async runExclusively(cb) {
const unlock = await this.lock();
try {
await cb();
}
finally {
unlock();
}
}
}
@@ -215,6 +215,10 @@ const ATTR_HELP = {
"shareAlias": "define an alias using which the note will be available under https://your_trilium_host/share/[your_alias]",
"shareOmitDefaultCss": "default share page CSS will be omitted. Use when you make extensive styling changes.",
"shareRoot": "marks note which is served on /share root.",
"shareRaw": "note will be served in its raw format, without HTML wrapper",
"shareDisallowRobotIndexing": `will forbid robot indexing of this note via <code>X-Robots-Tag: noindex</code> header`,
"displayRelations": "comma delimited names of relations which should be displayed. All other ones will be hidden.",
"hideRelations": "comma delimited names of relations which should be hidden. All other ones will be displayed.",
},
"relation": {
"runOnNoteCreation": "executes when note is created on backend",
@@ -497,6 +497,16 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
await this.renderOwnedAttributes(attributes, false);
}
focus() {
this.$editor.trigger('focus');
this.textEditor.model.change( writer => {
const positionAt = writer.createPositionAt(this.textEditor.model.document.getRoot(), 'end');
writer.setSelection(positionAt);
} );
}
entitiesReloadedEvent({loadResults}) {
if (loadResults.getAttributes(this.componentId).find(attr => attributeService.isAffecting(attr, this.note))) {
this.refresh();
@@ -186,7 +186,9 @@ export default class RibbonContainer extends NoteContextAwareWidget {
const activeChild = this.getActiveRibbonWidget();
if (activeChild && (refreshActiveTab || !wasAlreadyActive)) {
activeChild.handleEvent('noteSwitched', {noteContext: this.noteContext, notePath: this.notePath});
activeChild.handleEvent('noteSwitched', {noteContext: this.noteContext, notePath: this.notePath}).then(() => {
activeChild.focus?.();
});
}
} else {
this.lastActiveComponentId = null;
+20 -2
View File
@@ -17,11 +17,12 @@ const TPL = `<div class="mermaid-widget">
.mermaid-render {
overflow: auto;
height: 100%;
text-align: center;
}
</style>
<div class="mermaid-error alert alert-warning">
<p><strong>The diagram could not displayed. See <a href="https://mermaid-js.github.io/mermaid/#/flowchart?id=graph">help and examples</a>.</strong></p>
<p><strong>The diagram could not be displayed. See <a href="https://mermaid-js.github.io/mermaid/#/flowchart?id=graph">help and examples</a>.</strong></p>
<p class="error-content"></p>
</div>
@@ -70,8 +71,25 @@ export default class MermaidWidget extends NoteContextAwareWidget {
this.$display.empty();
const libLoaded = libraryLoader.requireLibrary(libraryLoader.WHEEL_ZOOM);
try {
mermaid.mermaidAPI.render('mermaid-graph-' + idCounter++, content, content => this.$display.html(content));
const idNumber = idCounter++;
mermaid.mermaidAPI.render('mermaid-graph-' + idNumber, content, async content => {
this.$display.html(content);
await libLoaded;
this.$display.attr("id", 'mermaid-render-' + idNumber);
WZoom.create('#mermaid-render-' + idNumber, {
type: 'html',
maxScale: 10,
speed: 20,
zoomOnClick: false
});
});
this.$errorContainer.hide();
} catch (e) {
+1 -1
View File
@@ -275,7 +275,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
const label = attrs.find(attr =>
attr.type === 'label'
&& ['readOnly', 'autoReadOnlyDisabled', 'cssClass', 'displayRelations'].includes(attr.name)
&& ['readOnly', 'autoReadOnlyDisabled', 'cssClass', 'displayRelations', 'hideRelations'].includes(attr.name)
&& attributeService.isAffecting(attr, this.note));
const relation = attrs.find(attr =>
@@ -82,4 +82,8 @@ export default class OwnedAttributeListWidget extends NoteContextAwareWidget {
this.getTitle(this.note);
}
}
focus() {
this.attributeEditorWidget.focus();
}
}
@@ -310,6 +310,10 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
$attr.attr("data-attribute-id", result.attributeId);
}
focus() {
this.$widget.find(".promoted-attribute-input:first").focus();
}
entitiesReloadedEvent({loadResults}) {
if (loadResults.getAttributes(this.componentId).find(attr => attributeService.isAffecting(attr, this.note))) {
this.refresh();
@@ -23,6 +23,7 @@ import Limit from "../search_options/limit.js";
import DeleteNoteRevisionsSearchAction from "../search_actions/delete_note_revisions.js";
import Debug from "../search_options/debug.js";
import appContext from "../../services/app_context.js";
import MoveNoteSearchAction from "../search_actions/move_note.js";
const TPL = `
<div class="search-definition-widget">
@@ -127,10 +128,14 @@ const TPL = `
action
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="#" data-action-add="moveNote">
Move note</a>
<a class="dropdown-item" href="#" data-action-add="deleteNote">
Delete note</a>
<a class="dropdown-item" href="#" data-action-add="deleteNoteRevisions">
Delete note revisions</a>
<a class="dropdown-item" href="#" data-action-add="moveNote">
Delete note revisions</a>
<a class="dropdown-item" href="#" data-action-add="deleteLabel">
Delete label</a>
<a class="dropdown-item" href="#" data-action-add="deleteRelation">
@@ -193,6 +198,7 @@ const OPTION_CLASSES = [
const ACTION_CLASSES = {};
for (const clazz of [
MoveNoteSearchAction,
DeleteNoteSearchAction,
DeleteNoteRevisionsSearchAction,
DeleteLabelSearchAction,
@@ -0,0 +1,58 @@
import SpacedUpdate from "../../services/spaced_update.js";
import AbstractSearchAction from "./abstract_search_action.js";
import noteAutocompleteService from "../../services/note_autocomplete.js";
const TPL = `
<tr>
<td colspan="2">
<div style="display: flex; align-items: center">
<div style="margin-right: 10px;" class="text-nowrap">Move note</div>
<div style="margin-right: 10px;" class="text-nowrap">to</div>
<div class="input-group">
<input type="text" class="form-control target-parent-note" placeholder="target parent note"/>
</div>
</div>
</td>
<td class="button-column">
<div class="dropdown help-dropdown">
<span class="bx bx-help-circle icon-action" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div class="dropdown-menu dropdown-menu-right p-4">
<p>On all matched notes:</p>
<ul>
<li>move note to the new parent if note has only one parent (i.e. the old placement is removed and new placement into the new parent is created)</li>
<li>clone note to the new parent if note has multiple clones/placements (it's not clear which placement should be removed)</li>
<li>nothing will happen if note cannot be moved to the target note (i.e. this would create a tree cycle)</li>
</ul>
</div>
</div>
<span class="bx bx-x icon-action action-conf-del"></span>
</td>
</tr>`;
export default class MoveNoteSearchAction extends AbstractSearchAction {
static get actionName() { return "moveNote"; }
doRender() {
const $action = $(TPL);
const $targetParentNote = $action.find('.target-parent-note');
noteAutocompleteService.initNoteAutocomplete($targetParentNote);
$targetParentNote.setNote(this.actionDef.targetParentNoteId);
$targetParentNote.on('autocomplete:closed', () => spacedUpdate.scheduleUpdate());
const spacedUpdate = new SpacedUpdate(async () => {
await this.saveAction({
targetParentNoteId: $targetParentNote.getSelectedNoteId()
});
}, 1000)
$targetParentNote.on('input', () => spacedUpdate.scheduleUpdate());
return $action;
}
}
@@ -7,7 +7,7 @@ const TPL = `
<tr>
<td class="title-column">Search string:</td>
<td>
<input type="text" class="form-control search-string" placeholder="fulltext keywords, #tag = value ...">
<textarea class="form-control search-string" placeholder="fulltext keywords, #tag = value ..."></textarea>
</td>
<td class="button-column">
<div class="dropdown help-dropdown">
@@ -46,6 +46,9 @@ export default class SearchString extends AbstractSearchOption {
this.$searchString.on('input', () => this.spacedUpdate.scheduleUpdate());
utils.bindElShortcut(this.$searchString, 'return', async () => {
// this also in effect disallows new lines in query string.
// on one hand this makes sense since search string is a label
// on the other hand it could be nice for structuring long search string. It's probably a niche case though.
await this.spacedUpdate.updateNowIfNecessary();
this.triggerCommand('refreshResults');
@@ -49,6 +49,7 @@ class ImageTypeWidget extends TypeWidget {
libraryLoader.requireLibrary(libraryLoader.WHEEL_ZOOM).then(() => {
WZoom.create('#' + this.$imageView.attr("id"), {
maxScale: 10,
speed: 20,
zoomOnClick: false
});
});
+1
View File
@@ -1,2 +1,3 @@
User-agent: *
Allow: /share/
Disallow: /
@@ -91,6 +91,10 @@ body {
--ck-color-table-focused-cell-background: var(--more-accented-background-color);
--ck-color-labeled-field-label-background: var(--accented-background-color);
--ck-color-editable-blur-selection: var(--accented-background-color);
/* todo lists */
--ck-color-todo-list-checkmark-border: var(--main-border-color);
+1
View File
@@ -1,5 +1,6 @@
body {
font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif;
line-height: 1.5;
}
#layout {
+7 -23
View File
@@ -7,6 +7,8 @@ const treeService = require('../../services/tree');
const noteService = require('../../services/notes');
const becca = require('../../becca/becca');
const TaskContext = require('../../services/task_context');
const branchService = require("../../services/branches");
const log = require("../../services/log.js");
/**
* Code in this file deals with moving and cloning branches. Relationship between note and parent note is unique
@@ -23,29 +25,7 @@ function moveBranchToParent(req) {
return [400, `One or both branches ${branchId}, ${parentBranchId} have not been found`];
}
if (branchToMove.parentNoteId === parentBranch.noteId) {
return { success: true }; // no-op
}
const validationResult = treeService.validateParentChild(parentBranch.noteId, branchToMove.noteId, branchId);
if (!validationResult.success) {
return [200, validationResult];
}
const maxNotePos = sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [parentBranch.noteId]);
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 10;
// expanding so that the new placement of the branch is immediately visible
parentBranch.isExpanded = true;
parentBranch.save();
const newBranch = branchToMove.createClone(parentBranch.noteId, newNotePos);
newBranch.save();
branchToMove.markAsDeleted();
return { success: true };
return branchService.moveBranchToBranch(branchToMove, parentBranch, branchId);
}
function moveBranchBeforeNote(req) {
@@ -101,6 +81,8 @@ function moveBranchBeforeNote(req) {
// if sorting is not needed then still the ordering might have changed above manually
entityChangesService.addNoteReorderingEntityChange(parentNote.noteId);
log.info(`Moved note ${branchToMove.noteId}, branch ${branchId} before note ${beforeBranch.noteId}, branch ${beforeBranchId}`);
return { success: true };
}
@@ -150,6 +132,8 @@ function moveBranchAfterNote(req) {
// if sorting is not needed then still the ordering might have changed above manually
entityChangesService.addNoteReorderingEntityChange(parentNote.noteId);
log.info(`Moved note ${branchToMove.noteId}, branch ${branchId} after note ${afterNote.noteId}, branch ${afterBranchId}`);
return { success: true };
}
+4 -1
View File
@@ -138,12 +138,15 @@ function processContent(images, note, content) {
value: imageNote.noteId
}).save();
console.log(`Replacing ${imageId} with ${url}`);
log.info(`Replacing ${imageId} with ${url} in note ${note.noteId}`);
rewrittenContent = utils.replaceAll(rewrittenContent, imageId, url);
}
}
// fallback if parsing/downloading images fails for some reason on the extension side (
rewrittenContent = noteService.downloadImages(note.noteId, rewrittenContent);
return rewrittenContent;
}
+14 -3
View File
@@ -6,8 +6,8 @@ const backupService = require('../../services/backup');
const anonymizationService = require('../../services/anonymization');
const consistencyChecksService = require('../../services/consistency_checks');
async function anonymize() {
return await anonymizationService.createAnonymizedCopy();
async function anonymize(req) {
return await anonymizationService.createAnonymizedCopy(req.params.type);
}
async function backupDatabase() {
@@ -22,6 +22,16 @@ function vacuumDatabase() {
log.info("Database has been vacuumed.");
}
function checkIntegrity() {
const results = sql.getRows("PRAGMA integrity_check");
log.info("Integrity check result: " + JSON.stringify(results));
return {
results
};
}
function findAndFixConsistencyIssues() {
consistencyChecksService.runOnDemandChecks(true);
}
@@ -30,5 +40,6 @@ module.exports = {
backupDatabase,
vacuumDatabase,
findAndFixConsistencyIssues,
anonymize
anonymize,
checkIntegrity
};
+4 -6
View File
@@ -2,16 +2,14 @@ const etapiTokenService = require("../../services/etapi_tokens");
function getTokens() {
const tokens = etapiTokenService.getTokens();
tokens.sort((a, b) => a.utcDateCreated < b.utcDateCreated ? -1 : 1);
return tokens;
}
function createToken(req) {
return {
authToken: etapiTokenService.createToken(req.body.tokenName)
};
return etapiTokenService.createToken(req.body.tokenName);
}
function patchToken(req) {
@@ -27,4 +25,4 @@ module.exports = {
createToken,
patchToken,
deleteToken
};
};
+7 -2
View File
@@ -153,7 +153,10 @@ function getRelationMap(req) {
.split(",")
.map(token => token.trim());
console.log("displayRelations", displayRelations);
const hideRelationsVal = relationMapNote.getLabelValue('hideRelations');
const hideRelations = !hideRelationsVal ? [] : hideRelationsVal
.split(",")
.map(token => token.trim());
const foundNoteIds = sql.getColumn(`SELECT noteId FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
const notes = becca.getNotes(foundNoteIds);
@@ -163,7 +166,9 @@ function getRelationMap(req) {
resp.relations = resp.relations.concat(note.getRelations()
.filter(relation => !relation.isAutoLink() || displayRelations.includes(relation.name))
.filter(relation => displayRelations.length === 0 || displayRelations.includes(relation.name))
.filter(relation => displayRelations.length > 0
? displayRelations.includes(relation.name)
: !hideRelations.includes(relation.name))
.filter(relation => noteIds.includes(relation.value))
.map(relation => ({
attributeId: relation.attributeId,
+22
View File
@@ -6,6 +6,8 @@ const log = require('../../services/log');
const scriptService = require('../../services/script');
const searchService = require('../../services/search/services/search');
const noteRevisionService = require("../../services/note_revisions");
const branchService = require("../../services/branches");
const cloningService = require("../../services/cloning");
const {formatAttrForSearch} = require("../../services/attribute_formatter");
async function searchFromNoteInt(note) {
@@ -92,6 +94,26 @@ const ACTION_HANDLERS = {
setRelationTarget: (action, note) => {
note.setRelation(action.relationName, action.targetNoteId);
},
moveNote: (action, note) => {
const targetParentNote = becca.getNote(action.targetParentNoteId);
if (!targetParentNote) {
return;
}
let res;
if (note.getParentBranches().length > 1) {
res = cloningService.cloneNoteToNote(note.noteId, action.targetParentNoteId);
}
else {
res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId);
}
if (!res.success) {
log.info(`Moving/cloning note ${note.noteId} to ${action.targetParentNoteId} failed with error ${JSON.stringify(res)}`);
}
},
executeScript: (action, note) => {
if (!action.script || !action.script.trim()) {
log.info("Ignoring executeScript since the script is empty.")
+2
View File
@@ -29,6 +29,8 @@ function uploadImage(req) {
}
}
note.setLabel("sentFromSender");
return {
noteId: noteId
};
+6 -2
View File
@@ -42,6 +42,7 @@ const fontsRoute = require('./api/fonts');
const etapiTokensApiRoutes = require('./api/etapi_tokens');
const shareRoutes = require('../share/routes');
const etapiAuthRoutes = require('../etapi/auth');
const etapiAppInfoRoutes = require('../etapi/app_info');
const etapiAttributeRoutes = require('../etapi/attributes');
const etapiBranchRoutes = require('../etapi/branches');
const etapiNoteRoutes = require('../etapi/notes');
@@ -324,7 +325,7 @@ function register(app) {
apiRoute(GET, '/api/sql/schema', sqlRoute.getSchema);
apiRoute(POST, '/api/sql/execute/:noteId', sqlRoute.execute);
route(POST, '/api/database/anonymize', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.anonymize, apiResultHandler, false);
route(POST, '/api/database/anonymize/:type', [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);
@@ -334,6 +335,8 @@ function register(app) {
route(POST, '/api/database/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.findAndFixConsistencyIssues, apiResultHandler, false);
apiRoute(GET, '/api/database/check-integrity', databaseRoute.checkIntegrity);
apiRoute(POST, '/api/script/exec', scriptRoute.exec);
apiRoute(POST, '/api/script/run/:noteId', scriptRoute.run);
apiRoute(GET, '/api/script/startup', scriptRoute.getStartupBundles);
@@ -387,8 +390,9 @@ function register(app) {
apiRoute(DELETE, '/api/etapi-tokens/:etapiTokenId', etapiTokensApiRoutes.deleteToken);
shareRoutes.register(router);
etapiAuthRoutes.register(router);
etapiAppInfoRoutes.register(router);
etapiAttributeRoutes.register(router);
etapiBranchRoutes.register(router);
etapiNoteRoutes.register(router);
+26 -7
View File
@@ -5,7 +5,7 @@ const dateUtils = require("./date_utils");
const Database = require("better-sqlite3");
const sql = require("./sql");
function getAnonymizationScript() {
function getFullAnonymizationScript() {
// 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 builtinAttrNames = BUILTIN_ATTRIBUTES
@@ -13,14 +13,14 @@ function getAnonymizationScript() {
const anonymizeScript = `
UPDATE etapi_tokens SET tokenHash = 'API token hash value';
UPDATE notes SET title = 'title';
UPDATE notes SET title = 'title' WHERE title NOT IN ('root', 'hidden', 'share');
UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL;
UPDATE note_revisions SET title = 'title';
UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL;
UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN(${builtinAttrNames});
UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN (${builtinAttrNames});
UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL;
UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL AND prefix != 'recovered';
UPDATE options SET value = 'anonymized' WHERE name IN
('documentId', 'documentSecret', 'encryptedDataKey',
'passwordVerificationHash', 'passwordVerificationSalt',
@@ -33,18 +33,37 @@ VACUUM;
return anonymizeScript;
}
async function createAnonymizedCopy() {
function getLightAnonymizationScript() {
return `
UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL AND noteId NOT IN (
SELECT noteId FROM notes WHERE mime IN ('application/javascript;env=backend', 'application/javascript;env=frontend')
);
UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL AND noteRevisionId NOT IN (
SELECT noteRevisionId FROM note_revisions WHERE mime IN ('application/javascript;env=backend', 'application/javascript;env=frontend')
);
`;
}
async function createAnonymizedCopy(type) {
if (!['full', 'light'].includes(type)) {
throw new Error(`Unrecognized anonymization type '${type}'`);
}
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 anonymizedFile = `${dataDir.ANONYMIZED_DB_DIR}/anonymized-${type}-${dateUtils.getDateTimeForFile()}.db`;
await sql.copyDatabase(anonymizedFile);
const db = new Database(anonymizedFile);
db.exec(getAnonymizationScript());
const anonymizationScript = type === 'light'
? getLightAnonymizationScript()
: getFullAnonymizationScript();
db.exec(anonymizationScript);
db.close();
@@ -55,6 +74,6 @@ async function createAnonymizedCopy() {
}
module.exports = {
getAnonymizationScript,
getFullAnonymizationScript,
createAnonymizedCopy
}
+2 -1
View File
@@ -15,5 +15,6 @@ module.exports = {
buildDate: build.buildDate,
buildRevision: build.buildRevision,
dataDirectory: TRILIUM_DATA_DIR,
clipperProtocolVersion: CLIPPER_PROTOCOL_VERSION
clipperProtocolVersion: CLIPPER_PROTOCOL_VERSION,
utcDateTime: new Date().toISOString() // for timezone inference
};
+1 -6
View File
@@ -20,7 +20,7 @@ function getNotesWithLabel(name, value) {
// TODO: should be in search service
/** @returns {Note|null} */
function getNoteWithLabel(name, value) {
function getNoteWithLabel(name, value = undefined) {
// optimized version (~20 times faster) without using normal search, useful for e.g. finding date notes
const attrs = becca.findAttributes('label', name);
@@ -128,10 +128,6 @@ function isAttributeDangerous(type, name) {
);
}
function getBuiltinAttributeNames() {
return BUILTIN_ATTRIBUTES;
}
function sanitizeAttributeName(origName) {
let fixedName;
@@ -156,6 +152,5 @@ module.exports = {
getAttributeNames,
isAttributeType,
isAttributeDangerous,
getBuiltinAttributeNames,
sanitizeAttributeName
};
+6
View File
@@ -94,6 +94,12 @@ function reject(req, res, message) {
function checkCredentials(req, res, next) {
if (!sqlInit.isDbInitialized()) {
res.status(400).send('Database is not initialized yet.');
return;
}
if (!passwordService.isPasswordSet()) {
res.status(400).send('Password has not been set yet. Please set a password and repeat the action');
return;
}
const header = req.headers['trilium-cred'] || '';
+1 -1
View File
@@ -293,7 +293,7 @@ function BackendScriptApi(currentNote, apiParams) {
*
* @param message
*/
this.log = message => log.info(`Script "${currentNote.title}" (${currentNote.noteId}): ${message}`);
this.log = message => log.info(message);
/**
* Returns root note of the calendar.
+46
View File
@@ -0,0 +1,46 @@
const treeService = require("./tree.js");
const sql = require("./sql.js");
function moveBranchToNote(sourceBranch, targetParentNoteId) {
if (sourceBranch.parentNoteId === targetParentNoteId) {
return {success: true}; // no-op
}
const validationResult = treeService.validateParentChild(targetParentNoteId, sourceBranch.noteId, sourceBranch.branchId);
if (!validationResult.success) {
return [200, validationResult];
}
const maxNotePos = sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [targetParentNoteId]);
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 10;
const newBranch = sourceBranch.createClone(targetParentNoteId, newNotePos);
newBranch.save();
sourceBranch.markAsDeleted();
return {
success: true,
branch: newBranch
};
}
function moveBranchToBranch(sourceBranch, targetParentBranch) {
const res = moveBranchToNote(sourceBranch, targetParentBranch.noteId);
if (!res.success) {
return res;
}
// expanding so that the new placement of the branch is immediately visible
targetParentBranch.isExpanded = true;
targetParentBranch.save();
return res;
}
module.exports = {
moveBranchToBranch,
moveBranchToNote
};
+1 -1
View File
@@ -1 +1 @@
module.exports = { buildDate:"2022-01-19T20:14:40+01:00", buildRevision: "780b5205067623de0e3224bfeb571d6ccad9e6cd" };
module.exports = { buildDate:"2022-04-10T14:13:51+02:00", buildRevision: "a04becc4ec653e21c2c80aa9d9ef5b7c9a8e1aa8" };
+4
View File
@@ -46,6 +46,10 @@ module.exports = [
{ type: 'label', name: 'shareAlias' },
{ type: 'label', name: 'shareOmitDefaultCss' },
{ type: 'label', name: 'shareRoot' },
{ type: 'label', name: 'shareRaw' },
{ type: 'label', name: 'shareDisallowRobotIndexing' },
{ type: 'label', name: 'displayRelations' },
{ type: 'label', name: 'hideRelations' },
// relation names
{ type: 'relation', name: 'internalLink' },
+13
View File
@@ -9,6 +9,7 @@ const TaskContext = require("./task_context");
const utils = require('./utils');
const becca = require("../becca/becca");
const beccaService = require("../becca/becca_service");
const log = require("./log");
function cloneNoteToNote(noteId, parentNoteId, prefix) {
if (parentNoteId === 'share') {
@@ -34,6 +35,8 @@ function cloneNoteToNote(noteId, parentNoteId, prefix) {
isExpanded: 0
}).save();
log.info(`Cloned note ${noteId} to new parent note ${parentNoteId} with prefix ${prefix}`);
return {
success: true,
branchId: branch.branchId,
@@ -73,6 +76,8 @@ function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
prefix: prefix,
isExpanded: 0
}).save();
log.info(`Ensured note ${noteId} is in parent note ${parentNoteId} with prefix ${prefix}`);
}
function ensureNoteIsAbsentFromParent(noteId, parentNoteId) {
@@ -80,8 +85,14 @@ function ensureNoteIsAbsentFromParent(noteId, parentNoteId) {
const branch = becca.getBranch(branchId);
if (branch) {
if (branch.getNote().getParentBranches().length <= 1) {
throw new Error(`Cannot remove branch ${branch.branchId} between child ${noteId} and parent ${parentNoteId} because this would delete the note as well.`);
}
const deleteId = utils.randomString(10);
noteService.deleteBranch(branch, deleteId, new TaskContext());
log.info(`Ensured note ${noteId} is NOT in parent note ${parentNoteId}`);
}
}
@@ -121,6 +132,8 @@ function cloneNoteAfter(noteId, afterBranchId) {
isExpanded: 0
}).save();
log.info(`Cloned note ${noteId} into parent note ${afterNote.parentNoteId} after note ${afterNote.noteId}, branch ${afterBranchId}`);
return { success: true, branchId: branch.branchId };
}
+3 -3
View File
@@ -283,7 +283,7 @@ class ConsistencyChecks {
findLogicIssues() {
const noteTypesStr = noteTypes.map(nt => `'${nt}'`).join(", ");
this.findAndFixIssues(`
SELECT noteId, type
FROM notes
@@ -696,14 +696,14 @@ class ConsistencyChecks {
function getBlankContent(isProtected, type, mime) {
if (isProtected) {
return null; // this is wrong for protected non-erased notes but we cannot create a valid value without password
return null; // this is wrong for protected non-erased notes, but we cannot create a valid value without password
}
if (mime === 'application/json') {
return '{}';
}
return ''; // empty string might be wrong choice for some note types but it's a best guess
return ''; // empty string might be wrong choice for some note types, but it's the best guess
}
function logFix(message) {
+17 -18
View File
@@ -19,7 +19,7 @@ function createToken(tokenName) {
name: tokenName,
tokenHash
}).save();
return {
authToken: `${etapiToken.etapiTokenId}_${token}`
};
@@ -29,14 +29,14 @@ function parseAuthToken(auth) {
if (!auth) {
return null;
}
const chunks = auth.split("_");
if (chunks.length === 1) {
return { token: auth }; // legacy format without etapiTokenId
}
else if (chunks.length === 2) {
return {
return {
etapiTokenId: chunks[0],
token: chunks[1]
}
@@ -48,20 +48,20 @@ function parseAuthToken(auth) {
function isValidAuthHeader(auth) {
const parsed = parseAuthToken(auth);
if (!parsed) {
return false;
}
const authTokenHash = getTokenHash(parsed.token);
const authTokenHash = getTokenHash(parsed.token);
if (parsed.etapiTokenId) {
const etapiToken = becca.getEtapiToken(parsed.etapiTokenId);
if (!etapiToken) {
return false;
}
return etapiToken.tokenHash === authTokenHash;
}
else {
@@ -70,31 +70,30 @@ function isValidAuthHeader(auth) {
return true;
}
}
return false;
}
}
function renameToken(etapiTokenId, newName) {
const etapiToken = becca.getEtapiToken(etapiTokenId);
if (!etapiToken) {
throw new Error(`Token ${etapiTokenId} does not exist`);
}
etapiToken.name = newName;
etapiToken.save();
}
function deleteToken(etapiTokenId) {
const etapiToken = becca.getEtapiToken(etapiTokenId);
if (!etapiToken) {
return; // ok, already deleted
}
etapiToken.isDeleted = true;
etapiToken.save();
etapiToken.markAsDeletedSimple();
}
module.exports = {
@@ -104,4 +103,4 @@ module.exports = {
deleteToken,
parseAuthToken,
isValidAuthHeader
};
};
+7 -2
View File
@@ -13,6 +13,7 @@ const fs = require("fs");
const becca = require("../../becca/becca");
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
const archiver = require('archiver');
const log = require("../log");
/**
* @param {TaskContext} taskContext
@@ -54,7 +55,7 @@ function exportToZip(taskContext, branch, format, res) {
let existingExtension = path.extname(fileName).toLowerCase();
let newExtension;
if (fileName.length > 30) {
fileName = fileName.substr(0, 30);
}
@@ -254,7 +255,9 @@ ${content}
</html>`;
}
return html.prettyPrint(content, {indent_size: 2});
return content.length < 100000
? html.prettyPrint(content, {indent_size: 2})
: content;
}
else if (noteMeta.format === 'markdown') {
let markdownContent = mdService.toMarkdown(content);
@@ -274,6 +277,8 @@ ${content}
const notePaths = {};
function saveNote(noteMeta, filePathPrefix) {
log.info(`Exporting note ${noteMeta.noteId}`);
if (noteMeta.isClone) {
const targetUrl = getTargetUrl(noteMeta.noteId, noteMeta);
+16 -6
View File
@@ -17,8 +17,7 @@ async function processImage(uploadBuffer, originalName, shrinkImageSwitch) {
const compressImages = optionService.getOptionBool("compressImages");
const origImageFormat = getImageType(uploadBuffer);
if (origImageFormat && ["webp", "svg", "gif"].includes(origImageFormat.ext)) {
// JIMP does not support webp at the moment: https://github.com/oliver-moran/jimp/issues/144
if (!origImageFormat || !["jpg", "png"].includes(origImageFormat.ext)) {
shrinkImageSwitch = false;
}
else if (isAnimated(uploadBuffer)) {
@@ -26,9 +25,18 @@ async function processImage(uploadBuffer, originalName, shrinkImageSwitch) {
shrinkImageSwitch = false;
}
const finalImageBuffer = (compressImages && shrinkImageSwitch) ? await shrinkImage(uploadBuffer, originalName) : uploadBuffer;
let finalImageBuffer;
let imageFormat;
const imageFormat = getImageType(finalImageBuffer);
if (compressImages && shrinkImageSwitch) {
finalImageBuffer = await shrinkImage(uploadBuffer, originalName);
imageFormat = getImageType(finalImageBuffer);
} else {
finalImageBuffer = uploadBuffer;
imageFormat = origImageFormat || {
ext: 'dat'
};
}
return {
buffer: finalImageBuffer,
@@ -43,7 +51,9 @@ function getImageType(buffer) {
}
}
else {
return imageType(buffer) || "jpg"; // optimistic JPG default
return imageType(buffer) || {
ext: "jpg"
}; // optimistic JPG default
}
}
@@ -75,7 +85,7 @@ function updateImage(noteId, uploadBuffer, originalName) {
}
function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSwitch, trimFilename = false) {
log.info(`Saving image ${originalName}`);
log.info(`Saving image ${originalName} into parent ${parentNoteId}`);
if (trimFilename && originalName.length > 40) {
// https://github.com/zadam/trilium/issues/2307
+7
View File
@@ -2,6 +2,7 @@
const fs = require('fs');
const dataDir = require('./data_dir');
const cls = require('./cls');
if (!fs.existsSync(dataDir.LOG_DIR)) {
fs.mkdirSync(dataDir.LOG_DIR, 0o700);
@@ -49,6 +50,12 @@ function checkDate(millisSinceMidnight) {
}
function log(str) {
const bundleNoteId = cls.get("bundleNoteId");
if (bundleNoteId) {
str = `[Script ${bundleNoteId}] ${str}`;
}
let millisSinceMidnight = Date.now() - todaysMidnight.getTime();
millisSinceMidnight = checkDate(millisSinceMidnight);
+12 -2
View File
@@ -138,6 +138,8 @@ function createNewNote(params) {
triggerNoteTitleChanged(note);
triggerChildNoteCreated(note, parentNote);
log.info(`Created new note ${note.noteId}, branch ${branch.branchId} of type ${note.type}, mime ${note.mime}`);
return {
note,
branch
@@ -632,7 +634,10 @@ function undeleteBranch(branchId, deleteId, taskContext) {
taskContext.increaseProgressCount();
if (note.isDeleted && note.deleteId === deleteId) {
new Note(note).save();
// becca entity was already created as skeleton in "new Branch()" above
const noteEntity = becca.getNote(note.noteId);
noteEntity.updateFromRow(note);
noteEntity.save();
const attributes = sql.getRows(`
SELECT * FROM attributes
@@ -736,6 +741,8 @@ function eraseBranches(branchIdsToErase) {
sql.executeMany(`DELETE FROM branches WHERE branchId IN (???)`, branchIdsToErase);
setEntityChangesAsErased(sql.getManyRows(`SELECT * FROM entity_changes WHERE entityName = 'branches' AND entityId IN (???)`, branchIdsToErase));
log.info(`Erased branches: ${JSON.stringify(branchIdsToErase)}`);
}
function eraseAttributes(attributeIdsToErase) {
@@ -746,6 +753,8 @@ function eraseAttributes(attributeIdsToErase) {
sql.executeMany(`DELETE FROM attributes WHERE attributeId IN (???)`, attributeIdsToErase);
setEntityChangesAsErased(sql.getManyRows(`SELECT * FROM entity_changes WHERE entityName = 'attributes' AND entityId IN (???)`, attributeIdsToErase));
log.info(`Erased attributes: ${JSON.stringify(attributeIdsToErase)}`);
}
function eraseDeletedEntities(eraseEntitiesAfterTimeInSeconds = null) {
@@ -940,5 +949,6 @@ module.exports = {
triggerNoteTitleChanged,
eraseDeletedNotesNow,
eraseNotesWithDeleteId,
saveNoteRevision
saveNoteRevision,
downloadImages
};
+1
View File
@@ -33,6 +33,7 @@ function executeBundle(bundle, apiParams = {}) {
const originalComponentId = cls.get('componentId');
cls.set('componentId', 'script');
cls.set('bundleNoteId', bundle.note.noteId);
// last \r\n is necessary if script contains line comment on its last line
const script = "function() {\r\n" + bundle.script + "\r\n}";
-2
View File
@@ -222,8 +222,6 @@ function getShareRoot() {
function createMissingSpecialNotes() {
getSinglesNoteRoot();
getSqlConsoleRoot();
getSinglesNoteRoot();
getSinglesNoteRoot();
getGlobalNoteMap();
// share root is not automatically created since it's visible in the tree and many won't need it/use it
+13
View File
@@ -151,6 +151,14 @@ function setDbAsInitialized() {
}
}
function optimize() {
log.info("Optimizing database");
sql.execute("PRAGMA optimize");
log.info("Optimization finished.");
}
dbReady.then(() => {
if (config.General && config.General.noBackup === true) {
log.info("Disabling scheduled backups.");
@@ -162,6 +170,11 @@ dbReady.then(() => {
// kickoff first backup soon after start up
setTimeout(() => require('./backup').regularBackup(), 5 * 60 * 1000);
// optimize is usually inexpensive no-op so running it semi-frequently is not a big deal
setTimeout(() => optimize(), 60 * 60 * 1000);
setInterval(() => optimize(), 10 * 60 * 60 * 1000);
});
log.info("DB size: " + sql.getValue("SELECT page_count * page_size / 1000 as size FROM pragma_page_count(), pragma_page_size()") + " KB");
+7 -4
View File
@@ -131,7 +131,8 @@ async function pullChanges(syncContext) {
while (true) {
const lastSyncedPull = getLastSyncedPull();
const changesUri = `/api/sync/changed?instanceId=${instanceId}&lastEntityChangeId=${lastSyncedPull}`;
const logMarkerId = utils.randomString(10); // to easily pair sync events between client and server logs
const changesUri = `/api/sync/changed?instanceId=${instanceId}&lastEntityChangeId=${lastSyncedPull}&logMarkerId=${logMarkerId}`;
const startDate = Date.now();
@@ -168,7 +169,7 @@ async function pullChanges(syncContext) {
} else {
const sizeInKb = Math.round(JSON.stringify(resp).length / 1024);
log.info(`Pulled ${entityChanges.length} changes in ${sizeInKb} KB, starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${outstandingPullCount} outstanding pulls`);
log.info(`Sync ${logMarkerId}: Pulled ${entityChanges.length} changes in ${sizeInKb} KB, starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${outstandingPullCount} outstanding pulls`);
}
}
@@ -211,14 +212,16 @@ async function pushChanges(syncContext) {
const entityChangesRecords = getEntityChangeRecords(filteredEntityChanges);
const startDate = new Date();
await syncRequest(syncContext, 'PUT', '/api/sync/update', {
const logMarkerId = utils.randomString(10); // to easily pair sync events between client and server logs
await syncRequest(syncContext, 'PUT', `/api/sync/update?logMarkerId=${logMarkerId}`, {
entities: entityChangesRecords,
instanceId
});
ws.syncPushInProgress();
log.info(`Pushing ${entityChangesRecords.length} sync changes in ` + (Date.now() - startDate.getTime()) + "ms");
log.info(`Sync ${logMarkerId}: Pushing ${entityChangesRecords.length} sync changes in ` + (Date.now() - startDate.getTime()) + "ms");
lastSyncedPush = entityChangesRecords[entityChangesRecords.length - 1].entityChange.id;
+6 -3
View File
@@ -223,12 +223,15 @@ function setNoteToParent(noteId, prefix, parentNoteId) {
if (branch) {
if (!parentNoteId) {
log.info(`Removing note ${noteId} from parent ${parentNoteId}`);
branch.markAsDeleted();
}
else {
branch.parentNoteId = parentNoteId;
branch.prefix = prefix;
branch.save();
const newBranch = branch.createClone(parentNoteId);
newBranch.save();
branch.markAsDeleted();
}
}
else if (parentNoteId) {
+37 -13
View File
@@ -20,23 +20,39 @@ function getSharedSubTreeRoot(note) {
return getSharedSubTreeRoot(parentNote);
}
function addNoIndexHeader(note, res) {
if (note.hasLabel('shareDisallowRobotIndexing')) {
res.setHeader('X-Robots-Tag', 'noindex');
}
}
function register(router) {
function renderNote(note, res) {
if (note) {
const {header, content, isEmpty} = contentRenderer.getContent(note);
const subRoot = getSharedSubTreeRoot(note);
res.render("share/page", {
note,
header,
content,
isEmpty,
subRoot
});
} else {
if (!note) {
res.status(404).render("share/404");
return;
}
addNoIndexHeader(note, res);
if (note.hasLabel('shareRaw') || ['image', 'file'].includes(note.type)) {
res.setHeader('Content-Type', note.mime);
res.send(note.getContent());
return;
}
const {header, content, isEmpty} = contentRenderer.getContent(note);
const subRoot = getSharedSubTreeRoot(note);
res.render("share/page", {
note,
header,
content,
isEmpty,
subRoot
});
}
router.get(['/share', '/share/'], (req, res, next) => {
@@ -63,6 +79,8 @@ function register(router) {
return res.status(404).send(`Note ${noteId} not found`);
}
addNoIndexHeader(note, res);
res.json(note.getPojoWithAttributes());
});
@@ -74,6 +92,8 @@ function register(router) {
return res.status(404).send(`Note ${noteId} not found`);
}
addNoIndexHeader(note, res);
const utils = require("../services/utils");
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
@@ -96,6 +116,8 @@ function register(router) {
return res.status(400).send("Requested note is not an image");
}
addNoIndexHeader(image, res);
res.set('Content-Type', image.mime);
res.send(image.getContent());
@@ -110,6 +132,8 @@ function register(router) {
return res.status(404).send(`Note ${noteId} not found`);
}
addNoIndexHeader(note, res);
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader('Content-Type', note.mime);
+2 -2
View File
@@ -34,9 +34,9 @@ class Attribute extends AbstractEntity {
const linkedChildNote = this.note.getChildNotes().find(childNote => childNote.noteId === this.value);
if (linkedChildNote) {
this.note.children = this.note.children.filter(childNote => childNote.noteId !== this.value);
const branch = this.shaca.getBranchFromChildAndParent(this.noteId, linkedChildNote.noteId);
linkedChildNote.parents = linkedChildNote.parents.filter(parentNote => parentNote.noteId !== this.noteId);
branch.isHidden = true;
}
}
+2
View File
@@ -16,6 +16,8 @@ class Branch extends AbstractEntity {
this.prefix = prefix;
/** @param {boolean} */
this.isExpanded = !!isExpanded;
/** @param {boolean} */
this.isHidden = false;
const childNote = this.childNote;
const parentNote = this.parentNote;
+11
View File
@@ -58,10 +58,21 @@ class Note extends AbstractEntity {
return this.children;
}
getVisibleChildNotes() {
return this.getChildBranches()
.filter(branch => !branch.isHidden)
.map(branch => branch.getNote())
.filter(childNote => !childNote.hasLabel('shareHiddenFromTree') && !childNote.isProtected);
}
hasChildren() {
return this.children && this.children.length > 0;
}
hasVisibleChildren() {
return this.getVisibleChildNotes().length > 0;
}
getChildBranches() {
return this.children.map(childNote => this.shaca.getBranchFromChildAndParent(childNote.noteId, this.noteId));
}
+1 -1
View File
@@ -1,5 +1,5 @@
<div id="prompt-dialog" class="modal mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form id="prompt-dialog-form">
<div class="modal-header">
+6 -3
View File
@@ -21,6 +21,9 @@
<% for (const jsRelation of note.getRelations("shareJs")) { %>
<script type="module" src="api/notes/<%= jsRelation.value %>/download"></script>
<% } %>
<% if (note.hasLabel('shareDisallowRobotIndexing')) { %>
<meta name="robots" content="noindex,follow" />
<% } %>
<%- header %>
<title><%= note.title %></title>
</head>
@@ -46,7 +49,7 @@
</div>
<% } %>
<% if (note.hasChildren()) { %>
<% if (note.hasVisibleChildren()) { %>
<nav id="childLinks" class="<% if (isEmpty) { %>grid<% } else { %>list<% } %>">
<% if (!isEmpty) { %>
<hr>
@@ -54,7 +57,7 @@
<% } %>
<ul>
<% for (const childNote of note.getChildNotes()) { %>
<% for (const childNote of note.getVisibleChildNotes()) { %>
<li>
<a href="<%= childNote.shareId %>"
class="type-<%= childNote.type %>"><%= childNote.title %></a>
@@ -67,7 +70,7 @@
<% } %>
</div>
<% if (subRoot.hasChildren()) { %>
<% if (subRoot.hasVisibleChildren()) { %>
<button id="toggleMenuButton"></button>
<nav id="menu">
+1 -3
View File
@@ -8,12 +8,10 @@
<% if (note.hasChildren()) { %>
<ul>
<% note.getChildNotes().forEach(function (childNote) { %>
<% if (!childNote.hasLabel("shareHiddenFromTree")) { %>
<% note.getVisibleChildNotes().forEach(function (childNote) { %>
<li>
<%- include('tree_item', {note: childNote}) %>
</li>
<% } %>
<% }) %>
</ul>
<% } %>
+7
View File
@@ -0,0 +1,7 @@
GET {{triliumHost}}/etapi/app-info
Authorization: {{authToken}}
> {%
client.assert(response.status === 200);
client.assert(response.body == "Hi there!");
%}
+7 -1
View File
@@ -96,8 +96,14 @@ POST {{triliumHost}}/etapi/create-note
> {% client.assert(response.status === 401); %}
###
GET {{triliumHost}}/etapi/app-info
> {% client.assert(response.status === 401); %}
### Fake URL will get a 404 even without token
GET {{triliumHost}}/etapi/zzzzzz
> {% client.assert(response.status === 404); %}
> {% client.assert(response.status === 404); %}
+25
View File
@@ -0,0 +1,25 @@
POST {{triliumHost}}/etapi/create-note
Authorization: {{authToken}}
Content-Type: application/json
{
"parentNoteId": "root",
"title": "Hello",
"type": "image",
"mime": "image/png",
"content": ""
}
> {% client.global.set("createdNoteId", response.body.note.noteId); %}
###
PUT {{triliumHost}}/etapi/notes/{{createdNoteId}}/content
Authorization: {{authToken}}
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary
< ../images/icon-color.png
> {% client.assert(response.status === 204); %}
+2
View File
@@ -20,6 +20,8 @@ Content-Type: text/plain
Changed content
> {% client.assert(response.status === 200); %}
###
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content