Merge branch 'beta'

# Conflicts:
#	package-lock.json
This commit is contained in:
zadam 2023-10-07 20:51:26 +03:00
commit 99d0f22403
48 changed files with 591 additions and 328 deletions

0
bin/create-anonymization-script.js Normal file → Executable file
View File

View File

@ -1,165 +1,47 @@
UPDATE etapi_tokens SET tokenHash = 'API token hash value';
UPDATE notes SET title = 'title' WHERE noteId != 'root' AND noteId NOT LIKE '\_%' ESCAPE '\';
UPDATE notes SET title = 'title' WHERE title NOT IN ('root', '_hidden', '_share');
UPDATE blobs SET content = 'text' WHERE content IS NOT NULL;
UPDATE revisions SET title = 'title';
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',
'cssClass',
'iconClass',
'keyboardShortcut',
'run',
'runOnInstance',
'runAtHour',
'customRequestHandler',
'customResourceProvider',
'widget',
'noteInfoWidgetDisabled',
'linkMapWidgetDisabled',
'revisionsWidgetDisabled',
'whatLinksHereWidgetDisabled',
'similarNotesWidgetDisabled',
'workspace',
'workspaceIconClass',
'workspaceTabBackgroundColor',
'searchHome',
'workspaceInbox',
'workspaceSearchHome',
'sqlConsoleHome',
'datePattern',
'pageSize',
'viewType',
'mapRootNoteId',
'bookmarkFolder',
'sorted',
'top',
'fullContentWidth',
'shareHiddenFromTree',
'shareAlias',
'shareOmitDefaultCss',
'shareRoot',
'internalLink',
'imageLink',
'relationMapLink',
'includeMapLink',
'runOnNoteCreation',
'runOnNoteTitleChange',
'runOnNoteContentChange',
'runOnNoteChange',
'runOnChildNoteCreation',
'runOnAttributeCreation',
'runOnAttributeChange',
'template',
'inherit',
'widget',
'renderNote',
'shareCss',
'shareJs',
'shareFavicon',
'executeButton',
'keepCurrentHoisting',
'color',
'toc',
'excludeFromNoteMap',
'docName',
'launcherType',
'builtinWidget',
'baseSize',
'growthFactor'
);
UPDATE attributes SET name = 'name'
AND name NOT IN ('inbox',
'disableVersioning',
'calendarRoot',
'archived',
'excludeFromExport',
'disableInclusion',
'appCss',
'appTheme',
'hidePromotedAttributes',
'readOnly',
'autoReadOnlyDisabled',
'cssClass',
'iconClass',
'keyboardShortcut',
'run',
'runOnInstance',
'runAtHour',
'customRequestHandler',
'customResourceProvider',
'widget',
'noteInfoWidgetDisabled',
'linkMapWidgetDisabled',
'revisionsWidgetDisabled',
'whatLinksHereWidgetDisabled',
'similarNotesWidgetDisabled',
'workspace',
'workspaceIconClass',
'workspaceTabBackgroundColor',
'searchHome',
'workspaceInbox',
'workspaceSearchHome',
'sqlConsoleHome',
'datePattern',
'pageSize',
'viewType',
'mapRootNoteId',
'bookmarkFolder',
'sorted',
'top',
'fullContentWidth',
'shareHiddenFromTree',
'shareAlias',
'shareOmitDefaultCss',
'shareRoot',
'internalLink',
'imageLink',
'relationMapLink',
'includeMapLink',
'runOnNoteCreation',
'runOnNoteTitleChange',
'runOnNoteContentChange',
'runOnNoteChange',
'runOnChildNoteCreation',
'runOnAttributeCreation',
'runOnAttributeChange',
'template',
'inherit',
'widget',
'renderNote',
'shareCss',
'shareJs',
'shareFavicon',
'executeButton',
'keepCurrentHoisting',
'color',
'toc',
'excludeFromNoteMap',
'docName',
'launcherType',
'builtinWidget',
'baseSize',
'growthFactor'
);
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', 'cssClass', 'iconClass',
'keyboardShortcut', 'run', 'runOnInstance', 'runAtHour', 'customRequestHandler', 'customResourceProvider',
'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'revisionsWidgetDisabled',
'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass',
'workspaceTabBackgroundColor', 'workspaceCalendarRoot', 'workspaceTemplate', 'searchHome', 'workspaceInbox',
'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId',
'bookmarkFolder', 'sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale', 'top',
'fullContentWidth', 'shareHiddenFromTree', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription',
'shareRaw', 'shareDisallowRobotIndexing', 'shareIndex', 'displayRelations', 'hideRelations', 'titleTemplate',
'template', 'toc', 'color', 'keepCurrentHoisting', 'executeButton', 'executeDescription', 'newNotesOnTop',
'clipperInbox', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation',
'runOnNoteTitleChange', 'runOnNoteChange', 'runOnNoteContentChange', 'runOnNoteDeletion', 'runOnBranchCreation',
'runOnBranchDeletion', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template',
'inherit', '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', 'cssClass', 'iconClass',
'keyboardShortcut', 'run', 'runOnInstance', 'runAtHour', 'customRequestHandler', 'customResourceProvider',
'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'revisionsWidgetDisabled',
'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass',
'workspaceTabBackgroundColor', 'workspaceCalendarRoot', 'workspaceTemplate', 'searchHome', 'workspaceInbox',
'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId',
'bookmarkFolder', 'sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale', 'top',
'fullContentWidth', 'shareHiddenFromTree', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription',
'shareRaw', 'shareDisallowRobotIndexing', 'shareIndex', 'displayRelations', 'hideRelations', 'titleTemplate',
'template', 'toc', 'color', 'keepCurrentHoisting', 'executeButton', 'executeDescription', 'newNotesOnTop',
'clipperInbox', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation',
'runOnNoteTitleChange', 'runOnNoteChange', 'runOnNoteContentChange', 'runOnNoteDeletion', 'runOnBranchCreation',
'runOnBranchDeletion', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template',
'inherit', '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',
'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy')
AND value != '';
('documentId', 'documentSecret', 'encryptedDataKey',
'passwordVerificationHash', 'passwordVerificationSalt',
'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy')
AND value != '';
VACUUM;

View File

@ -0,0 +1 @@
UPDATE attributes SET value = 'contentAndAttachmentsAndRevisionsSize' WHERE name = 'orderBy' AND value = 'noteSize';

