diff --git a/migrations/0043__clear_sync_table.sql b/migrations/0043__clear_sync_table.sql new file mode 100644 index 000000000..147560955 --- /dev/null +++ b/migrations/0043__clear_sync_table.sql @@ -0,0 +1,3 @@ +DELETE FROM sync; + +UPDATE options SET opt_value = 0 WHERE opt_name IN ('last_synced_push', 'last_synced_pull'); \ No newline at end of file diff --git a/routes/api/sync.js b/routes/api/sync.js index 9bddbca20..d6e35df87 100644 --- a/routes/api/sync.js +++ b/routes/api/sync.js @@ -7,6 +7,14 @@ const sync = require('../../services/sync'); const syncUpdate = require('../../services/sync_update'); const sql = require('../../services/sql'); const options = require('../../services/options'); +const content_hash = require('../../services/content_hash'); + +router.get('/check', auth.checkApiAuth, async (req, res, next) => { + res.send({ + 'content_hash': await content_hash.getContentHash(), + 'max_sync_id': await sql.getSingleValue('SELECT MAX(id) FROM sync') + }); +}); router.post('/now', auth.checkApiAuth, async (req, res, next) => { res.send(await sync.sync()); @@ -59,10 +67,10 @@ router.get('/notes_reordering/:noteTreeParentId', auth.checkApiAuth, async (req, }); }); -router.get('/recent_notes/:noteId', auth.checkApiAuth, async (req, res, next) => { - const noteId = req.params.noteId; +router.get('/recent_notes/:notePath', auth.checkApiAuth, async (req, res, next) => { + const notePath = req.params.notePath; - res.send(await sql.getSingleResult("SELECT * FROM recent_notes WHERE note_id = ?", [noteId])); + res.send(await sql.getSingleResult("SELECT * FROM recent_notes WHERE note_path = ?", [notePath])); }); router.put('/notes', auth.checkApiAuth, async (req, res, next) => { diff --git a/services/content_hash.js b/services/content_hash.js new file mode 100644 index 000000000..aaac6eb3e --- /dev/null +++ b/services/content_hash.js @@ -0,0 +1,38 @@ +const sql = require('./sql'); +const utils = require('./utils'); +const options = require('./options'); + +function updateHash(hash, rows) { + for (const row of rows) { + hash = utils.hash(hash + JSON.stringify(row)); + } + + return hash; +} + +async function getContentHash() { + let hash = ''; + + hash = updateHash(hash, await sql.getResults("SELECT note_id, note_title, note_text, date_modified, is_protected, " + + "is_deleted FROM notes ORDER BY note_id")); + + hash = updateHash(hash, await sql.getResults("SELECT note_tree_id, note_id, note_pid, note_pos, date_modified, " + + "is_deleted FROM notes_tree ORDER BY note_tree_id")); + + hash = updateHash(hash, await sql.getResults("SELECT note_history_id, note_id, note_title, note_text, " + + "date_modified_from, date_modified_to FROM notes_history ORDER BY note_history_id")); + + hash = updateHash(hash, await sql.getResults("SELECT note_path, date_accessed, is_deleted FROM recent_notes " + + "ORDER BY note_path")); + + const questionMarks = Array(options.SYNCED_OPTIONS.length).fill('?').join(','); + + hash = updateHash(hash, await sql.getResults("SELECT opt_name, opt_value FROM options " + + "WHERE opt_name IN (" + questionMarks + ") ORDER BY opt_name", options.SYNCED_OPTIONS)); + + return hash; +} + +module.exports = { + getContentHash +}; \ No newline at end of file diff --git a/services/migration.js b/services/migration.js index ab846a126..a1b5f2cf4 100644 --- a/services/migration.js +++ b/services/migration.js @@ -4,7 +4,7 @@ const options = require('./options'); const fs = require('fs-extra'); const log = require('./log'); -const APP_DB_VERSION = 42; +const APP_DB_VERSION = 43; const MIGRATIONS_DIR = "migrations"; async function migrate() { diff --git a/services/sql.js b/services/sql.js index 4fdd2751f..f1e60247c 100644 --- a/services/sql.js +++ b/services/sql.js @@ -130,14 +130,14 @@ async function deleteRecentAudits(category, browserId, noteId) { } async function wrap(func) { + const thisError = new Error(); + const db = await dbReady; try { return await func(db); } catch (e) { - const thisError = new Error(); - log.error("Error executing query. Inner exception: " + e.stack + thisError.stack); throw thisError; diff --git a/services/sync.js b/services/sync.js index 666adc612..d8b62a407 100644 --- a/services/sync.js +++ b/services/sync.js @@ -10,6 +10,8 @@ const config = require('./config'); const source_id = require('./source_id'); const notes = require('./notes'); const syncUpdate = require('./sync_update'); +const content_hash = require('./content_hash'); +const event_log = require('./event_log'); const SYNC_SERVER = config['Sync']['syncServerHost']; const isSyncSetup = !!SYNC_SERVER; @@ -49,6 +51,8 @@ async function sync() { await pushSync(syncContext); + await checkContentHash(syncContext); + return { success: true }; @@ -97,8 +101,12 @@ async function login() { return syncContext; } +async function getLastSyncedPull() { + return parseInt(await options.getOption('last_synced_pull')); +} + async function pullSync(syncContext) { - const lastSyncedPull = parseInt(await options.getOption('last_synced_pull')); + const lastSyncedPull = await getLastSyncedPull(); const changesUri = '/api/sync/changed?lastSyncId=' + lastSyncedPull; @@ -145,8 +153,12 @@ async function pullSync(syncContext) { log.info("Finished pull"); } +async function getLastSyncedPush() { + return parseInt(await options.getOption('last_synced_push')); +} + async function pushSync(syncContext) { - let lastSyncedPush = parseInt(await options.getOption('last_synced_push')); + let lastSyncedPush = await getLastSyncedPush(); while (true) { const sync = await sql.getSingleResultOrNull('SELECT * FROM sync WHERE id > ? LIMIT 1', [lastSyncedPush]); @@ -223,6 +235,34 @@ async function sendEntity(syncContext, entity, entityName) { await syncRequest(syncContext, 'PUT', '/api/sync/' + entityName, payload); } +async function checkContentHash(syncContext) { + const lastSyncedPush = await getLastSyncedPush(); + const notPushedSyncs = await sql.getSingleValue("SELECT COUNT(*) FROM sync WHERE id > ?", [lastSyncedPush]); + + if (notPushedSyncs > 0) { + log.info("There's " + notPushedSyncs + " outstanding pushes, skipping content check."); + + return; + } + + const resp = await syncRequest(syncContext, 'GET', '/api/sync/check'); + + // if (await getLastSyncedPull() < resp.max_sync_id) { + // log.info("There are some outstanding pulls, skipping content check."); + // + // return; + // } + + const localContentHash = await content_hash.getContentHash(); + + if (resp.content_hash === localContentHash) { + log.info("Content hash check passed with value: " + localContentHash); + } + else { + await event_log.addEvent("Content hash check failed. Local is " + localContentHash + ", remote is " + resp.content_hash); + } +} + async function syncRequest(syncContext, method, uri, body) { const fullUri = SYNC_SERVER + uri; diff --git a/services/sync_update.js b/services/sync_update.js index c0e57d162..6ebd5296c 100644 --- a/services/sync_update.js +++ b/services/sync_update.js @@ -112,7 +112,7 @@ async function updateRecentNotes(entity, sourceId) { await sql.doInTransaction(async () => { await sql.replace('recent_notes', entity); - await sync_table.addRecentNoteSync(entity.note_id, sourceId); + await sync_table.addRecentNoteSync(entity.note_path, sourceId); }); } } diff --git a/services/utils.js b/services/utils.js index ba4b9516e..dd45569d1 100644 --- a/services/utils.js +++ b/services/utils.js @@ -66,6 +66,11 @@ function formatTwoTimestamps(origTS, newTS) { return "orig: " + formatDateTimeFromTS(origTS) + ", new: " + formatDateTimeFromTS(newTS); } +function hash(text) { + return crypto.createHash('sha1').update(text).digest('base64'); +} + + module.exports = { randomSecureToken, randomString, @@ -78,5 +83,6 @@ module.exports = { hmac, browserId, isElectron, - formatTwoTimestamps + formatTwoTimestamps, + hash }; \ No newline at end of file