Merge branch 'master' into ckeditor

This commit is contained in:
azivner 2017-12-09 11:56:36 -05:00
commit 7ca043ebc6
34 changed files with 504 additions and 136 deletions

View file

@ -18,6 +18,7 @@ const log = require('../services/log');
const app_info = require('../services/app_info');
const messaging = require('../services/messaging');
const utils = require('../services/utils');
const sql = require('../services/sql');
const port = normalizePort(config['Network']['port'] || '3000');
app.set('port', port);
@ -53,7 +54,7 @@ httpServer.listen(port);
httpServer.on('error', onError);
httpServer.on('listening', onListening);
messaging.init(httpServer, sessionParser);
sql.dbReady.then(() => messaging.init(httpServer, sessionParser));
if (utils.isElectron()) {
const electronRouting = require('../routes/electron');

3
export-schema.sh Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
sqlite3 ~/trilium-data/document.db .schema > schema.sql

View file

@ -0,0 +1 @@
UPDATE options SET opt_name = 'start_note_path' WHERE opt_name = 'start_note_tree_id';

33
package-lock.json generated
View file

@ -2419,19 +2419,19 @@
"integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo="
},
"electron": {
"version": "1.8.2-beta.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2-beta.2.tgz",
"integrity": "sha1-tTLHEFDd0tSwDi4NNV3k51vB5f0=",
"version": "1.8.2-beta.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2-beta.3.tgz",
"integrity": "sha1-Ljkcy9YnaKOzsmC48uPxZHNWeuw=",
"requires": {
"@types/node": "8.0.49",
"@types/node": "8.0.56",
"electron-download": "3.3.0",
"extract-zip": "1.6.5"
},
"dependencies": {
"@types/node": {
"version": "8.0.49",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.49.tgz",
"integrity": "sha512-Oq3cV/mrMKy6Tv42llfS8YIH30ooDdhbJ40h1zoWl+goOJw8Kjy8j8RfjGZtZIUDO0gLwCfcbYM7+LModnbeMw=="
"version": "8.0.56",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.56.tgz",
"integrity": "sha512-JAlQv3hUWbrnruuTiLDf1scd4F/TBT0LgGEe+BBeF3p/Rc3yL6RV57WJN2nK5i+BshEz1sDllwH0Fzbuo7G4QA=="
}
}
},
@ -2590,7 +2590,7 @@
},
"fs-extra": {
"version": "0.30.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz",
"resolved": "http://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz",
"integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=",
"requires": {
"graceful-fs": "4.1.11",
@ -2807,6 +2807,23 @@
"yargs": "6.6.0"
},
"dependencies": {
"@types/node": {
"version": "8.0.56",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.56.tgz",
"integrity": "sha512-JAlQv3hUWbrnruuTiLDf1scd4F/TBT0LgGEe+BBeF3p/Rc3yL6RV57WJN2nK5i+BshEz1sDllwH0Fzbuo7G4QA==",
"dev": true
},
"electron": {
"version": "1.8.2-beta.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2-beta.2.tgz",
"integrity": "sha1-tTLHEFDd0tSwDi4NNV3k51vB5f0=",
"dev": true,
"requires": {
"@types/node": "8.0.56",
"electron-download": "3.3.0",
"extract-zip": "1.6.5"
}
},
"yargs": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz",

View file

@ -19,7 +19,7 @@
"debug": "~3.1.0",
"devtron": "^1.4.0",
"ejs": "~2.5.7",
"electron": "^1.8.2-beta.2",
"electron": "^1.8.2-beta.3",
"electron-debug": "^1.0.0",
"express": "~4.16.2",
"express-session": "^1.15.6",

View file

@ -19,7 +19,7 @@ const contextMenu = (function() {
// just do nothing
}
else {
throw new Error("Unrecognized clipboard mode=" + clipboardMode);
throwError("Unrecognized clipboard mode=" + clipboardMode);
}
clipboardId = null;
@ -36,7 +36,7 @@ const contextMenu = (function() {
treeChanges.cloneNoteTo(clipboardId, node.data.note_id);
}
else {
throw new Error("Unrecognized clipboard mode=" + mode);
throwError("Unrecognized clipboard mode=" + mode);
}
clipboardId = null;

View file

@ -27,7 +27,7 @@ const noteHistory = (function() {
historyItems = await server.get('notes-history/' + noteId);
for (const item of historyItems) {
const dateModified = getDateFromTS(item.date_modified_to);
const dateModified = getDateFromTS(item.date_modified_from);
$("#note-history-list").append($('<option>', {
value: item.note_history_id,

View file

@ -4,7 +4,7 @@ const messaging = (function() {
let ws = null;
function logError(message) {
console.error(message);
console.trace(message);
if (ws && ws.readyState === 1) {
ws.send(JSON.stringify({

View file

@ -4,7 +4,7 @@ const noteTree = (function() {
const treeEl = $("#tree");
const parentListEl = $("#parent-list");
let startNoteTreeId = null;
let startNotePath = null;
let notesTreeMap = {};
let parentToChildren = {};
@ -25,7 +25,7 @@ const noteTree = (function() {
let title = noteIdToTitle[noteId];
if (!title) {
throw new Error("Can't find title for noteId='" + noteId + "'");
throwError("Can't find title for noteId='" + noteId + "'");
}
if (parentNoteId !== null) {
@ -265,7 +265,7 @@ const noteTree = (function() {
const parents = childToParents[noteId];
if (!parents) {
throw new Error("Can't find parents for noteId=" + noteId);
throwError("Can't find parents for noteId=" + noteId);
}
if (parents.length <= 1) {
@ -278,7 +278,9 @@ const noteTree = (function() {
const list = $("<ul/>");
for (const parentNoteId of parents) {
const notePath = getSomeNotePath(parentNoteId) + '/' + noteId;
const parentNotePath = getSomeNotePath(parentNoteId);
// this is to avoid having root notes leading '/'
const notePath = parentNotePath ? (parentNotePath + '/' + noteId) : noteId;
const title = getNotePathTitle(notePath);
let item;
@ -303,6 +305,8 @@ const noteTree = (function() {
let parentNoteId = 'root';
for (const noteId of notePath.split('/')) {
console.log('noteId: ' + noteId);
titlePath.push(getNoteTitle(noteId, parentNoteId));
parentNoteId = noteId;
@ -403,8 +407,8 @@ const noteTree = (function() {
setExpandedToServer(data.node.data.note_tree_id, false);
},
init: (event, data) => {
if (startNoteTreeId) {
activateNode(startNoteTreeId);
if (startNotePath) {
activateNode(startNotePath);
}
else {
showAppIfHidden();
@ -494,10 +498,10 @@ const noteTree = (function() {
function loadTree() {
return server.get('tree').then(resp => {
startNoteTreeId = resp.start_note_tree_id;
startNotePath = resp.start_note_path;
if (document.location.hash) {
startNoteTreeId = document.location.hash.substr(1); // strip initial #
startNotePath = document.location.hash.substr(1); // strip initial #
}
return prepareNoteTree(resp.notes, resp.notes_parent);

View file

@ -0,0 +1,34 @@
$("#setup-form").submit(() => {
const username = $("#username").val();
const password1 = $("#password1").val();
const password2 = $("#password2").val();
if (!username) {
showAlert("Username can't be empty");
return false;
}
if (!password1) {
showAlert("Password can't be empty");
return false;
}
if (password1 !== password2) {
showAlert("Both password fields need be identical.");
return false;
}
server.post('setup', {
username: username,
password: password1
}).then(() => {
window.location.replace("/");
});
return false;
});
function showAlert(message) {
$("#alert").html(message);
$("#alert").show();
}

View file

@ -30,6 +30,12 @@ function showError(message) {
});
}
function throwError(message) {
messaging.logError(message);
throw new Error(message);
}
function getDateFromTS(timestamp) {
// Date accepts number of milliseconds since epoch so UTC timestamp works without any extra handling
// see https://stackoverflow.com/questions/4631928/convert-utc-epoch-to-local-date-with-javascript

View file

@ -85,6 +85,10 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit
text-decoration: none;
}
.icon-action {
cursor: pointer;
}
#protect-button, #unprotect-button {
display: none;
}
@ -115,6 +119,17 @@ div.ui-tooltip {
width: auto;
}
#parent-list {
display: none;
margin-left: 20px;
border-top: 2px solid #eee;
padding-top: 10px;
}
#parent-list ul {
padding-left: 20px;
}
#loader-wrapper{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1000;background-color:#fff;opacity:1;transition:opacity 2s ease}
#loader{display:block;position:relative;left:50%;top:50%;width:150px;height:150px;margin:-75px 0 0 -75px;border-radius:50%;border:3px solid transparent;border-top-color:#777;-webkit-animation:spin 2s linear infinite;animation:spin 2s linear infinite}
#loader:before{content:"";position:absolute;top:5px;left:5px;right:5px;bottom:5px;border-radius:50%;border:3px solid transparent;border-top-color:#aaa;-webkit-animation:spin 3s linear infinite;animation:spin 3s linear infinite}

View file

@ -9,6 +9,7 @@ const source_id = require('../../services/source_id');
const auth = require('../../services/auth');
const password_encryption = require('../../services/password_encryption');
const protected_session = require('../../services/protected_session');
const app_info = require('../../services/app_info');
router.post('/sync', async (req, res, next) => {
const timestamp = req.body.timestamp;
@ -22,9 +23,9 @@ router.post('/sync', async (req, res, next) => {
const dbVersion = req.body.dbVersion;
if (dbVersion !== migration.APP_DB_VERSION) {
if (dbVersion !== app_info.db_version) {
res.status(400);
res.send({ message: 'Non-matching db versions, local is version ' + migration.APP_DB_VERSION });
res.send({ message: 'Non-matching db versions, local is version ' + app_info.db_version });
}
const documentSecret = await options.getOption('document_secret');

View file

@ -5,11 +5,12 @@ const router = express.Router();
const auth = require('../../services/auth');
const options = require('../../services/options');
const migration = require('../../services/migration');
const app_info = require('../../services/app_info');
router.get('', auth.checkApiAuthForMigrationPage, async (req, res, next) => {
res.send({
db_version: parseInt(await options.getOption('db_version')),
app_db_version: migration.APP_DB_VERSION
app_db_version: app_info.db_version
});
});

View file

@ -26,7 +26,7 @@ router.put('/:noteTreeId/:notePath', auth.checkApiAuth, async (req, res, next) =
await sync_table.addRecentNoteSync(noteTreeId);
await options.setOption('start_note_tree_id', notePath);
await options.setOption('start_note_path', notePath);
});
res.send(await getRecentNotes());

32
routes/api/setup.js Normal file
View file

@ -0,0 +1,32 @@
"use strict";
const express = require('express');
const router = express.Router();
const auth = require('../../services/auth');
const options = require('../../services/options');
const sql = require('../../services/sql');
const utils = require('../../services/utils');
const my_scrypt = require('../../services/my_scrypt');
const password_encryption = require('../../services/password_encryption');
router.post('', auth.checkAppNotInitialized, async (req, res, next) => {
const { username, password } = req.body;
await sql.doInTransaction(async () => {
await options.setOption('username', username);
await options.setOption('password_verification_salt', utils.randomSecureToken(32));
await options.setOption('password_derived_key_salt', utils.randomSecureToken(32));
const passwordVerificationKey = utils.toBase64(await my_scrypt.getVerificationHash(password));
await options.setOption('password_verification_hash', passwordVerificationKey);
await password_encryption.setDataKey(password, utils.randomSecureToken(16));
});
sql.setDbReadyAsResolved();
res.send({});
});
module.exports = router;

View file

@ -31,7 +31,7 @@ router.get('/', auth.checkApiAuth, async (req, res, next) => {
res.send({
notes: notes,
start_note_tree_id: await options.getOption('start_note_tree_id')
start_note_path: await options.getOption('start_note_path')
});
});

View file

@ -2,6 +2,7 @@ const indexRoute = require('./index');
const loginRoute = require('./login');
const logoutRoute = require('./logout');
const migrationRoute = require('./migration');
const setupRoute = require('./setup');
// API routes
const treeApiRoute = require('./api/tree');
@ -19,12 +20,14 @@ const recentNotesRoute = require('./api/recent_notes');
const appInfoRoute = require('./api/app_info');
const exportRoute = require('./api/export');
const importRoute = require('./api/import');
const setupApiRoute = require('./api/setup');
function register(app) {
app.use('/', indexRoute);
app.use('/login', loginRoute);
app.use('/logout', logoutRoute);
app.use('/migration', migrationRoute);
app.use('/setup', setupRoute);
app.use('/api/tree', treeApiRoute);
app.use('/api/notes', notesApiRoute);
@ -41,6 +44,7 @@ function register(app) {
app.use('/api/app-info', appInfoRoute);
app.use('/api/export', exportRoute);
app.use('/api/import', importRoute);
app.use('/api/setup', setupApiRoute);
}
module.exports = {

11
routes/setup.js Normal file
View file

@ -0,0 +1,11 @@
"use strict";
const express = require('express');
const router = express.Router();
const auth = require('../services/auth');
router.get('', auth.checkAppNotInitialized, (req, res, next) => {
res.render('setup', {});
});
module.exports = router;

92
schema.sql Normal file
View file

@ -0,0 +1,92 @@
CREATE TABLE `migrations` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`name` TEXT NOT NULL,
`version` INTEGER NOT NULL,
`success` INTEGER NOT NULL,
`error` TEXT
);
CREATE TABLE `sync` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entity_name` TEXT NOT NULL,
`entity_id` TEXT NOT NULL,
`sync_date` INTEGER NOT NULL
, source_id TEXT);
CREATE UNIQUE INDEX `IDX_sync_entity_name_id` ON `sync` (
`entity_name`,
`entity_id`
);
CREATE INDEX `IDX_sync_sync_date` ON `sync` (
`sync_date`
);
CREATE TABLE IF NOT EXISTS "options" (
`opt_name` TEXT NOT NULL PRIMARY KEY,
`opt_value` TEXT,
`date_modified` INT
);
CREATE TABLE `event_log` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`note_id` TEXT,
`comment` TEXT,
`date_added` INTEGER NOT NULL
);
CREATE INDEX `IDX_event_log_date_added` ON `event_log` (
`date_added`
);
CREATE TABLE IF NOT EXISTS "notes" (
`note_id` TEXT NOT NULL,
`note_title` TEXT,
`note_text` TEXT,
`date_created` INT,
`date_modified` INT,
`is_protected` INT NOT NULL DEFAULT 0,
`is_deleted` INT NOT NULL DEFAULT 0,
PRIMARY KEY(`note_id`)
);
CREATE INDEX `IDX_notes_is_deleted` ON `notes` (
`is_deleted`
);
CREATE TABLE IF NOT EXISTS "notes_history" (
`note_history_id` TEXT NOT NULL PRIMARY KEY,
`note_id` TEXT NOT NULL,
`note_title` TEXT,
`note_text` TEXT,
`is_protected` INT,
`date_modified_from` INT,
`date_modified_to` INT
);
CREATE INDEX `IDX_notes_history_note_id` ON `notes_history` (
`note_id`
);
CREATE INDEX `IDX_notes_history_note_date_modified_from` ON `notes_history` (
`date_modified_from`
);
CREATE INDEX `IDX_notes_history_note_date_modified_to` ON `notes_history` (
`date_modified_to`
);
CREATE TABLE `source_ids` (
`source_id` TEXT NOT NULL,
`date_created` INTEGER NOT NULL,
PRIMARY KEY(`source_id`)
);
CREATE TABLE IF NOT EXISTS "notes_tree" (
[note_tree_id] VARCHAR(30) PRIMARY KEY NOT NULL,
[note_id] VARCHAR(30) NOT NULL,
[note_pid] VARCHAR(30) NOT NULL,
[note_pos] INTEGER NOT NULL,
[is_expanded] BOOLEAN NULL ,
date_modified INTEGER NOT NULL DEFAULT 0,
is_deleted INTEGER NOT NULL DEFAULT 0
, `prefix` TEXT);
CREATE INDEX `IDX_notes_tree_note_tree_id` ON `notes_tree` (
`note_tree_id`
);
CREATE INDEX `IDX_notes_tree_note_id_note_pid` ON `notes_tree` (
`note_id`,
`note_pid`
);
CREATE TABLE `recent_notes` (
'note_tree_id'TEXT NOT NULL PRIMARY KEY,
`note_path` TEXT NOT NULL,
`date_accessed` INTEGER NOT NULL ,
is_deleted INT
);

View file

@ -2,11 +2,12 @@
const build = require('./build');
const packageJson = require('../package');
const migration = require('./migration');
const APP_DB_VERSION = 49;
module.exports = {
app_version: packageJson.version,
db_version: migration.APP_DB_VERSION,
db_version: APP_DB_VERSION,
build_date: build.build_date,
build_revision: build.build_revision
};

View file

@ -2,16 +2,22 @@
const migration = require('./migration');
const utils = require('./utils');
const options = require('./options');
async function checkAuth(req, res, next) {
if (!req.session.loggedIn && !utils.isElectron()) {
const username = await options.getOption('username');
if (!username) {
res.redirect("setup");
}
else if (!req.session.loggedIn && !utils.isElectron()) {
res.redirect("login");
}
else if (await migration.isDbUpToDate()) {
next();
else if (!await migration.isDbUpToDate()) {
res.redirect("migration");
}
else {
res.redirect("migration");
next();
}
}
@ -45,9 +51,21 @@ async function checkApiAuthForMigrationPage(req, res, next) {
}
}
async function checkAppNotInitialized(req, res, next) {
const username = await options.getOption('username');
if (username) {
res.status(400).send("App already initialized.");
}
else {
next();
}
}
module.exports = {
checkAuth,
checkAuthForMigrationPage,
checkApiAuth,
checkApiAuthForMigrationPage
checkApiAuthForMigrationPage,
checkAppNotInitialized
};

View file

@ -58,10 +58,12 @@ if (!fs.existsSync(dataDir.BACKUP_DIR)) {
fs.mkdirSync(dataDir.BACKUP_DIR, 0o700);
}
setInterval(regularBackup, 60 * 60 * 1000);
sql.dbReady.then(() => {
setInterval(regularBackup, 60 * 60 * 1000);
// kickoff backup immediately
setTimeout(regularBackup, 1000);
// kickoff backup immediately
setTimeout(regularBackup, 1000);
});
module.exports = {
backupNow

View file

@ -1 +1 @@
module.exports = { build_date:"2017-11-30T00:11:04-05:00", build_revision: "719f5530544efa1d7aae16afd8a9e64db04ff206" };
module.exports = { build_date:"2017-12-06T21:44:45-05:00", build_revision: "4f47c4d6e919aefd303617ac459cea41a1761385" };

View file

@ -1,15 +1,15 @@
"use strict";
const fs = require('fs');
const LOG_DIR = require('./data_dir').LOG_DIR;
const data_dir = require('./data_dir');
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, 0o700);
if (!fs.existsSync(data_dir.LOG_DIR)) {
fs.mkdirSync(data_dir.LOG_DIR, 0o700);
}
const logger = require('simple-node-logger').createRollingFileLogger({
errorEventName: 'error',
logDirectory: LOG_DIR,
logDirectory: data_dir.LOG_DIR,
fileNamePattern: 'trilium-<DATE>.log',
dateFormat:'YYYY-MM-DD'
});
@ -37,6 +37,8 @@ function request(req) {
logger.info(req.method + " " + req.url);
}
info("Using data dir: " + data_dir.TRILIUM_DATA_DIR);
module.exports = {
info,
error,

View file

@ -3,9 +3,15 @@ const sql = require('./sql');
const options = require('./options');
const fs = require('fs-extra');
const log = require('./log');
const app_info = require('./app_info');
const path = require('path');
const APP_DB_VERSION = 48;
const MIGRATIONS_DIR = "migrations";
const MIGRATIONS_DIR = path.resolve(__dirname, "..", "migrations");
if (!fs.existsSync(MIGRATIONS_DIR)) {
log.error("Could not find migration directory: " + MIGRATIONS_DIR);
process.exit(1);
}
async function migrate() {
const migrations = [];
@ -84,11 +90,10 @@ async function migrate() {
async function isDbUpToDate() {
const dbVersion = parseInt(await options.getOption('db_version'));
return dbVersion >= APP_DB_VERSION;
return dbVersion >= app_info.db_version;
}
module.exports = {
migrate,
isDbUpToDate,
APP_DB_VERSION
isDbUpToDate
};

View file

@ -81,6 +81,12 @@ async function protectNoteRecursively(noteId, dataKey, protect) {
}
}
function decryptNote(note, dataKey) {
note.note_title = data_encryption.decryptString(dataKey, data_encryption.noteTitleIv(note.note_id), note.note_title);
note.note_text = data_encryption.decryptString(dataKey, data_encryption.noteTextIv(note.note_id), note.note_text);
note.is_protected = false;
}
async function protectNote(note, dataKey, protect) {
let changed = false;
@ -92,9 +98,7 @@ async function protectNote(note, dataKey, protect) {
changed = true;
}
else if (!protect && note.is_protected) {
note.note_title = data_encryption.decryptString(dataKey, data_encryption.noteTitleIv(note.note_id), note.note_title);
note.note_text = data_encryption.decryptString(dataKey, data_encryption.noteTextIv(note.note_id), note.note_text);
note.is_protected = false;
decryptNote(note, dataKey);
changed = true;
}
@ -134,9 +138,6 @@ async function protectNoteHistory(noteId, dataKey, protect) {
}
async function updateNote(noteId, newNote, ctx) {
let noteTitleForHistory = newNote.detail.note_title;
let noteTextForHistory = newNote.detail.note_text;
if (newNote.detail.is_protected) {
await encryptNote(newNote, ctx);
}
@ -147,19 +148,26 @@ async function updateNote(noteId, newNote, ctx) {
const historyCutoff = now - historySnapshotTimeInterval;
const existingNoteHistoryId = await sql.getSingleValue("SELECT note_history_id FROM notes_history WHERE note_id = ? AND date_modified_from >= ?", [noteId, historyCutoff]);
const existingNoteHistoryId = await sql.getSingleValue("SELECT note_history_id FROM notes_history WHERE note_id = ? AND date_modified_to >= ?", [noteId, historyCutoff]);
await sql.doInTransaction(async () => {
if (!existingNoteHistoryId && (now - newNote.detail.date_created) >= historySnapshotTimeInterval) {
const oldNote = await sql.getSingleResult("SELECT * FROM notes WHERE note_id = ?", [noteId]);
if (oldNote.is_protected) {
decryptNote(oldNote, ctx.getDataKey());
}
const newNoteHistoryId = utils.newNoteHistoryId();
await sql.insert('notes_history', {
note_history_id: newNoteHistoryId,
note_id: noteId,
note_title: noteTitleForHistory,
note_text: noteTextForHistory,
is_protected: false, // we don't care about encryption - this will be handled in protectNoteHistory()
date_modified_from: now,
// title and text should be decrypted now
note_title: oldNote.note_title,
note_text: oldNote.note_text,
is_protected: 0, // will be fixed in the protectNoteHistory() call
date_modified_from: oldNote.date_modified,
date_modified_to: now
});

View file

@ -1,6 +1,7 @@
const sql = require('./sql');
const utils = require('./utils');
const sync_table = require('./sync_table');
const app_info = require('./app_info');
const SYNCED_OPTIONS = [ 'username', 'password_verification_hash', 'encrypted_data_key', 'protected_session_timeout',
'history_snapshot_time_interval' ];
@ -20,21 +21,38 @@ async function setOption(optName, optValue) {
await sync_table.addOptionsSync(optName);
}
await sql.execute("UPDATE options SET opt_value = ?, date_modified = ? WHERE opt_name = ?",
[optValue, utils.nowTimestamp(), optName]);
await sql.replace("options", {
opt_name: optName,
opt_value: optValue,
date_modified: utils.nowTimestamp()
});
}
sql.dbReady.then(async () => {
if (!await getOption('document_id') || !await getOption('document_secret')) {
await sql.doInTransaction(async () => {
async function initOptions(startNotePath) {
await setOption('document_id', utils.randomSecureToken(16));
await setOption('document_secret', utils.randomSecureToken(16));
});
}
});
await setOption('username', '');
await setOption('password_verification_hash', '');
await setOption('password_verification_salt', '');
await setOption('password_derived_key_salt', '');
await setOption('encrypted_data_key', '');
await setOption('encrypted_data_key_iv', '');
await setOption('start_note_path', startNotePath);
await setOption('protected_session_timeout', 600);
await setOption('history_snapshot_time_interval', 600);
await setOption('last_backup_date', utils.nowTimestamp());
await setOption('db_version', app_info.db_version);
await setOption('last_synced_pull', app_info.db_version);
await setOption('last_synced_push', 0);
await setOption('last_synced_push', 0);
}
module.exports = {
getOption,
setOption,
initOptions,
SYNCED_OPTIONS
};

View file

@ -8,7 +8,7 @@ const sync = require('./sync');
let startTime = utils.nowTimestamp();
let sentSyncId = [];
setInterval(async () => {
async function sendPing() {
const syncs = await sql.getResults("SELECT * FROM sync WHERE sync_date >= ? AND source_id != ?", [startTime, source_id.currentSourceId]);
startTime = utils.nowTimestamp();
@ -41,4 +41,6 @@ setInterval(async () => {
for (const syncId of syncIds) {
sentSyncId.push(syncId);
}
}, 1000);
}
sql.dbReady.then(() => setInterval(sendPing, 1000));

View file

@ -2,13 +2,74 @@
const log = require('./log');
const dataDir = require('./data_dir');
const fs = require('fs');
const sqlite = require('sqlite');
const utils = require('./utils');
async function createConnection() {
return await sqlite.open(dataDir.DOCUMENT_PATH, {Promise});
}
const dbReady = createConnection();
const dbConnected = createConnection();
let dbReadyResolve = null;
const dbReady = new Promise((resolve, reject) => {
dbConnected.then(async db => {
dbReadyResolve = () => resolve(db);
const tableResults = await getResults("SELECT name FROM sqlite_master WHERE type='table' AND name='notes'");
if (tableResults.length !== 1) {
log.info("Connected to db, but schema doesn't exist. Initializing schema ...");
const schema = fs.readFileSync('schema.sql', 'UTF-8');
await doInTransaction(async () => {
await executeScript(schema);
const noteId = utils.newNoteId();
await insert('notes_tree', {
note_tree_id: utils.newNoteTreeId(),
note_id: noteId,
note_pid: 'root',
note_pos: 1,
is_deleted: 0,
date_modified: utils.nowTimestamp()
});
await insert('notes', {
note_id: noteId,
note_title: 'Welcome to Trilium!',
note_text: 'Text',
is_protected: 0,
is_deleted: 0,
date_created: utils.nowTimestamp(),
date_modified: utils.nowTimestamp()
});
await require('./options').initOptions(noteId);
});
// we don't resolve dbReady promise because user needs to setup the username and password to initialize
// the database
}
else {
const username = await getSingleValue("SELECT opt_value FROM options WHERE opt_name = 'username'");
if (username) {
resolve(db);
}
}
})
.catch(e => {
console.log("Error connecting to DB.", e);
process.exit(1);
});
});
function setDbReadyAsResolved() {
dbReadyResolve();
}
async function insert(table_name, rec, replace = false) {
const keys = Object.keys(rec);
@ -44,13 +105,10 @@ async function rollback() {
}
async function getSingleResult(query, params = []) {
const db = await dbReady;
return await wrap(async db => db.get(query, ...params));
}
async function getSingleResultOrNull(query, params = []) {
const db = await dbReady;
const all = await wrap(async db => db.all(query, ...params));
return all.length > 0 ? all[0] : null;
@ -67,8 +125,6 @@ async function getSingleValue(query, params = []) {
}
async function getResults(query, params = []) {
const db = await dbReady;
return await wrap(async db => db.all(query, ...params));
}
@ -106,7 +162,7 @@ async function executeScript(query) {
async function wrap(func) {
const thisError = new Error();
const db = await dbReady;
const db = await dbConnected;
try {
return await func(db);
@ -157,20 +213,6 @@ async function doInTransaction(func) {
}
}
dbReady
.then(async () => {
const tableResults = await getResults("SELECT name FROM sqlite_master WHERE type='table' AND name='notes'");
if (tableResults.length !== 1) {
console.log("No connection to initialized DB.");
process.exit(1);
}
})
.catch(e => {
console.log("Error connecting to DB.", e);
process.exit(1);
});
module.exports = {
dbReady,
insert,
@ -183,5 +225,6 @@ module.exports = {
getFlattenedResults,
execute,
executeScript,
doInTransaction
doInTransaction,
setDbReadyAsResolved
};

View file

@ -13,6 +13,7 @@ const syncUpdate = require('./sync_update');
const content_hash = require('./content_hash');
const event_log = require('./event_log');
const fs = require('fs');
const app_info = require('./app_info');
const SYNC_SERVER = config['Sync']['syncServerHost'];
const isSyncSetup = !!SYNC_SERVER;
@ -94,7 +95,7 @@ async function login() {
const resp = await syncRequest(syncContext, 'POST', '/api/login/sync', {
timestamp: timestamp,
dbVersion: migration.APP_DB_VERSION,
dbVersion: app_info.db_version,
hash: hash
});
@ -305,7 +306,8 @@ async function syncRequest(syncContext, method, uri, body) {
}
}
if (isSyncSetup) {
sql.dbReady.then(() => {
if (isSyncSetup) {
log.info("Setting up sync to " + SYNC_SERVER + " with timeout " + SYNC_TIMEOUT);
if (SYNC_PROXY) {
@ -324,10 +326,11 @@ if (isSyncSetup) {
// kickoff initial sync immediately
setTimeout(sync, 1000);
}
else {
}
else {
log.info("Sync server not configured, sync timer not running.")
}
}
});
module.exports = {
sync,

View file

@ -3,7 +3,7 @@
const crypto = require('crypto');
function newNoteId() {
return randomString(8);
return randomString(12);
}
function newNoteTreeId() {
@ -14,16 +14,10 @@ function newNoteHistoryId() {
return randomString(12);
}
const ALPHA_NUMERIC = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
function randomString(length) {
let result = '';
const token = randomSecureToken(32);
for (let i = length; i > 0; --i) {
result += ALPHA_NUMERIC[Math.floor(Math.random() * ALPHA_NUMERIC.length)];
}
return result;
return token.substr(0, length);
}
function randomSecureToken(bytes = 32) {

View file

@ -33,7 +33,8 @@
</div>
</div>
<div class="hide-toggle" style="grid-area: tree-actions">
<div class="hide-toggle" style="grid-area: tree-actions;">
<div style="display: flex; justify-content: space-evenly; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;">
<a onclick="noteTree.createNewTopLevelNote()" title="Create new top level note" class="icon-action">
<img src="images/icons/file-plus.png" alt="Create new top level note"/>
</a>
@ -49,6 +50,7 @@
<a onclick="searchTree.toggleSearch()" title="Search in notes" class="icon-action">
<img src="images/icons/search.png" alt="Search in notes"/>
</a>
</div>
<div id="search-box" style="display: none; padding: 10px; margin-top: 10px;">
<p>
@ -63,7 +65,7 @@
<div id="tree" class="hide-toggle" style="grid-area: tree; overflow: auto;">
</div>
<div id="parent-list" style="display: none;">
<div id="parent-list">
</div>
<div class="hide-toggle" style="grid-area: title;">

48
views/setup.ejs Normal file
View file

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Setup</title>
</head>
<body>
<div style="width: 500px; margin: auto;">
<h1>Trilium setup</h1>
<div class="alert alert-warning" id="alert" style="display: none;">
</div>
<form id="setup-form">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" placeholder="Arbitrary string">
</div>
<div class="form-group">
<label for="password1">Password</label>
<input type="password" class="form-control" id="password1" placeholder="Password">
</div>
<div class="form-group">
<label for="password2">Repeat password</label>
<input type="password" class="form-control" id="password2" placeholder="Password">
</div>
<button type="submit" class="btn btn-default">Save</button>
</form>
</div>
<script type="text/javascript">
const baseApiUrl = 'api/';
</script>
<!-- Required for correct loading of scripts in Electron -->
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<script src="libraries/jquery.min.js"></script>
<link href="libraries/bootstrap/css/bootstrap.css" rel="stylesheet">
<script src="libraries/bootstrap/js/bootstrap.js"></script>
<script src="javascripts/setup.js"></script>
<script src="javascripts/utils.js"></script>
<script src="javascripts/server.js"></script>
</body>
</html>