View File

@ -1,3 +1,5 @@
# Running `docker-compose up` will create/use the "trilium-data" directory in the user home
# Run `TRILIUM_DATA_DIR=/path/of/your/choice docker-compose up` to set a different directory
version: '2.1'
services:
trilium:
@ -8,7 +10,7 @@ services:
ports:
- "8080:8080"
volumes:
- trilium:/home/node/trilium-data
- ${TRILIUM_DATA_DIR:-~/trilium-data}:/home/node/trilium-data
volumes:
trilium:

View File

@ -46,7 +46,7 @@
const errors = new eslint().verify(text, {
root: true,
parserOptions: {
ecmaVersion: 2022
ecmaVersion: "latest"
},
extends: ['eslint:recommended', 'airbnb-base'],
env: {

View File

@ -24,8 +24,10 @@
function position(e) {
if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position);
tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + "px";
tt.style.left = (e.clientX + 5) + "px";
var top = Math.max(0, e.clientY - tt.offsetHeight - 5);
var left = Math.max(0, Math.min(e.clientX + 5, tt.ownerDocument.defaultView.innerWidth - tt.offsetWidth));
tt.style.top = top + "px"
tt.style.left = left + "px";
}
CodeMirror.on(document, "mousemove", position);
position(e);
@ -199,10 +201,6 @@
var anns = annotations[line];
if (!anns) continue;
// filter out duplicate messages
var message = [];
anns = anns.filter(function(item) { return message.indexOf(item.message) > -1 ? false : message.push(item.message) });
var maxSeverity = null;
var tipLabel = state.hasGutter && document.createDocumentFragment();
@ -220,9 +218,8 @@
__annotation: ann
}));
}
// use original annotations[line] to show multiple messages
if (state.hasGutter)
cm.setGutterMarker(line, GUTTER_ID, makeMarker(cm, tipLabel, maxSeverity, annotations[line].length > 1,
cm.setGutterMarker(line, GUTTER_ID, makeMarker(cm, tipLabel, maxSeverity, anns.length > 1,
options.tooltips));
if (options.highlightLines)

View File

@ -0,0 +1,136 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.multiplexingMode = function(outer /*, others */) {
// Others should be {open, close, mode [, delimStyle] [, innerStyle] [, parseDelimiters]} objects
var others = Array.prototype.slice.call(arguments, 1);
function indexOf(string, pattern, from, returnEnd) {
if (typeof pattern == "string") {
var found = string.indexOf(pattern, from);
return returnEnd && found > -1 ? found + pattern.length : found;
}
var m = pattern.exec(from ? string.slice(from) : string);
return m ? m.index + from + (returnEnd ? m[0].length : 0) : -1;
}
return {
startState: function() {
return {
outer: CodeMirror.startState(outer),
innerActive: null,
inner: null,
startingInner: false
};
},
copyState: function(state) {
return {
outer: CodeMirror.copyState(outer, state.outer),
innerActive: state.innerActive,
inner: state.innerActive && CodeMirror.copyState(state.innerActive.mode, state.inner),
startingInner: state.startingInner
};
},
token: function(stream, state) {
if (!state.innerActive) {
var cutOff = Infinity, oldContent = stream.string;
for (var i = 0; i < others.length; ++i) {
var other = others[i];
var found = indexOf(oldContent, other.open, stream.pos);
if (found == stream.pos) {
if (!other.parseDelimiters) stream.match(other.open);
state.startingInner = !!other.parseDelimiters
state.innerActive = other;
// Get the outer indent, making sure to handle CodeMirror.Pass
var outerIndent = 0;
if (outer.indent) {
var possibleOuterIndent = outer.indent(state.outer, "", "");
if (possibleOuterIndent !== CodeMirror.Pass) outerIndent = possibleOuterIndent;
}
state.inner = CodeMirror.startState(other.mode, outerIndent);
return other.delimStyle && (other.delimStyle + " " + other.delimStyle + "-open");
} else if (found != -1 && found < cutOff) {
cutOff = found;
}
}
if (cutOff != Infinity) stream.string = oldContent.slice(0, cutOff);
var outerToken = outer.token(stream, state.outer);
if (cutOff != Infinity) stream.string = oldContent;
return outerToken;
} else {
var curInner = state.innerActive, oldContent = stream.string;
if (!curInner.close && stream.sol()) {
state.innerActive = state.inner = null;
return this.token(stream, state);
}
var found = curInner.close && !state.startingInner ?
indexOf(oldContent, curInner.close, stream.pos, curInner.parseDelimiters) : -1;
if (found == stream.pos && !curInner.parseDelimiters) {
stream.match(curInner.close);
state.innerActive = state.inner = null;
return curInner.delimStyle && (curInner.delimStyle + " " + curInner.delimStyle + "-close");
}
if (found > -1) stream.string = oldContent.slice(0, found);
var innerToken = curInner.mode.token(stream, state.inner);
if (found > -1) stream.string = oldContent;
else if (stream.pos > stream.start) state.startingInner = false
if (found == stream.pos && curInner.parseDelimiters)
state.innerActive = state.inner = null;
if (curInner.innerStyle) {
if (innerToken) innerToken = innerToken + " " + curInner.innerStyle;
else innerToken = curInner.innerStyle;
}
return innerToken;
}
},
indent: function(state, textAfter, line) {
var mode = state.innerActive ? state.innerActive.mode : outer;
if (!mode.indent) return CodeMirror.Pass;
return mode.indent(state.innerActive ? state.inner : state.outer, textAfter, line);
},
blankLine: function(state) {
var mode = state.innerActive ? state.innerActive.mode : outer;
if (mode.blankLine) {
mode.blankLine(state.innerActive ? state.inner : state.outer);
}
if (!state.innerActive) {
for (var i = 0; i < others.length; ++i) {
var other = others[i];
if (other.open === "\n") {
state.innerActive = other;
state.inner = CodeMirror.startState(other.mode, mode.indent ? mode.indent(state.outer, "", "") : 0);
}
}
} else if (state.innerActive.close === "\n") {
state.innerActive = state.inner = null;
}
},
electricChars: outer.electricChars,
innerMode: function(state) {
return state.inner ? {state: state.inner, mode: state.innerActive.mode} : {state: state.outer, mode: outer};
}
};
};
});

View File

