Merge remote-tracking branch 'origin/stable'

# Conflicts:
#	src/routes/api/attributes.js
#	src/routes/api/sender.js
This commit is contained in:
zadam 2020-11-18 22:42:37 +01:00
commit 8e2bf7795c
20 changed files with 139 additions and 92 deletions

23
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "trilium", "name": "trilium",
"version": "0.45.2", "version": "0.45.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -4838,6 +4838,11 @@
"type-check": "~0.3.2" "type-check": "~0.3.2"
} }
}, },
"limiter": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
"integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
},
"line-column": { "line-column": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz", "resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz",
@ -6913,6 +6918,22 @@
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
}, },
"stream-throttle": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz",
"integrity": "sha1-rdV8jXzHOoFjDTHNVdOWHPr7qcM=",
"requires": {
"commander": "^2.2.0",
"limiter": "^1.0.5"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
}
}
},
"streamsearch": { "streamsearch": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",

View File

@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.45.3", "version": "0.45.4",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {
@ -65,6 +65,7 @@
"semver": "7.3.2", "semver": "7.3.2",
"serve-favicon": "2.5.0", "serve-favicon": "2.5.0",
"session-file-store": "1.5.0", "session-file-store": "1.5.0",
"stream-throttle": "^0.1.3",
"striptags": "3.1.1", "striptags": "3.1.1",
"tmp": "^0.2.1", "tmp": "^0.2.1",
"turndown": "7.0.0", "turndown": "7.0.0",

View File

@ -34,6 +34,10 @@ class Attribute extends Entity {
this.isInheritable = !!this.isInheritable; this.isInheritable = !!this.isInheritable;
} }
isAutoLink() {
return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name);
}
/** /**
* @returns {Note|null} * @returns {Note|null}
*/ */

View File

@ -274,7 +274,10 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
* *
* @method * @method
* @param {string} notePath (or noteId) * @param {string} notePath (or noteId)
* @param {string} [noteTitle] - if not present we'll use note title * @param {object} [params]
* @param {boolean} [params.showTooltip=true] - enable/disable tooltip on the link
* @param {boolean} [params.showNotePath=false] - show also whole note's path as part of the link
* @param {string} [title=] - custom link tile with note's title as default
*/ */
this.createNoteLink = linkService.createNoteLink; this.createNoteLink = linkService.createNoteLink;

View File

