mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	#98, working sync setup from server to desktop instance + refactoring of DB initialization
This commit is contained in:
		| @@ -1,4 +1,3 @@ | |||||||
| import server from './services/server.js'; |  | ||||||
| import utils from "./services/utils.js"; | import utils from "./services/utils.js"; | ||||||
|  |  | ||||||
| function SetupModel() { | function SetupModel() { | ||||||
| @@ -56,7 +55,8 @@ function SetupModel() { | |||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             server.post('setup', { |             // not using server.js because it loads too many dependencies | ||||||
|  |             $.post('/api/setup/new-document', { | ||||||
|                 username: username, |                 username: username, | ||||||
|                 password: password1 |                 password: password1 | ||||||
|             }).then(() => { |             }).then(() => { | ||||||
| @@ -83,7 +83,17 @@ function SetupModel() { | |||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             showAlert("All OK"); |             // not using server.js because it loads too many dependencies | ||||||
|  |             $.post('/api/setup/sync-from-server', { | ||||||
|  |                 serverAddress: serverAddress, | ||||||
|  |                 username: username, | ||||||
|  |                 password: password | ||||||
|  |             }).then(() => { | ||||||
|  |                 window.location.replace("/"); | ||||||
|  |             }).catch((err) => { | ||||||
|  |                 alert("Error, see dev console for details."); | ||||||
|  |                 console.error(err); | ||||||
|  |             }); | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,13 +1,74 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const sqlInit = require('../../services/sql_init'); | const sqlInit = require('../../services/sql_init'); | ||||||
|  | const sql = require('../../services/sql'); | ||||||
|  | const cls = require('../../services/cls'); | ||||||
|  | const tmp = require('tmp-promise'); | ||||||
|  | const http = require('http'); | ||||||
|  | const fs = require('fs'); | ||||||
|  | const log = require('../../services/log'); | ||||||
|  | const DOCUMENT_PATH = require('../../services/data_dir').DOCUMENT_PATH; | ||||||
|  | const sourceIdService = require('../../services/source_id'); | ||||||
|  | const url = require('url'); | ||||||
|  |  | ||||||
| async function setup(req) { | async function setupNewDocument(req) { | ||||||
|     const { username, password } = req.body; |     const { username, password } = req.body; | ||||||
|  |  | ||||||
|     await sqlInit.createInitialDatabase(username, password); |     await sqlInit.createInitialDatabase(username, password); | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | async function setupSyncFromServer(req) { | ||||||
|     setup |     const { serverAddress, username, password } = req.body; | ||||||
|  |  | ||||||
|  |     const tempFile = await tmp.file(); | ||||||
|  |  | ||||||
|  |     await new Promise((resolve, reject) => { | ||||||
|  |         const file = fs.createWriteStream(tempFile.path); | ||||||
|  |         const parsedAddress = url.parse(serverAddress); | ||||||
|  |  | ||||||
|  |         const options = { | ||||||
|  |             method: 'GET', | ||||||
|  |             protocol: parsedAddress.protocol, | ||||||
|  |             host: parsedAddress.hostname, | ||||||
|  |             port: parsedAddress.port, | ||||||
|  |             path: '/api/sync/document', | ||||||
|  |             auth: username + ':' + password | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         log.info("Getting document from: " + serverAddress + JSON.stringify(options)); | ||||||
|  |  | ||||||
|  |         http.request(options, function(response) { | ||||||
|  |             response.pipe(file); | ||||||
|  |  | ||||||
|  |             file.on('finish', function() { | ||||||
|  |                 log.info("Document download finished, closing & renaming."); | ||||||
|  |  | ||||||
|  |                 file.close(() => { // close() is async, call after close completes. | ||||||
|  |                     fs.rename(tempFile.path, DOCUMENT_PATH, async () => { | ||||||
|  |                         cls.reset(); | ||||||
|  |  | ||||||
|  |                         await sqlInit.initDbConnection(); | ||||||
|  |  | ||||||
|  |                         // we need to generate new source ID for this instance, otherwise it will | ||||||
|  |                         // match the original server one | ||||||
|  |                         await sql.transactional(async () => { | ||||||
|  |                             await sourceIdService.generateSourceId(); | ||||||
|  |                         }); | ||||||
|  |  | ||||||
|  |                         resolve(); | ||||||
|  |                     }); | ||||||
|  |                 }); | ||||||
|  |             }); | ||||||
|  |         }).on('error', function(err) { // Handle errors | ||||||
|  |             fs.unlink(tempFile.path); // Delete the file async. (But we don't check the result) | ||||||
|  |  | ||||||
|  |             reject(err.message); | ||||||
|  |             log.error(err.message); | ||||||
|  |         }).end(); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     setupNewDocument, | ||||||
|  |     setupSyncFromServer | ||||||
| }; | }; | ||||||
| @@ -7,6 +7,7 @@ const sql = require('../../services/sql'); | |||||||
| const optionService = require('../../services/options'); | const optionService = require('../../services/options'); | ||||||
| const contentHashService = require('../../services/content_hash'); | const contentHashService = require('../../services/content_hash'); | ||||||
| const log = require('../../services/log'); | const log = require('../../services/log'); | ||||||
|  | const DOCUMENT_PATH = require('../../services/data_dir').DOCUMENT_PATH; | ||||||
|  |  | ||||||
| async function checkSync() { | async function checkSync() { | ||||||
|     return { |     return { | ||||||
| @@ -72,6 +73,12 @@ async function update(req) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function getDocument(req, resp) { | ||||||
|  |     log.info("Serving document."); | ||||||
|  |  | ||||||
|  |     resp.sendFile(DOCUMENT_PATH); | ||||||
|  | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     checkSync, |     checkSync, | ||||||
|     syncNow, |     syncNow, | ||||||
| @@ -79,5 +86,6 @@ module.exports = { | |||||||
|     forceFullSync, |     forceFullSync, | ||||||
|     forceNoteSync, |     forceNoteSync, | ||||||
|     getChanged, |     getChanged, | ||||||
|     update |     update, | ||||||
|  |     getDocument | ||||||
| }; | }; | ||||||
| @@ -62,16 +62,21 @@ function apiRoute(method, path, routeHandler) { | |||||||
|     route(method, path, [auth.checkApiAuth], routeHandler, apiResultHandler); |     route(method, path, [auth.checkApiAuth], routeHandler, apiResultHandler); | ||||||
| } | } | ||||||
|  |  | ||||||
| function route(method, path, middleware, routeHandler, resultHandler) { | function route(method, path, middleware, routeHandler, resultHandler, transactional = true) { | ||||||
|     router[method](path, ...middleware, async (req, res, next) => { |     router[method](path, ...middleware, async (req, res, next) => { | ||||||
|         try { |         try { | ||||||
|             const result = await cls.init(async () => { |             const result = await cls.init(async () => { | ||||||
|                 cls.namespace.set('sourceId', req.headers.source_id); |                 cls.namespace.set('sourceId', req.headers.source_id); | ||||||
|                 protectedSessionService.setProtectedSessionId(req); |                 protectedSessionService.setProtectedSessionId(req); | ||||||
|  |  | ||||||
|  |                 if (transactional) { | ||||||
|                     return await sql.transactional(async () => { |                     return await sql.transactional(async () => { | ||||||
|                         return await routeHandler(req, res, next); |                         return await routeHandler(req, res, next); | ||||||
|                     }); |                     }); | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     return await routeHandler(req, res, next); | ||||||
|  |                 } | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             if (resultHandler) { |             if (resultHandler) { | ||||||
| @@ -149,6 +154,7 @@ function register(app) { | |||||||
|     apiRoute(POST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync); |     apiRoute(POST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync); | ||||||
|     apiRoute(GET, '/api/sync/changed', syncApiRoute.getChanged); |     apiRoute(GET, '/api/sync/changed', syncApiRoute.getChanged); | ||||||
|     apiRoute(PUT, '/api/sync/update', syncApiRoute.update); |     apiRoute(PUT, '/api/sync/update', syncApiRoute.update); | ||||||
|  |     route(GET, '/api/sync/document', [auth.checkBasicAuth], syncApiRoute.getDocument); | ||||||
|  |  | ||||||
|     apiRoute(GET, '/api/event-log', eventLogRoute.getEventLog); |     apiRoute(GET, '/api/event-log', eventLogRoute.getEventLog); | ||||||
|  |  | ||||||
| @@ -156,7 +162,8 @@ function register(app) { | |||||||
|     apiRoute(PUT, '/api/recent-notes/:branchId/:notePath', recentNotesRoute.addRecentNote); |     apiRoute(PUT, '/api/recent-notes/:branchId/:notePath', recentNotesRoute.addRecentNote); | ||||||
|     apiRoute(GET, '/api/app-info', appInfoRoute.getAppInfo); |     apiRoute(GET, '/api/app-info', appInfoRoute.getAppInfo); | ||||||
|  |  | ||||||
|     route(POST, '/api/setup', [auth.checkAppNotInitialized], setupApiRoute.setup, apiResultHandler); |     route(POST, '/api/setup/new-document', [auth.checkAppNotInitialized], setupApiRoute.setupNewDocument, apiResultHandler); | ||||||
|  |     route(POST, '/api/setup/sync-from-server', [auth.checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler, false); | ||||||
|  |  | ||||||
|     apiRoute(POST, '/api/sql/execute', sqlRoute.execute); |     apiRoute(POST, '/api/sql/execute', sqlRoute.execute); | ||||||
|     apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize); |     apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize); | ||||||
|   | |||||||
| @@ -1,12 +1,13 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const migrationService = require('./migration'); |  | ||||||
| const sql = require('./sql'); | const sql = require('./sql'); | ||||||
| const sqlInit = require('./sql_init'); | const sqlInit = require('./sql_init'); | ||||||
| const utils = require('./utils'); | const utils = require('./utils'); | ||||||
|  | const passwordEncryptionService = require('./password_encryption'); | ||||||
|  | const optionService = require('./options'); | ||||||
|  |  | ||||||
| async function checkAuth(req, res, next) { | async function checkAuth(req, res, next) { | ||||||
|     if (!await sqlInit.isUserInitialized()) { |     if (!await sqlInit.isDbInitialized()) { | ||||||
|         res.redirect("setup"); |         res.redirect("setup"); | ||||||
|     } |     } | ||||||
|     else if (!req.session.loggedIn && !utils.isElectron()) { |     else if (!req.session.loggedIn && !utils.isElectron()) { | ||||||
| @@ -38,7 +39,7 @@ async function checkApiAuth(req, res, next) { | |||||||
| } | } | ||||||
|  |  | ||||||
| async function checkAppNotInitialized(req, res, next) { | async function checkAppNotInitialized(req, res, next) { | ||||||
|     if (await sqlInit.isUserInitialized()) { |     if (await sqlInit.isDbInitialized()) { | ||||||
|         res.status(400).send("App already initialized."); |         res.status(400).send("App already initialized."); | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
| @@ -57,10 +58,27 @@ async function checkSenderToken(req, res, next) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function checkBasicAuth(req, res, next) { | ||||||
|  |     const header = req.headers.authorization || ''; | ||||||
|  |     const token = header.split(/\s+/).pop() || ''; | ||||||
|  |     const auth = new Buffer.from(token, 'base64').toString(); | ||||||
|  |     const [username, password] = auth.split(/:/); | ||||||
|  |  | ||||||
|  |     const dbUsername = await optionService.getOption('username'); | ||||||
|  |  | ||||||
|  |     if (dbUsername !== username || !await passwordEncryptionService.verifyPassword(password)) { | ||||||
|  |         res.status(401).send("Not authorized"); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         next(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     checkAuth, |     checkAuth, | ||||||
|     checkApiAuth, |     checkApiAuth, | ||||||
|     checkAppNotInitialized, |     checkAppNotInitialized, | ||||||
|     checkApiAuthOrElectron, |     checkApiAuthOrElectron, | ||||||
|     checkSenderToken |     checkSenderToken, | ||||||
|  |     checkBasicAuth | ||||||
| }; | }; | ||||||
| @@ -13,9 +13,14 @@ function getSourceId() { | |||||||
|     return namespace.get('sourceId'); |     return namespace.get('sourceId'); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function reset() { | ||||||
|  |     clsHooked.reset(); | ||||||
|  | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     init, |     init, | ||||||
|     wrap, |     wrap, | ||||||
|     namespace, |     namespace, | ||||||
|     getSourceId |     getSourceId, | ||||||
|  |     reset | ||||||
| }; | }; | ||||||
| @@ -86,7 +86,7 @@ async function migrate() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (sqlInit.isDbUpToDate()) { |     if (sqlInit.isDbUpToDate()) { | ||||||
|         sqlInit.setDbReadyAsResolved(); |         await sqlInit.initDbConnection(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return migrations; |     return migrations; | ||||||
|   | |||||||
| @@ -157,10 +157,10 @@ async function transactional(func) { | |||||||
|     transactionActive = true; |     transactionActive = true; | ||||||
|     transactionPromise = new Promise(async (resolve, reject) => { |     transactionPromise = new Promise(async (resolve, reject) => { | ||||||
|         try { |         try { | ||||||
|             cls.namespace.set('isInTransaction', true); |  | ||||||
|  |  | ||||||
|             await beginTransaction(); |             await beginTransaction(); | ||||||
|  |  | ||||||
|  |             cls.namespace.set('isInTransaction', true); | ||||||
|  |  | ||||||
|             ret = await func(); |             ret = await func(); | ||||||
|  |  | ||||||
|             await commit(); |             await commit(); | ||||||
|   | |||||||
| @@ -11,38 +11,32 @@ async function createConnection() { | |||||||
|     return await sqlite.open(dataDir.DOCUMENT_PATH, {Promise}); |     return await sqlite.open(dataDir.DOCUMENT_PATH, {Promise}); | ||||||
| } | } | ||||||
|  |  | ||||||
| let schemaReadyResolve = null; |  | ||||||
| const schemaReady = new Promise((resolve, reject) => schemaReadyResolve = resolve); |  | ||||||
|  |  | ||||||
| let dbReadyResolve = null; | let dbReadyResolve = null; | ||||||
| const dbReady = new Promise((resolve, reject) => { | const dbReady = new Promise((resolve, reject) => { | ||||||
|     cls.init(async () => { |     dbReadyResolve = resolve; | ||||||
|  |  | ||||||
|  |     initDbConnection(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | async function isDbInitialized() { | ||||||
|  |     const tableResults = await sql.getRows("SELECT name FROM sqlite_master WHERE type='table' AND name='notes'"); | ||||||
|  |  | ||||||
|  |     return tableResults.length === 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function initDbConnection() { | ||||||
|  |     await cls.init(async () => { | ||||||
|         const db = await createConnection(); |         const db = await createConnection(); | ||||||
|         sql.setDbConnection(db); |         sql.setDbConnection(db); | ||||||
|  |  | ||||||
|         await sql.execute("PRAGMA foreign_keys = ON"); |         await sql.execute("PRAGMA foreign_keys = ON"); | ||||||
|  |  | ||||||
|         dbReadyResolve = () => { |         if (isDbInitialized()) { | ||||||
|             log.info("DB ready."); |  | ||||||
|  |  | ||||||
|             resolve(db); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         const tableResults = await sql.getRows("SELECT name FROM sqlite_master WHERE type='table' AND name='notes'"); |  | ||||||
|         if (tableResults.length !== 1) { |  | ||||||
|             log.info("DB not found, please visit setup page to initialize Trilium."); |             log.info("DB not found, please visit setup page to initialize Trilium."); | ||||||
|  |  | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         schemaReadyResolve(); |  | ||||||
|  |  | ||||||
|         if (!await isUserInitialized()) { |  | ||||||
|             log.info("Login/password not initialized. DB not ready."); |  | ||||||
|  |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (!await isDbUpToDate()) { |         if (!await isDbUpToDate()) { | ||||||
|             // avoiding circular dependency |             // avoiding circular dependency | ||||||
|             const migrationService = require('./migration'); |             const migrationService = require('./migration'); | ||||||
| @@ -50,9 +44,10 @@ const dbReady = new Promise((resolve, reject) => { | |||||||
|             await migrationService.migrate(); |             await migrationService.migrate(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         resolve(db); |         log.info("DB ready."); | ||||||
|     }); |         dbReadyResolve(db); | ||||||
|     }); |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
| async function createInitialDatabase(username, password) { | async function createInitialDatabase(username, password) { | ||||||
|     log.info("Connected to db, but schema doesn't exist. Initializing schema ..."); |     log.info("Connected to db, but schema doesn't exist. Initializing schema ..."); | ||||||
| @@ -78,11 +73,7 @@ async function createInitialDatabase(username, password) { | |||||||
|  |  | ||||||
|     log.info("Schema and initial content generated. Waiting for user to enter username/password to finish setup."); |     log.info("Schema and initial content generated. Waiting for user to enter username/password to finish setup."); | ||||||
|  |  | ||||||
|     setDbReadyAsResolved(); |     await initDbConnection(); | ||||||
| } |  | ||||||
|  |  | ||||||
| function setDbReadyAsResolved() { |  | ||||||
|     dbReadyResolve(); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| async function isDbUpToDate() { | async function isDbUpToDate() { | ||||||
| @@ -97,23 +88,10 @@ async function isDbUpToDate() { | |||||||
|     return upToDate; |     return upToDate; | ||||||
| } | } | ||||||
|  |  | ||||||
| async function isUserInitialized() { |  | ||||||
|     const optionsTable = await sql.getRows("SELECT name FROM sqlite_master WHERE type='table' AND name='options'"); |  | ||||||
|  |  | ||||||
|     if (optionsTable.length !== 1) { |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const username = await sql.getValue("SELECT value FROM options WHERE name = 'username'"); |  | ||||||
|  |  | ||||||
|     return !!username; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     dbReady, |     dbReady, | ||||||
|     schemaReady, |     isDbInitialized, | ||||||
|     isUserInitialized, |     initDbConnection, | ||||||
|     setDbReadyAsResolved, |  | ||||||
|     isDbUpToDate, |     isDbUpToDate, | ||||||
|     createInitialDatabase |     createInitialDatabase | ||||||
| }; | }; | ||||||
| @@ -28,7 +28,7 @@ async function setUserNamePassword() { | |||||||
|  |  | ||||||
|     await passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16)); |     await passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16)); | ||||||
|  |  | ||||||
|     sqlInit.setDbReadyAsResolved(); |     await sqlInit.initDbConnection(); | ||||||
| } | } | ||||||
|  |  | ||||||
| const noteCount = parseInt(process.argv[2]); | const noteCount = parseInt(process.argv[2]); | ||||||
| @@ -71,4 +71,4 @@ async function start() { | |||||||
|     process.exit(0); |     process.exit(0); | ||||||
| } | } | ||||||
|  |  | ||||||
| sqlInit.schemaReady.then(cls.wrap(start)); | sqlInit.dbReady.then(cls.wrap(start)); | ||||||
		Reference in New Issue
	
	Block a user