@ -0,0 +1,90 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
// Utility function that allows modes to be combined. The mode given
// as the base argument takes care of most of the normal mode
// functionality, but a second (typically simple) mode is used, which
// can override the style of text. Both modes get to parse all of the
// text, but when both assign a non-null style to a piece of code, the
// overlay wins, unless the combine argument was true and not overridden,
// or state.overlay.combineTokens was true, in which case the styles are
// combined.
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.overlayMode = function(base, overlay, combine) {
return {
startState: function() {
return {
base: CodeMirror.startState(base),
overlay: CodeMirror.startState(overlay),
basePos: 0, baseCur: null,
overlayPos: 0, overlayCur: null,
streamSeen: null
};
},
copyState: function(state) {
return {
base: CodeMirror.copyState(base, state.base),
overlay: CodeMirror.copyState(overlay, state.overlay),
basePos: state.basePos, baseCur: null,
overlayPos: state.overlayPos, overlayCur: null
};
},
token: function(stream, state) {
if (stream != state.streamSeen ||
Math.min(state.basePos, state.overlayPos) < stream.start) {
state.streamSeen = stream;
state.basePos = state.overlayPos = stream.start;
}
if (stream.start == state.basePos) {
state.baseCur = base.token(stream, state.base);
state.basePos = stream.pos;
}
if (stream.start == state.overlayPos) {
stream.pos = stream.start;
state.overlayCur = overlay.token(stream, state.overlay);
state.overlayPos = stream.pos;
}
stream.pos = Math.min(state.basePos, state.overlayPos);
// state.overlay.combineTokens always takes precedence over combine,
// unless set to null
if (state.overlayCur == null) return state.baseCur;
else if (state.baseCur != null &&
state.overlay.combineTokens ||
combine && state.overlay.combineTokens == null)
return state.baseCur + " " + state.overlayCur;
else return state.overlayCur;
},
indent: base.indent && function(state, textAfter, line) {
return base.indent(state.base, textAfter, line);
},
electricChars: base.electricChars,
innerMode: function(state) { return {state: state.base, mode: base}; },
blankLine: function(state) {
var baseToken, overlayToken;
if (base.blankLine) baseToken = base.blankLine(state.base);
if (overlay.blankLine) overlayToken = overlay.blankLine(state.overlay);
return overlayToken == null ?
baseToken :
(combine && baseToken != null ? baseToken + " " + overlayToken : overlayToken);
}
};
};
});

View File