@ -64,6 +64,7 @@ function getHost() {
} }
export default { export default {
download,
downloadFileNote, downloadFileNote,
openFileNote, openFileNote,
downloadNoteRevision, downloadNoteRevision,

View File

@ -8,8 +8,6 @@ import options from "./options.js";
import treeCache from "./tree_cache.js"; import treeCache from "./tree_cache.js";
import noteAttributeCache from "./note_attribute_cache.js"; import noteAttributeCache from "./note_attribute_cache.js";
const $outstandingSyncsCount = $("#outstanding-syncs-count");
const messageHandlers = []; const messageHandlers = [];
let ws; let ws;
@ -64,8 +62,6 @@ async function handleMessage(event) {
let syncRows = message.data; let syncRows = message.data;
lastPingTs = Date.now(); lastPingTs = Date.now();
$outstandingSyncsCount.html(message.outstandingSyncs);
if (syncRows.length > 0) { if (syncRows.length > 0) {
logRows(syncRows); logRows(syncRows);

View File

@ -130,7 +130,7 @@ function SetupModel() {
} }
async function checkOutstandingSyncs() { async function checkOutstandingSyncs() {
const { stats, initialized } = await $.get('api/sync/stats'); const { outstandingPullCount, initialized } = await $.get('api/sync/stats');
if (initialized) { if (initialized) {
if (utils.isElectron()) { if (utils.isElectron()) {
@ -143,9 +143,7 @@ async function checkOutstandingSyncs() {
} }
} }
else { else {
const totalOutstandingSyncs = stats.outstandingPushes + stats.outstandingPulls; $("#outstanding-syncs").html(outstandingPullCount);
$("#outstanding-syncs").html(totalOutstandingSyncs);
} }
} }

View File

@ -41,7 +41,7 @@ const TPL = `
<a class="dropdown-item sync-now-button" title="Trigger sync"> <a class="dropdown-item sync-now-button" title="Trigger sync">
<span class="bx bx-refresh"></span> <span class="bx bx-refresh"></span>
Sync now (<span id="outstanding-syncs-count">0</span>) Sync now
</a> </a>
<a class="dropdown-item" data-trigger-command="openNewWindow"> <a class="dropdown-item" data-trigger-command="openNewWindow">

View File

@ -28,8 +28,10 @@ function updateNoteAttribute(req) {
|| body.name !== attribute.name || body.name !== attribute.name
|| (body.type === 'relation' && body.value !== attribute.value)) { || (body.type === 'relation' && body.value !== attribute.value)) {
let newAttribute;
if (body.type !== 'relation' || !!body.value.trim()) { if (body.type !== 'relation' || !!body.value.trim()) {
const newAttribute = attribute.createClone(body.type, body.name, body.value); newAttribute = attribute.createClone(body.type, body.name, body.value);
newAttribute.save(); newAttribute.save();
} }
@ -37,7 +39,7 @@ function updateNoteAttribute(req) {
attribute.save(); attribute.save();
return { return {
attributeId: attribute.attributeId attributeId: newAttribute ? newAttribute.attributeId : null
}; };
} }
} }
@ -54,6 +56,7 @@ function updateNoteAttribute(req) {
if (attribute.type === 'label' || body.value.trim()) { if (attribute.type === 'label' || body.value.trim()) {
attribute.value = body.value; attribute.value = body.value;
attribute.isDeleted = false;
} }
else { else {
// relations should never have empty target // relations should never have empty target
@ -160,8 +163,10 @@ function updateNoteAttributes(req) {
// all the remaining existing attributes are not defined anymore and should be deleted // all the remaining existing attributes are not defined anymore and should be deleted
for (const toDeleteAttr of existingAttrs) { for (const toDeleteAttr of existingAttrs) {
toDeleteAttr.isDeleted = true; if (!toDeleteAttr.isAutoLink()) {
toDeleteAttr.save(); toDeleteAttr.isDeleted = true;
toDeleteAttr.save();
}
} }
} }

View File

@ -31,19 +31,36 @@ function getRecentChanges(req) {
} }
} }
// now we need to also collect date points not represented in note revisions:
// 1. creation for all notes (dateCreated)
// 2. deletion for deleted notes (dateModified)
const notes = sql.getRows(` const notes = sql.getRows(`
SELECT SELECT
notes.noteId, notes.noteId,
notes.isDeleted AS current_isDeleted, notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId, notes.deleteId AS current_deleteId,
notes.isErased AS current_isErased, notes.isErased AS current_isErased,
notes.title AS current_title, notes.title AS current_title,
notes.isProtected AS current_isProtected, notes.isProtected AS current_isProtected,
notes.title, notes.title,
notes.utcDateCreated AS utcDate, notes.utcDateCreated AS utcDate,
notes.dateCreated AS date notes.dateCreated AS date
FROM FROM
notes`); notes
UNION ALL
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.isErased AS current_isErased,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
notes.title,
notes.utcDateModified AS utcDate,
notes.dateModified AS date
FROM
notes
WHERE notes.isDeleted = 1 AND notes.isErased = 0`);
for (const note of notes) { for (const note of notes) {
if (noteCacheService.isInAncestor(note.noteId, ancestorNoteId)) { if (noteCacheService.isInAncestor(note.noteId, ancestorNoteId)) {

View File

@ -4,6 +4,7 @@ const imageType = require('image-type');
const imageService = require('../../services/image'); const imageService = require('../../services/image');
const dateNoteService = require('../../services/date_notes'); const dateNoteService = require('../../services/date_notes');
const noteService = require('../../services/notes'); const noteService = require('../../services/notes');
const attributeService = require('../../services/attributes');
function uploadImage(req) { function uploadImage(req) {
const file = req.file; const file = req.file;
@ -35,12 +36,10 @@ function saveNote(req) {
mime: 'text/html' mime: 'text/html'
}); });
if (req.body.label && req.body.label.trim()){ if (req.body.labels) {
let value; for (const {name, value} of req.body.labels) {
if (req.body.labelValue && req.body.labelValue.trim()){ note.setLabel(attributeService.sanitizeAttributeName(name), value);
value = req.body.labelValue;
} }
note.setLabel(req.body.label, value);
} }
return { return {

View File

@ -13,13 +13,13 @@ const dateUtils = require('../../services/date_utils');
const entityConstructor = require('../../entities/entity_constructor'); const entityConstructor = require('../../entities/entity_constructor');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
function testSync() { async function testSync() {
try { try {
if (!syncOptions.isSyncSetup()) { if (!syncOptions.isSyncSetup()) {
return { success: false, message: "Sync server host is not configured. Please configure sync first." }; return { success: false, message: "Sync server host is not configured. Please configure sync first." };
} }
syncService.login(); await syncService.login();
// login was successful so we'll kick off sync now // login was successful so we'll kick off sync now
// this is important in case when sync server has been just initialized // this is important in case when sync server has been just initialized
@ -43,7 +43,7 @@ function getStats() {
const stats = { const stats = {
initialized: optionService.getOption('initialized') === 'true', initialized: optionService.getOption('initialized') === 'true',
stats: syncService.stats outstandingPullCount: syncService.getOutstandingPullCount()
}; };
log.info(`Returning sync stats: ${JSON.stringify(stats)}`); log.info(`Returning sync stats: ${JSON.stringify(stats)}`);

View File

@ -2,7 +2,6 @@
const repository = require('./repository'); const repository = require('./repository');
const sql = require('./sql'); const sql = require('./sql');
const utils = require('./utils');
const Attribute = require('../entities/attribute'); const Attribute = require('../entities/attribute');
const ATTRIBUTE_TYPES = [ 'label', 'relation' ]; const ATTRIBUTE_TYPES = [ 'label', 'relation' ];
@ -146,6 +145,20 @@ function getBuiltinAttributeNames() {
]); ]);
} }
function sanitizeAttributeName(origName) {
let fixedName;
if (origName === '') {
fixedName = "unnamed";
}
else {
// any not allowed character should be replaced with underscore
fixedName = origName.replace(/[^\p{L}\p{N}_:]/ug, "_");
}
return fixedName;
}
module.exports = { module.exports = {
getNotesWithLabel, getNotesWithLabel,
getNotesWithLabels, getNotesWithLabels,
@ -156,5 +169,6 @@ module.exports = {
getAttributeNames, getAttributeNames,
isAttributeType, isAttributeType,
isAttributeDangerous, isAttributeDangerous,
getBuiltinAttributeNames getBuiltinAttributeNames,
sanitizeAttributeName
}; };

View File

@ -1 +1 @@
module.exports = { buildDate:"2020-11-10T22:54:39+01:00", buildRevision: "5157fc15e9f7fa960ee35685426868d5599933dc" }; module.exports = { buildDate:"2020-11-12T22:15:23+01:00", buildRevision: "6c57b2220ff05059d7460369b195d281fcd9cbb6" };

View File

@ -11,6 +11,7 @@ const entityChangesService = require('./entity_changes.js');
const optionsService = require('./options'); const optionsService = require('./options');
const Branch = require('../entities/branch'); const Branch = require('../entities/branch');
const dateUtils = require('./date_utils'); const dateUtils = require('./date_utils');
const attributeService = require('./attributes');
class ConsistencyChecks { class ConsistencyChecks {
constructor(autoFix) { constructor(autoFix) {
@ -607,20 +608,10 @@ class ConsistencyChecks {
findWronglyNamedAttributes() { findWronglyNamedAttributes() {
const attrNames = sql.getColumn(`SELECT DISTINCT name FROM attributes`); const attrNames = sql.getColumn(`SELECT DISTINCT name FROM attributes`);
const attrNameMatcher = new RegExp("^[\\p{L}\\p{N}_:]+$", "u");
for (const origName of attrNames) { for (const origName of attrNames) {
if (!attrNameMatcher.test(origName)) { const fixedName = attributeService.sanitizeAttributeName(origName);
let fixedName;
if (origName === '') {
fixedName = "unnamed";
}
else {
// any not allowed character should be replaced with underscore
fixedName = origName.replace(/[^\p{L}\p{N}_:]/ug, "_");
}
if (fixedName !== origName) {
if (this.autoFix) { if (this.autoFix) {
// there isn't a good way to update this: // there isn't a good way to update this:
// - just SQL query will fix it in DB but not notify frontend (or other caches) that it has been fixed // - just SQL query will fix it in DB but not notify frontend (or other caches) that it has been fixed

View File

@ -37,7 +37,7 @@ function getImageType(buffer) {
} }
} }
else { else {
return imageType(buffer); return imageType(buffer) || "jpg"; // optimistic JPG default
} }
} }

View File

@ -1,5 +1,6 @@
const sax = require("sax"); const sax = require("sax");
const stream = require('stream'); const stream = require('stream');
const {Throttle} = require('stream-throttle');
const log = require("../log"); const log = require("../log");
const utils = require("../utils"); const utils = require("../utils");
const sql = require("../sql"); const sql = require("../sql");
@ -7,6 +8,7 @@ const noteService = require("../notes");
const imageService = require("../image"); const imageService = require("../image");
const protectedSessionService = require('../protected_session'); const protectedSessionService = require('../protected_session');
const htmlSanitizer = require("../html_sanitizer"); const htmlSanitizer = require("../html_sanitizer");
const attributeService = require("../attributes");
// date format is e.g. 20181121T193703Z // date format is e.g. 20181121T193703Z
function parseDate(text) { function parseDate(text) {
@ -37,10 +39,6 @@ function importEnex(taskContext, file, parentNote) {
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(), isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
})).note; })).note;
// we're persisting notes as we parse the document, but these are run asynchronously and may not be finished
// when we finish parsing. We use this to be sure that all saving has been finished before returning successfully.
const saveNotePromises = [];
function extractContent(content) { function extractContent(content) {
const openingNoteIndex = content.indexOf('<en-note>'); const openingNoteIndex = content.indexOf('<en-note>');
@ -105,9 +103,17 @@ function importEnex(taskContext, file, parentNote) {
const previousTag = getPreviousTag(); const previousTag = getPreviousTag();
if (previousTag === 'note-attributes') { if (previousTag === 'note-attributes') {
let labelName = currentTag;
if (labelName === 'source-url') {
labelName = 'sourceUrl';
}
labelName = attributeService.sanitizeAttributeName(labelName);
note.attributes.push({ note.attributes.push({
type: 'label', type: 'label',
name: currentTag, name: labelName,
value: text value: text
}); });
} }
@ -149,7 +155,7 @@ function importEnex(taskContext, file, parentNote) {
} else if (currentTag === 'tag') { } else if (currentTag === 'tag') {
note.attributes.push({ note.attributes.push({
type: 'label', type: 'label',
name: text, name: attributeService.sanitizeAttributeName(text),
value: '' value: ''
}) })
} }
@ -227,6 +233,10 @@ function importEnex(taskContext, file, parentNote) {
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();
for (const resource of resources) { for (const resource of resources) {
if (!resource.content) {
continue;
}
const hash = utils.md5(resource.content); const hash = utils.md5(resource.content);
const mediaRegex = new RegExp(`<en-media hash="${hash}"[^>]*>`, 'g'); const mediaRegex = new RegExp(`<en-media hash="${hash}"[^>]*>`, 'g');
@ -300,13 +310,7 @@ function importEnex(taskContext, file, parentNote) {
updateDates(noteEntity.noteId, utcDateCreated, utcDateModified); updateDates(noteEntity.noteId, utcDateCreated, utcDateModified);
} }
saxStream.on("closetag", tag => { saxStream.on("closetag", tag => path.pop());
path.pop();
if (tag === 'note') {
saveNotePromises.push(saveNote());
}
});
saxStream.on("opencdata", () => { saxStream.on("opencdata", () => {
//console.log("opencdata"); //console.log("opencdata");
@ -323,12 +327,15 @@ function importEnex(taskContext, file, parentNote) {
return new Promise((resolve, reject) => return new Promise((resolve, reject) =>
{ {
// resolve only when we parse the whole document AND saving of all notes have been finished // resolve only when we parse the whole document AND saving of all notes have been finished
saxStream.on("end", () => { Promise.all(saveNotePromises).then(() => resolve(rootNote)) }); saxStream.on("end", () => resolve(rootNote));
const bufferStream = new stream.PassThrough(); const bufferStream = new stream.PassThrough();
bufferStream.end(file.buffer); bufferStream.end(file.buffer);
bufferStream.pipe(saxStream); bufferStream
// rate limiting to improve responsiveness during / after import
.pipe(new Throttle({rate: 500000}))
.pipe(saxStream);
}); });
} }

View File

@ -17,10 +17,14 @@ const utils = require('../../utils.js');
*/ */
function findNotesWithExpression(expression) { function findNotesWithExpression(expression) {
const hoistedNote = noteCache.notes[hoistedNoteService.getHoistedNoteId()]; const hoistedNote = noteCache.notes[hoistedNoteService.getHoistedNoteId()];
const allNotes = (hoistedNote && hoistedNote.noteId !== 'root') let allNotes = (hoistedNote && hoistedNote.noteId !== 'root')
? hoistedNote.subtreeNotes ? hoistedNote.subtreeNotes
: Object.values(noteCache.notes); : Object.values(noteCache.notes);
// in the process of loading data sometimes we create "skeleton" note instances which are expected to be filled later
// in case of inconsistent data this might not work and search will then crash on these
allNotes = allNotes.filter(note => note.type !== undefined);
const allNoteSet = new NoteSet(allNotes); const allNoteSet = new NoteSet(allNotes);
const searchContext = { const searchContext = {

View File

@ -20,10 +20,7 @@ const entityConstructor = require('../entities/entity_constructor');
let proxyToggle = true; let proxyToggle = true;
const stats = { let outstandingPullCount = 0;
outstandingPushes: 0,
outstandingPulls: 0
};
async function sync() { async function sync() {
try { try {
@ -135,11 +132,7 @@ async function pullChanges(syncContext) {
const pulledDate = Date.now(); const pulledDate = Date.now();
stats.outstandingPulls = resp.maxEntityChangeId - lastSyncedPull; outstandingPullCount = Math.max(0, resp.maxEntityChangeId - lastSyncedPull);
if (stats.outstandingPulls < 0) {
stats.outstandingPulls = 0;
}
const {entityChanges} = resp; const {entityChanges} = resp;
@ -159,13 +152,13 @@ async function pullChanges(syncContext) {
syncUpdateService.updateEntity(entityChange, entity, syncContext.sourceId); syncUpdateService.updateEntity(entityChange, entity, syncContext.sourceId);
} }
stats.outstandingPulls = resp.maxEntityChangeId - entityChange.id; outstandingPullCount = Math.max(0, resp.maxEntityChangeId - entityChange.id);
} }
setLastSyncedPull(entityChanges[entityChanges.length - 1].entityChange.id); setLastSyncedPull(entityChanges[entityChanges.length - 1].entityChange.id);
}); });
log.info(`Pulled ${entityChanges.length} changes starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${stats.outstandingPulls} outstanding pulls`); log.info(`Pulled ${entityChanges.length} changes starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${outstandingPullCount} outstanding pulls`);
} }
if (atLeastOnePullApplied) { if (atLeastOnePullApplied) {
@ -359,31 +352,25 @@ function setLastSyncedPush(entityChangeId) {
optionService.setOption('lastSyncedPush', entityChangeId); optionService.setOption('lastSyncedPush', entityChangeId);
} }
function updatePushStats() {
if (syncOptions.isSyncSetup()) {
const lastSyncedPush = optionService.getOption('lastSyncedPush');
stats.outstandingPushes = sql.getValue("SELECT COUNT(1) FROM entity_changes WHERE isSynced = 1 AND id > ?", [lastSyncedPush]);
}
}
function getMaxEntityChangeId() { function getMaxEntityChangeId() {
return sql.getValue('SELECT COALESCE(MAX(id), 0) FROM entity_changes'); return sql.getValue('SELECT COALESCE(MAX(id), 0) FROM entity_changes');
} }
function getOutstandingPullCount() {
return outstandingPullCount;
}
sqlInit.dbReady.then(() => { sqlInit.dbReady.then(() => {
setInterval(cls.wrap(sync), 60000); setInterval(cls.wrap(sync), 60000);
// kickoff initial sync immediately // kickoff initial sync immediately
setTimeout(cls.wrap(sync), 3000); setTimeout(cls.wrap(sync), 3000);
setInterval(cls.wrap(updatePushStats), 1000);
}); });
module.exports = { module.exports = {
sync, sync,
login, login,
getEntityChangesRecords, getEntityChangesRecords,
stats, getOutstandingPullCount,
getMaxEntityChangeId getMaxEntityChangeId
}; };

View File

@ -110,8 +110,7 @@ function sendPing(client, syncRows = []) {
sendMessage(client, { sendMessage(client, {
type: 'sync', type: 'sync',
data: syncRows, data: syncRows
outstandingSyncs: stats.outstandingPushes + stats.outstandingPulls
}); });
} }