@ -8259,8 +8259,8 @@
}
function disableBrowserMagic(field, spellcheck, autocorrect, autocapitalize) {
field.setAttribute("autocorrect", autocorrect ? "" : "off");
field.setAttribute("autocapitalize", autocapitalize ? "" : "off");
field.setAttribute("autocorrect", autocorrect ? "on" : "off");
field.setAttribute("autocapitalize", autocapitalize ? "on" : "off");
field.setAttribute("spellcheck", !!spellcheck);
}
@ -8275,7 +8275,6 @@
else { te.setAttribute("wrap", "off"); }
// If border: 0; -- iOS fails to open keyboard (issue #1287)
if (ios) { te.style.border = "1px solid black"; }
disableBrowserMagic(te);
return div
}
@ -8897,6 +8896,7 @@
}
// Old-fashioned briefly-focus-a-textarea hack
var kludge = hiddenTextarea(), te = kludge.firstChild;
disableBrowserMagic(te);
cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild);
te.value = lastCopied.text.join("\n");
var hadFocus = activeElt(div.ownerDocument);
@ -9461,6 +9461,8 @@
// The semihidden textarea that is focused when the editor is
// focused, and receives input.
this.textarea = this.wrapper.firstChild;
var opts = this.cm.options;
disableBrowserMagic(this.textarea, opts.spellcheck, opts.autocorrect, opts.autocapitalize);
};
TextareaInput.prototype.screenReaderLabelChanged = function (label) {
@ -9865,7 +9867,7 @@
addLegacyProps(CodeMirror);
CodeMirror.version = "5.65.9";
CodeMirror.version = "5.65.15";
return CodeMirror;

View File

@ -218,7 +218,8 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
},
indent: function(state, textAfter) {
if (state.tokenize != tokenBase && state.tokenize != null || state.typeAtEndOfLine) return CodeMirror.Pass;
if (state.tokenize != tokenBase && state.tokenize != null || state.typeAtEndOfLine && isTopScope(state.context))
return CodeMirror.Pass;
var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
var closing = firstChar == ctx.type;
if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev;
@ -512,8 +513,8 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
name: "clike",
keywords: words("abstract as async await base break case catch checked class const continue" +
" default delegate do else enum event explicit extern finally fixed for" +
" foreach goto if implicit in interface internal is lock namespace new" +
" operator out override params private protected public readonly ref return sealed" +
" foreach goto if implicit in init interface internal is lock namespace new" +
" operator out override params private protected public readonly record ref required return sealed" +
" sizeof stackalloc static struct switch this throw try typeof unchecked" +
" unsafe using virtual void volatile while add alias ascending descending dynamic from get" +
" global group into join let orderby partial remove select set value var yield"),
@ -522,7 +523,7 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
" UInt64 bool byte char decimal double short int long object" +
" sbyte float string ushort uint ulong"),
blockKeywords: words("catch class do else finally for foreach if struct switch try while"),
defKeywords: words("class interface namespace struct var"),
defKeywords: words("class interface namespace record struct var"),
typeFirstDefinitions: true,
atoms: words("true false null"),
hooks: {
@ -613,6 +614,7 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
return state.tokenize(stream, state);
},
"'": function(stream) {
if (stream.match(/^(\\[^'\s]+|[^\\'])'/)) return "string-2"
stream.eatWhile(/[\w\$_\xa1-\uffff]/);
return "atom";
},

View File

@ -12,10 +12,10 @@
"use strict";
var keywords = ("this super static final const abstract class extends external factory " +
"implements mixin get native set typedef with enum throw rethrow " +
"assert break case continue default in return new deferred async await covariant " +
"try catch finally do else for if switch while import library export " +
"part of show hide is as extension on yield late required").split(" ");
"implements mixin get native set typedef with enum throw rethrow assert break case " +
"continue default in return new deferred async await covariant try catch finally " +
"do else for if switch while import library export part of show hide is as extension " +
"on yield late required sealed base interface when inline").split(" ");
var blockKeywords = "try catch finally do else for if switch while".split(" ");
var atoms = "true false null".split(" ");
var builtins = "void bool num int double dynamic var String Null Never".split(" ");

View File

@ -779,7 +779,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == "async" ||
(type == "variable" &&
(value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) &&
cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false))) {
cx.stream.match(/^\s+#?[\w$\xa1-\uffff]/, false))) {
cx.marked = "keyword";
return cont(classBody);
}

View File

@ -24,7 +24,7 @@ CodeMirror.defineSimpleMode("nsis",{
{ regex: /`(?:[^\\`]|\\.)*`?/, token: "string" },
// Compile Time Commands
{regex: /^\s*(?:\!(addincludedir|addplugindir|appendfile|cd|define|delfile|echo|error|execute|finalize|getdllversion|gettlbversion|include|insertmacro|macro|macroend|makensis|packhdr|pragma|searchparse|searchreplace|system|tempfile|undef|uninstfinalize|verbose|warning))\b/i, token: "keyword"},
{regex: /^\s*(?:\!(addincludedir|addplugindir|appendfile|assert|cd|define|delfile|echo|error|execute|finalize|getdllversion|gettlbversion|include|insertmacro|macro|macroend|makensis|packhdr|pragma|searchparse|searchreplace|system|tempfile|undef|uninstfinalize|verbose|warning))\b/i, token: "keyword"},
// Conditional Compilation
{regex: /^\s*(?:\!(if(?:n?def)?|ifmacron?def|macro))\b/i, token: "keyword", indent: true},

View File

@ -31,8 +31,6 @@ CodeMirror.defineMode("pegjs", function (config) {
};
},
token: function (stream, state) {
if (stream)
//check for state changes
if (!state.inString && !state.inComment && ((stream.peek() == '"') || (stream.peek() == "'"))) {
state.stringType = stream.peek();
@ -43,7 +41,6 @@ CodeMirror.defineMode("pegjs", function (config) {
state.inComment = true;
}
//return state
if (state.inString) {
while (state.inString && !stream.eol()) {
if (stream.peek() === state.stringType) {

View File

@ -20,7 +20,7 @@
"def", "del", "elif", "else", "except", "finally",
"for", "from", "global", "if", "import",
"lambda", "pass", "raise", "return",
"try", "while", "with", "yield", "in"];
"try", "while", "with", "yield", "in", "False", "True"];
var commonBuiltins = ["abs", "all", "any", "bin", "bool", "bytearray", "callable", "chr",
"classmethod", "compile", "complex", "delattr", "dict", "dir", "divmod",
"enumerate", "eval", "filter", "float", "format", "frozenset",
@ -60,7 +60,7 @@
if (py3) {
// since http://legacy.python.org/dev/peps/pep-0465/ @ is also an operator
var identifiers = parserConf.identifiers|| /^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*/;
myKeywords = myKeywords.concat(["nonlocal", "False", "True", "None", "async", "await"]);
myKeywords = myKeywords.concat(["nonlocal", "None", "aiter", "anext", "async", "await", "breakpoint", "match", "case"]);
myBuiltins = myBuiltins.concat(["ascii", "bytes", "exec", "print"]);
var stringPrefixes = new RegExp("^(([rbuf]|(br)|(rb)|(fr)|(rf))?('{3}|\"{3}|['\"]))", "i");
} else {
@ -68,7 +68,7 @@
myKeywords = myKeywords.concat(["exec", "print"]);
myBuiltins = myBuiltins.concat(["apply", "basestring", "buffer", "cmp", "coerce", "execfile",
"file", "intern", "long", "raw_input", "reduce", "reload",
"unichr", "unicode", "xrange", "False", "True", "None"]);
"unichr", "unicode", "xrange", "None"]);
var stringPrefixes = new RegExp("^(([rubf]|(ur)|(br))?('{3}|\"{3}|['\"]))", "i");
}
var keywords = wordRegexp(myKeywords);

View File

@ -33,6 +33,9 @@ CodeMirror.defineMode("sparql", function(config) {
"true", "false", "with",
"data", "copy", "to", "move", "add", "create", "drop", "clear", "load", "into"]);
var operatorChars = /[*+\-<>=&|\^\/!\?]/;
var PN_CHARS = "[A-Za-z_\\-0-9]";
var PREFIX_START = new RegExp("[A-Za-z]");
var PREFIX_REMAINDER = new RegExp("((" + PN_CHARS + "|\\.)*(" + PN_CHARS + "))?:");
function tokenBase(stream, state) {
var ch = stream.next();
@ -71,20 +74,18 @@ CodeMirror.defineMode("sparql", function(config) {
stream.eatWhile(/[a-z\d\-]/i);
return "meta";
}
else {
stream.eatWhile(/[_\w\d]/);
if (stream.eat(":")) {
else if (PREFIX_START.test(ch) && stream.match(PREFIX_REMAINDER)) {
eatPnLocal(stream);
return "atom";
}
var word = stream.current();
if (ops.test(word))
return "builtin";
else if (keywords.test(word))
return "keyword";
else
return "variable";
}
stream.eatWhile(/[_\w\d]/);
var word = stream.current();
if (ops.test(word))
return "builtin";
else if (keywords.test(word))
return "keyword";
else
return "variable";
}
function eatPnLocal(stream) {

File diff suppressed because one or more lines are too long

View File

@ -85,7 +85,7 @@ CodeMirror.defineMode("yaml", function() {
}
/* pairs (associative arrays) -> key */
if (!state.pair && stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^,\[\]{}#&*!|>'"%@`])[^#]*?(?=\s*:($|\s))/)) {
if (!state.pair && stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^\s,\[\]{}#&*!|>'"%@`])[^#:]*(?=:($|\s))/)) {
state.pair = true;
state.keyCol = stream.indentation();
return "atom";

View File

@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.61.7-beta",
"version": "0.61.8-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@ -36,7 +36,7 @@
"@excalidraw/excalidraw": "0.16.1",
"archiver": "5.3.1",
"async-mutex": "0.4.0",
"axios": "1.5.0",
"axios": "1.5.1",
"better-sqlite3": "8.4.0",
"chokidar": "3.5.3",
"cls-hooked": "4.2.2",
@ -68,7 +68,7 @@
"jimp": "0.22.10",
"joplin-turndown-plugin-gfm": "1.0.12",
"jsdom": "22.1.0",
"marked": "9.0.3",
"marked": "9.1.0",
"mime-types": "2.1.35",
"multer": "1.4.5-lts.1",
"node-abi": "3.47.0",
@ -78,11 +78,11 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"request": "2.88.2",
"rimraf": "5.0.1",
"rimraf": "5.0.5",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.11.0",
"sax": "1.2.4",
"sax": "1.3.0",
"semver": "7.5.4",
"serve-favicon": "2.5.0",
"session-file-store": "1.5.0",
@ -97,11 +97,11 @@
},
"devDependencies": {
"cross-env": "7.0.3",
"electron": "25.8.2",
"electron": "25.9.0",
"electron-builder": "24.6.4",
"electron-packager": "17.1.2",
"electron-rebuild": "3.2.9",
"eslint": "8.49.0",
"eslint": "8.50.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-import": "2.28.1",

View File

@ -208,6 +208,7 @@ class Becca {
return this.etapiTokens[etapiTokenId];
}
/** @returns {AbstractBeccaEntity|null} */
getEntity(entityName, entityId) {
if (!entityName || !entityId) {
return null;

View File

@ -63,10 +63,10 @@ function load() {
log.info(`Becca (note cache) load took ${Date.now() - start}ms`);
}
function reload() {
function reload(reason) {
load();
require('../services/ws').reloadFrontend();
require('../services/ws').reloadFrontend(reason || "becca reloaded");
}
eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({entityName, entityRow}) => {

View File

@ -18,31 +18,11 @@ let becca = null;
class AbstractBeccaEntity {
/** @protected */
beforeSaving() {
this.generateIdIfNecessary();
}
/** @protected */
generateIdIfNecessary() {
if (!this[this.constructor.primaryKeyName]) {
this[this.constructor.primaryKeyName] = utils.newEntityId();
}
}
/** @protected */
generateHash(isDeleted = false) {
let contentToHash = "";
for (const propertyName of this.constructor.hashedProperties) {
contentToHash += `|${this[propertyName]}`;
}
if (isDeleted) {
contentToHash += "|deleted";
}
return utils.hash(contentToHash).substr(0, 10);
}
/** @protected */
getUtcDateChanged() {
return this.utcDateModified || this.utcDateCreated;
@ -61,7 +41,7 @@ class AbstractBeccaEntity {
}
/** @protected */
putEntityChange(isDeleted = false) {
putEntityChange(isDeleted) {
entityChangesService.putEntityChange({
entityName: this.constructor.entityName,
entityId: this[this.constructor.primaryKeyName],
@ -72,11 +52,37 @@ class AbstractBeccaEntity {
});
}
/**
* @protected
* @returns {string}
*/
generateHash(isDeleted) {
let contentToHash = "";
for (const propertyName of this.constructor.hashedProperties) {
contentToHash += `|${this[propertyName]}`;
}
if (isDeleted) {
contentToHash += "|deleted";
}
return utils.hash(contentToHash).substr(0, 10);
}
/** @protected */
getPojoToSave() {
return this.getPojo();
}
/**
* @protected
* @abstract
*/
getPojo() {
throw new Error(`Unimplemented getPojo() for entity '${this.constructor.name}'`)
}
/**
* Saves entity - executes SQL, but doesn't commit the transaction on its own
*
@ -88,9 +94,7 @@ class AbstractBeccaEntity {
const isNewEntity = !this[primaryKeyName];
if (this.beforeSaving) {
this.beforeSaving(opts);
}
this.beforeSaving(opts);
const pojo = this.getPojoToSave();
@ -101,7 +105,7 @@ class AbstractBeccaEntity {
return;
}
this.putEntityChange(false);
this.putEntityChange(!!this.isDeleted);
if (!cls.isEntityEventsDisabled()) {
const eventPayload = {

View File

@ -20,8 +20,7 @@ const attachmentRoleToNoteTypeMapping = {
class BAttachment extends AbstractBeccaEntity {
static get entityName() { return "attachments"; }
static get primaryKeyName() { return "attachmentId"; }
static get hashedProperties() { return ["attachmentId", "ownerId", "role", "mime", "title", "blobId",
"utcDateScheduledForErasureSince", "utcDateModified"]; }
static get hashedProperties() { return ["attachmentId", "ownerId", "role", "mime", "title", "blobId", "utcDateScheduledForErasureSince"]; }
constructor(row) {
super();

View File

@ -150,11 +150,17 @@ class BNote extends AbstractBeccaEntity {
*/
this.contentSize = null;
/**
* size of the content and note revision contents in bytes
* size of the note content, attachment contents in bytes
* @type {int|null}
* @private
*/
this.noteSize = null;
this.contentAndAttachmentsSize = null;
/**
* size of the note content, attachment contents and revision contents in bytes
* @type {int|null}
* @private
*/
this.contentAndAttachmentsAndRevisionsSize = null;
/**
* number of note revisions for this note
* @type {int|null}
@ -1625,16 +1631,12 @@ class BNote extends AbstractBeccaEntity {
revision.save(); // to generate revisionId, which is then used to save attachments
if (this.type === 'text') {
for (const noteAttachment of this.getAttachments()) {
if (noteAttachment.utcDateScheduledForErasureSince) {
continue;
}
const revisionAttachment = noteAttachment.copy();
revisionAttachment.ownerId = revision.revisionId;
revisionAttachment.setContent(noteAttachment.getContent(), {forceSave: true});
for (const noteAttachment of this.getAttachments()) {
const revisionAttachment = noteAttachment.copy();
revisionAttachment.ownerId = revision.revisionId;
revisionAttachment.setContent(noteAttachment.getContent(), {forceSave: true});
if (this.type === 'text') {
// content is rewritten to point to the revision attachments
noteContent = noteContent.replaceAll(`attachments/${noteAttachment.attachmentId}`,
`attachments/${revisionAttachment.attachmentId}`);

View File

@ -86,6 +86,29 @@ class BRevision extends AbstractBeccaEntity {
return this._getContent();
}
/**
* @returns {*}
* @throws Error in case of invalid JSON */
getJsonContent() {
const content = this.getContent();
if (!content || !content.trim()) {
return null;
}
return JSON.parse(content);
}
/** @returns {*|null} valid object or null if the content cannot be parsed as JSON */
getJsonContentSafely() {
try {
return this.getJsonContent();
}
catch (e) {
return null;
}
}
/**
* @param content
* @param {object} [opts]
@ -105,6 +128,45 @@ class BRevision extends AbstractBeccaEntity {
.map(row => new BAttachment(row));
}
/** @returns {BAttachment|null} */
getAttachmentById(attachmentId, opts = {}) {
opts.includeContentLength = !!opts.includeContentLength;
const query = opts.includeContentLength
? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
FROM attachments
JOIN blobs USING (blobId)
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
: `SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
return sql.getRows(query, [this.revisionId, attachmentId])
.map(row => new BAttachment(row))[0];
}
/** @returns {BAttachment[]} */
getAttachmentsByRole(role) {
return sql.getRows(`
SELECT attachments.*
FROM attachments
WHERE ownerId = ?
AND role = ?
AND isDeleted = 0
ORDER BY position`, [this.revisionId, role])
.map(row => new BAttachment(row));
}
/** @returns {BAttachment} */
getAttachmentByTitle(title) {
return sql.getRows(`
SELECT attachments.*
FROM attachments
WHERE ownerId = ?
AND title = ?
AND isDeleted = 0
ORDER BY position`, [this.revisionId, title])
.map(row => new BAttachment(row))[0];
}
beforeSaving() {
super.beforeSaving();

View File

@ -127,7 +127,8 @@ paths:
- targetRelationCount
- targetRelationCountIncludingLinks
- contentSize
- noteSize
- contentAndAttachmentsSize
- contentAndAttachmentsAndRevisionsSize
- revisionCount
- name: orderDirection
in: query

View File

@ -32,8 +32,8 @@ export default class LauncherContextMenu {
const isVisibleItem = parentNoteId === '_lbVisibleLaunchers';
const isAvailableItem = parentNoteId === '_lbAvailableLaunchers';
const isItem = isVisibleItem || isAvailableItem;
const canBeDeleted = !note.isLaunchBarConfig();
const canBeReset = note.isLaunchBarConfig();
const canBeDeleted = !note.noteId.startsWith("_"); // fixed notes can't be deleted
const canBeReset = !canBeDeleted && note.isLaunchBarConfig();;
return [
(isVisibleRoot || isAvailableRoot) ? { title: 'Add a note launcher', command: 'addNoteLauncher', uiIcon: "bx bx-plus" } : null,

View File

@ -10,6 +10,8 @@ const CODE_MIRROR = {
"libraries/codemirror/addon/lint/lint.js",
"libraries/codemirror/addon/lint/eslint.js",
"libraries/codemirror/addon/mode/loadmode.js",
"libraries/codemirror/addon/mode/multiplex.js",
"libraries/codemirror/addon/mode/overlay.js",
"libraries/codemirror/addon/mode/simple.js",
"libraries/codemirror/addon/search/match-highlighter.js",
"libraries/codemirror/mode/meta.js",

View File

@ -252,11 +252,15 @@ class NoteListRenderer {
if (pageCount < 20 || i <= 5 || pageCount - i <= 5 || Math.abs(this.page - i) <= 2) {
lastPrinted = true;
const startIndex = (i - 1) * this.pageSize + 1;
const endIndex = Math.min(this.noteIds.length, i * this.pageSize);
$pager.append(
i === this.page
? $('<span>').text(i).css('text-decoration', 'underline').css('font-weight', "bold")
: $('<a href="javascript:">')
.text(i)
.attr("title", `Page of ${startIndex} - ${endIndex}`)
.on('click', () => {
this.page = i;
this.renderList();
@ -270,6 +274,9 @@ class NoteListRenderer {
lastPrinted = false;
}
}
// no need to distinguish "note" vs "notes" since in case of one result, there's no paging at all
$pager.append(`<span class="note-list-pager-total-count">(${this.noteIds.length} notes)</span>`);
}
async renderNote(note, expand = false) {

View File

@ -45,6 +45,7 @@ const TPL = `
<a class="dropdown-item export-note-button">Export note</a>
<a class="dropdown-item delete-note-button">Delete note</a>
<a data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button"><kbd data-command="printActiveNote"></kbd> Print note</a>
<a data-trigger-command="forceSaveRevision" class="dropdown-item save-revision-button"><kbd data-command="forceSaveRevision"></kbd> Save revision</a>
</div>
</div>`;

View File

@ -274,26 +274,11 @@ export default class RevisionsDialog extends BasicWidget {
this.$content.html($table);
} else if (revisionItem.type === 'canvas') {
/**
* FIXME: We load a font called Virgil.wof2, which originates from excalidraw.com
* REMOVE external dependency!!!! This is defined in the svg in defs.style
*/
const content = fullRevision.content;
const sanitizedTitle = revisionItem.title.replace(/[^a-z0-9-.]/gi, "");
try {
const data = JSON.parse(content)
const svg = data.svg || "no svg present."
/**
* maxWidth: 100% use full width of container but do not enlarge!
* height:auto to ensure that height scales with width
*/
const $svgHtml = $(svg).css({maxWidth: "100%", height: "auto"});
this.$content.html($('<div>').append($svgHtml));
} catch (err) {
console.error("error parsing fullRevision.content as JSON", fullRevision.content, err);
this.$content.html($("<div>").text("Error parsing content. Please check console.error() for more details."));
}
this.$content.html($("<img>")
.attr("src", `api/revisions/${revisionItem.revisionId}/image/${sanitizedTitle}?${Math.random()}`)
.css("max-width", "100%"));
} else {
this.$content.text("Preview isn't available for this note type.");
}

View File

@ -14,7 +14,8 @@ const TPL = `
<option value="dateCreated">Date created</option>
<option value="dateModified">Date of last modification</option>
<option value="contentSize">Note content size</option>
<option value="noteSize">Note content size including revisions</option>
<option value="contentAndAttachmentsSize">Note content size including attachments</option>
<option value="contentAndAttachmentsAndRevisionsSize">Note content size including attachments and revisions</option>
<option value="revisionCount">Number of revisions</option>
<option value="childrenCount">Number of children notes</option>
<option value="parentCount">Number of clones</option>

View File

@ -5,14 +5,27 @@ const becca = require('../../becca/becca');
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
const fs = require('fs');
function returnImage(req, res) {
function returnImageFromNote(req, res) {
const image = becca.getNote(req.params.noteId);
return returnImageInt(image, res);
}
function returnImageFromRevision(req, res) {
const image = becca.getRevision(req.params.revisionId);
return returnImageInt(image, res);
}
/**
* @param {BNote|BRevision} image
* @param res
*/
function returnImageInt(image, res) {
if (!image) {
res.set('Content-Type', 'image/png');
return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`));
}
else if (!["image", "canvas"].includes(image.type)){
} else if (!["image", "canvas"].includes(image.type)) {
return res.sendStatus(400);
}
@ -84,7 +97,8 @@ function updateImage(req) {
}
module.exports = {
returnImage,
returnImageFromNote,
returnImageFromRevision,
returnAttachedImage,
updateImage
};

View File

@ -181,6 +181,8 @@ function register(app) {
apiRoute(GET, '/api/revisions/:revisionId/blob', revisionsApiRoute.getRevisionBlob);
apiRoute(DEL, '/api/revisions/:revisionId', revisionsApiRoute.eraseRevision);
apiRoute(PST, '/api/revisions/:revisionId/restore', revisionsApiRoute.restoreRevision);
route(GET, '/api/revisions/:revisionId/image/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImageFromRevision);
route(GET, '/api/revisions/:revisionId/download', [auth.checkApiAuthOrElectron], revisionsApiRoute.downloadRevision);
@ -200,7 +202,7 @@ function register(app) {
apiRoute(GET, '/api/attribute-values/:attributeName', attributesRoute.getValuesForAttribute);
// :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename
route(GET, '/api/images/:noteId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage);
route(GET, '/api/images/:noteId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImageFromNote);
route(PUT, '/api/images/:noteId', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.updateImage, apiResultHandler);
apiRoute(GET, '/api/options', optionsApiRoute.getOptions);

View File

@ -7,8 +7,9 @@ const sql = require("./sql");
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
// on the other hand builtin/system attrs should not contain any sensitive info
const builtinAttrNames = BUILTIN_ATTRIBUTES
.filter(attr => !["shareCredentials", "shareAlias"].includes(attr.name))
.map(attr => `'${attr.name}'`).join(', ');
const anonymizeScript = `

View File

@ -4,7 +4,7 @@ const build = require('./build');
const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 225;
const APP_DB_VERSION = 226;
const SYNC_VERSION = 31;
const CLIPPER_PROTOCOL_VERSION = "1.0";

View File

@ -1 +1 @@
module.exports = { buildDate:"2023-09-21T23:38:18+02:00", buildRevision: "79e5e3b65ff613cdb81e2afaa832037ccf06d7b8" };
module.exports = { buildDate:"2023-09-29T00:54:45+02:00", buildRevision: "e5555beea9a1638fefa218118e0596f4cfc1f4d0" };

View File

@ -763,7 +763,7 @@ class ConsistencyChecks {
}
if (this.reloadNeeded) {
require("../becca/becca_loader").reload();
require("../becca/becca_loader").reload("consistency checks need becca reload");
}
return !this.unrecoveredConsistencyErrors;

View File

@ -148,12 +148,13 @@ function fillEntityChanges(entityName, entityPrimaryKey, condition = '') {
const entity = becca.getEntity(entityName, entityId);
if (entity) {
ec.hash = entity.generateHash() || "|deleted";
ec.hash = entity.generateHash();
ec.utcDateChanged = entity.getUtcDateChanged() || dateUtils.utcNowDateTime();
ec.isSynced = entityName !== 'options' || !!entity.isSynced;
} else {
// entity might be null (not present in becca) when it's deleted
// FIXME: hacky, not sure if it might cause some problems
// this will produce different hash value than when entity is being deleted since then
// all normal hashed attributes are being used. Sync should recover from that, though.
ec.hash = "deleted";
ec.utcDateChanged = dateUtils.utcNowDateTime();
ec.isSynced = true; // deletable (the ones with isDeleted) entities are synced

View File

@ -327,6 +327,11 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
content = content.replace(/<\/body>.*<\/html>/gis, "");
content = content.replace(/src="([^"]*)"/g, (match, url) => {
if (url.startsWith("data:image")) {
// inline images are parsed and saved into attachments in the note service
return match;
}
try {
url = decodeURIComponent(url).trim();
} catch (e) {
@ -456,7 +461,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
position: attachmentMeta.position
});
attachment.setContent(content);
attachment.setContent(content, { forceSave: true });
return;
}

View File

@ -60,7 +60,7 @@ function checkProtectedSessionExpiration() {
log.info("Expiring protected session");
require('./ws').reloadFrontend();
require('./ws').reloadFrontend("leaving protected session");
}
}

View File

@ -56,9 +56,9 @@ class OrderByAndLimitExp extends Expression {
if (!valA && !valB) {
// the attribute value is empty/zero in both notes so continue to the next order definition
continue;
} else if (!valB || valA < valB) {
} else if (valA < valB) {
return smaller;
} else if (!valA || valA > valB) {
} else if (valA > valB) {
return larger;
}
// else the values are equal and continue to next order definition

View File

@ -31,7 +31,8 @@ const PROP_MAPPING = {
"targetrelationcount": "targetRelationCount",
"targetrelationcountincludinglinks": "targetRelationCountIncludingLinks",
"contentsize": "contentSize",
"notesize": "noteSize",
"contentandattachmentssize": "contentAndAttachmentsSize",
"contentandattachmentsandrevisionssize": "contentAndAttachmentsAndRevisionsSize",
"revisioncount": "revisionCount"
};
@ -48,7 +49,7 @@ class PropertyComparisonExp extends Expression {
this.comparedValue = comparedValue; // for DEBUG mode
this.comparator = buildComparator(operator, comparedValue);
if (['contentsize', 'notesize', 'revisioncount'].includes(this.propertyName)) {
if (['contentsize', 'contentandattachmentssize', 'contentandattachmentsandrevisionssize', 'revisioncount'].includes(this.propertyName)) {
searchContext.dbLoadNeeded = true;
}
}

View File

@ -92,47 +92,107 @@ function searchFromRelation(note, relationName) {
function loadNeededInfoFromDatabase() {
const sql = require('../../sql');
for (const noteId in becca.notes) {
becca.notes[noteId].contentSize = 0;
becca.notes[noteId].noteSize = 0;
becca.notes[noteId].revisionCount = 0;
}
/**
* This complex structure is needed to calculate total occupied space by a note. Several object instances
* (note, revisions, attachments) can point to a single blobId, and thus the blob size should count towards the total
* only once.
*
* @var {Object.<string, Object.<string, int>>} - noteId => { blobId => blobSize }
*/
const noteBlobs = {};
const noteContentLengths = sql.getRows(`
SELECT
noteId,
blobId,
LENGTH(content) AS length
FROM notes
JOIN blobs USING(blobId)
WHERE notes.isDeleted = 0`);
for (const {noteId, length} of noteContentLengths) {
for (const {noteId, blobId, length} of noteContentLengths) {
if (!(noteId in becca.notes)) {
log.error(`Note ${noteId} not found in becca.`);
log.error(`Note '${noteId}' not found in becca.`);
continue;
}
becca.notes[noteId].contentSize = length;
becca.notes[noteId].noteSize = length;
becca.notes[noteId].revisionCount = 0;
noteBlobs[noteId] = { [blobId]: length };
}
const revisionContentLengths = sql.getRows(`
SELECT
noteId,
LENGTH(content) AS length
FROM notes
JOIN revisions USING(noteId)
JOIN blobs USING(blobId)
WHERE notes.isDeleted = 0`);
const attachmentContentLengths = sql.getRows(`
SELECT
ownerId AS noteId,
attachments.blobId,
LENGTH(content) AS length
FROM attachments
JOIN notes ON attachments.ownerId = notes.noteId
JOIN blobs ON attachments.blobId = blobs.blobId
WHERE attachments.isDeleted = 0
AND notes.isDeleted = 0`);
for (const {noteId, length} of revisionContentLengths) {
for (const {noteId, blobId, length} of attachmentContentLengths) {
if (!(noteId in becca.notes)) {
log.error(`Note ${noteId} not found in becca.`);
log.error(`Note '${noteId}' not found in becca.`);
continue;
}
becca.notes[noteId].noteSize += length;
becca.notes[noteId].revisionCount++;
if (!(noteId in noteBlobs)) {
log.error(`Did not find a '${noteId}' in the noteBlobs.`);
continue;
}
noteBlobs[noteId][blobId] = length;
}
for (const noteId in noteBlobs) {
becca.notes[noteId].contentAndAttachmentsSize = Object.values(noteBlobs[noteId]).reduce((acc, size) => acc + size, 0);
}
const revisionContentLengths = sql.getRows(`
SELECT
noteId,
revisions.blobId,
LENGTH(content) AS length,
1 AS isNoteRevision
FROM notes
JOIN revisions USING(noteId)
JOIN blobs ON revisions.blobId = blobs.blobId
WHERE notes.isDeleted = 0
UNION ALL
SELECT
noteId,
revisions.blobId,
LENGTH(content) AS length,
0 AS isNoteRevision -- it's attachment not counting towards revision count
FROM notes
JOIN revisions USING(noteId)
JOIN attachments ON attachments.ownerId = revisions.revisionId
JOIN blobs ON attachments.blobId = blobs.blobId
WHERE notes.isDeleted = 0`);
for (const {noteId, blobId, length, isNoteRevision} of revisionContentLengths) {
if (!(noteId in becca.notes)) {
log.error(`Note '${noteId}' not found in becca.`);
continue;
}
if (!(noteId in noteBlobs)) {
log.error(`Did not find a '${noteId}' in the noteBlobs.`);
continue;
}
noteBlobs[noteId][blobId] = length;
if (isNoteRevision) {
becca.notes[noteId].revisionCount++;
}
}
for (const noteId in noteBlobs) {
becca.notes[noteId].contentAndAttachmentsAndRevisionsSize = Object.values(noteBlobs[noteId]).reduce((acc, size) => acc + size, 0);
}
}

View File

@ -27,7 +27,8 @@ const PROP_MAPPING = {
"targetrelationcount": "targetRelationCount",
"targetrelationcountincludinglinks": "targetRelationCountIncludingLinks",
"contentsize": "contentSize",
"notesize": "noteSize",
"contentandattachmentssize": "contentAndAttachmentsSize",
"contentandattachmentsandrevisionssize": "contentAndAttachmentsAndRevisionsSize",
"revisioncount": "revisionCount"
};
@ -42,7 +43,7 @@ class ValueExtractor {
this.propertyPath = ['note', 'relations', this.propertyPath[0].substr(1), ...this.propertyPath.slice(1, this.propertyPath.length)];
}
if (['contentsize', 'notesize', 'revisioncount'].includes(this.propertyPath[this.propertyPath.length - 1])) {
if (['contentsize', 'contentandattachmentssize', 'contentandattachmentsandrevisionssize', 'revisioncount'].includes(this.propertyPath[this.propertyPath.length - 1])) {
searchContext.dbLoadNeeded = true;
}
}

View File

@ -151,9 +151,14 @@ async function pullChanges(syncContext) {
if (entityChanges.length === 0) {
break;
} else {
const sizeInKb = Math.round(JSON.stringify(resp).length / 1024);
try { // https://github.com/zadam/trilium/issues/4310
const sizeInKb = Math.round(JSON.stringify(resp).length / 1024);
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`);
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`);
}
catch (e) {
log.error(`Error occurred ${e.message} ${e.stack}`);
}
}
}

View File

@ -106,7 +106,7 @@ function updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext
updateContext.updated[remoteEC.entityName] = updateContext.updated[remoteEC.entityName] || [];
updateContext.updated[remoteEC.entityName].push(remoteEC.entityId);
if (!localEC || localEC.utcDateChanged < remoteEC.utcDateChanged) {
if (!localEC || localEC.utcDateChanged < remoteEC.utcDateChanged || localEC.hash !== remoteEC.hash) {
entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
}

View File

@ -13,7 +13,7 @@ const env = require('./env');
if (env.isDev()) {
const chokidar = require('chokidar');
const debounce = require('debounce');
const debouncedReloadFrontend = debounce(reloadFrontend, 200);
const debouncedReloadFrontend = debounce(() => reloadFrontend("source code change"), 200);
chokidar
.watch('src/public')
.on('add', debouncedReloadFrontend)
@ -230,8 +230,8 @@ function syncFailed() {
sendMessageToAllClients({ type: 'sync-failed', lastSyncedPush });
}
function reloadFrontend() {
sendMessageToAllClients({ type: 'reload-frontend' });
function reloadFrontend(reason) {
sendMessageToAllClients({ type: 'reload-frontend', reason });
}
function setLastSyncedPush(entityChangeId) {