2017-10-21 21:10:33 -04:00
"use strict" ;
2017-10-25 22:39:21 -04:00
const log = require ( './log' ) ;
const rp = require ( 'request-promise' ) ;
const sql = require ( './sql' ) ;
2017-11-02 20:48:02 -04:00
const options = require ( './options' ) ;
2017-10-25 22:39:21 -04:00
const migration = require ( './migration' ) ;
2017-10-26 21:16:21 -04:00
const utils = require ( './utils' ) ;
2017-10-26 23:21:31 -04:00
const config = require ( './config' ) ;
2017-10-31 00:15:49 -04:00
const SOURCE _ID = require ( './source_id' ) ;
2017-11-01 22:36:26 -04:00
const audit _category = require ( './audit_category' ) ;
2017-11-03 22:08:27 -04:00
const eventLog = require ( './event_log' ) ;
2017-11-05 10:41:54 -05:00
const notes = require ( './notes' ) ;
2017-10-22 20:22:09 -04:00
2017-10-26 23:21:31 -04:00
const SYNC _SERVER = config [ 'Sync' ] [ 'syncServerHost' ] ;
2017-11-01 22:36:26 -04:00
const isSyncSetup = ! ! SYNC _SERVER ;
2017-11-09 20:06:33 -05:00
const SYNC _TIMEOUT = config [ 'Sync' ] [ 'syncServerTimeout' ] || 5000 ;
2017-10-25 22:39:21 -04:00
let syncInProgress = false ;
2017-11-03 22:08:27 -04:00
async function pullSync ( syncContext ) {
2017-11-02 20:48:02 -04:00
const lastSyncedPull = parseInt ( await options . getOption ( 'last_synced_pull' ) ) ;
2017-10-26 20:31:31 -04:00
2017-10-31 19:34:58 -04:00
let syncRows ;
2017-10-29 14:55:48 -04:00
try {
2017-10-31 20:09:07 -04:00
logSync ( "Pulling changes: " + SYNC _SERVER + '/api/sync/changed?lastSyncId=' + lastSyncedPull + "&sourceId=" + SOURCE _ID ) ;
2017-10-31 19:34:58 -04:00
syncRows = await rp ( {
uri : SYNC _SERVER + '/api/sync/changed?lastSyncId=' + lastSyncedPull + "&sourceId=" + SOURCE _ID ,
2017-11-01 23:16:21 -04:00
jar : syncContext . cookieJar ,
2017-11-01 00:39:07 -04:00
json : true ,
2017-11-09 20:06:33 -05:00
timeout : SYNC _TIMEOUT
2017-10-29 14:55:48 -04:00
} ) ;
2017-10-31 19:34:58 -04:00
logSync ( "Pulled " + syncRows . length + " changes" ) ;
2017-10-29 14:55:48 -04:00
}
catch ( e ) {
2017-11-03 22:08:27 -04:00
logSyncError ( "Can't pull changes, inner exception: " , e ) ;
2017-10-29 14:55:48 -04:00
}
2017-10-26 20:31:31 -04:00
2017-10-31 19:34:58 -04:00
for ( const sync of syncRows ) {
let resp ;
try {
resp = await rp ( {
uri : SYNC _SERVER + "/api/sync/" + sync . entity _name + "/" + sync . entity _id ,
json : true ,
2017-11-09 20:06:33 -05:00
jar : syncContext . cookieJar ,
timeout : SYNC _TIMEOUT
2017-10-31 19:34:58 -04:00
} ) ;
}
catch ( e ) {
2017-11-03 22:08:27 -04:00
logSyncError ( "Can't pull " + sync . entity _name + " " + sync . entity _id , e ) ;
2017-10-31 19:34:58 -04:00
}
2017-10-31 20:09:07 -04:00
if ( sync . entity _name === 'notes' ) {
2017-11-03 22:08:27 -04:00
await updateNote ( resp . entity , resp . links , syncContext . sourceId ) ;
2017-10-31 20:09:07 -04:00
}
else if ( sync . entity _name === 'notes_tree' ) {
2017-11-03 22:08:27 -04:00
await updateNoteTree ( resp , syncContext . sourceId ) ;
2017-10-31 20:09:07 -04:00
}
else if ( sync . entity _name === 'notes_history' ) {
2017-11-03 22:08:27 -04:00
await updateNoteHistory ( resp , syncContext . sourceId ) ;
2017-10-31 20:09:07 -04:00
}
2017-11-02 22:55:22 -04:00
else if ( sync . entity _name === 'notes_reordering' ) {
2017-11-03 22:08:27 -04:00
await updateNoteReordering ( resp , syncContext . sourceId ) ;
2017-11-02 22:55:22 -04:00
}
2017-11-02 20:48:02 -04:00
else if ( sync . entity _name === 'options' ) {
2017-11-03 22:08:27 -04:00
await updateOptions ( resp , syncContext . sourceId ) ;
2017-11-02 20:48:02 -04:00
}
2017-11-05 00:16:02 -04:00
else if ( sync . entity _name === 'recent_notes' ) {
await updateRecentNotes ( resp , syncContext . sourceId ) ;
}
2017-10-31 20:09:07 -04:00
else {
2017-11-03 22:08:27 -04:00
logSyncError ( "Unrecognized entity type " + sync . entity _name , e ) ;
2017-10-31 20:09:07 -04:00
}
2017-11-02 20:48:02 -04:00
await options . setOption ( 'last_synced_pull' , sync . id ) ;
2017-10-26 20:31:31 -04:00
}
2017-10-31 19:34:58 -04:00
logSync ( "Finished pull" ) ;
2017-10-26 20:31:31 -04:00
}
2017-10-25 22:39:21 -04:00
2017-11-03 22:08:27 -04:00
async function sendEntity ( entity , entityName , cookieJar ) {
2017-10-29 22:22:30 -04:00
try {
const payload = {
2017-10-31 00:15:49 -04:00
sourceId : SOURCE _ID ,
2017-10-29 22:22:30 -04:00
entity : entity
} ;
2017-10-25 22:39:21 -04:00
2017-10-29 22:22:30 -04:00
if ( entityName === 'notes' ) {
payload . links = await sql . getResults ( 'select * from links where note_id = ?' , [ entity . note _id ] ) ;
2017-10-29 14:55:48 -04:00
}
2017-10-29 22:22:30 -04:00
await rp ( {
method : 'PUT' ,
uri : SYNC _SERVER + '/api/sync/' + entityName ,
body : payload ,
json : true ,
2017-11-09 20:06:33 -05:00
timeout : SYNC _TIMEOUT ,
2017-10-29 22:22:30 -04:00
jar : cookieJar
} ) ;
2017-10-29 11:22:41 -04:00
}
2017-10-29 22:22:30 -04:00
catch ( e ) {
2017-11-03 22:08:27 -04:00
logSyncError ( "Failed sending update for entity " + entityName , e ) ;
2017-10-26 21:16:21 -04:00
}
2017-10-29 22:22:30 -04:00
}
2017-11-03 22:08:27 -04:00
async function readAndPushEntity ( sync , syncContext ) {
2017-11-01 23:16:21 -04:00
let entity ;
if ( sync . entity _name === 'notes' ) {
entity = await sql . getSingleResult ( 'SELECT * FROM notes WHERE note_id = ?' , [ sync . entity _id ] ) ;
}
else if ( sync . entity _name === 'notes_tree' ) {
entity = await sql . getSingleResult ( 'SELECT * FROM notes_tree WHERE note_id = ?' , [ sync . entity _id ] ) ;
}
else if ( sync . entity _name === 'notes_history' ) {
entity = await sql . getSingleResult ( 'SELECT * FROM notes_history WHERE note_history_id = ?' , [ sync . entity _id ] ) ;
}
2017-11-02 22:55:22 -04:00
else if ( sync . entity _name === 'notes_reordering' ) {
entity = {
note _pid : sync . entity _id ,
ordering : await sql . getMap ( 'SELECT note_id, note_pos FROM notes_tree WHERE note_pid = ?' , [ sync . entity _id ] )
} ;
}
2017-11-02 20:48:02 -04:00
else if ( sync . entity _name === 'options' ) {
entity = await sql . getSingleResult ( 'SELECT * FROM options WHERE opt_name = ?' , [ sync . entity _id ] ) ;
}
2017-11-05 00:16:02 -04:00
else if ( sync . entity _name === 'recent_notes' ) {
entity = await sql . getSingleResult ( 'SELECT * FROM recent_notes WHERE note_id = ?' , [ sync . entity _id ] ) ;
}
2017-11-01 23:16:21 -04:00
else {
2017-11-03 22:08:27 -04:00
logSyncError ( "Unrecognized entity type " + sync . entity _name , null ) ;
2017-11-01 23:16:21 -04:00
}
2017-11-04 22:10:41 -04:00
if ( ! entity ) {
logSync ( "Sync entity for " + sync . entity _name + " " + sync . entity _id + " doesn't exist. Skipping." ) ;
return ;
}
2017-11-01 23:16:21 -04:00
logSync ( "Pushing changes in " + sync . entity _name + " " + sync . entity _id ) ;
2017-11-03 22:08:27 -04:00
await sendEntity ( entity , sync . entity _name , syncContext . cookieJar ) ;
2017-11-01 23:16:21 -04:00
}
2017-11-03 22:08:27 -04:00
async function pushSync ( syncContext ) {
2017-11-02 20:48:02 -04:00
let lastSyncedPush = parseInt ( await options . getOption ( 'last_synced_push' ) ) ;
2017-10-29 22:22:30 -04:00
while ( true ) {
2017-10-31 19:34:58 -04:00
const sync = await sql . getSingleResultOrNull ( 'SELECT * FROM sync WHERE id > ? LIMIT 1' , [ lastSyncedPush ] ) ;
if ( sync === null ) {
// nothing to sync
2017-11-03 22:08:27 -04:00
logSync ( "Nothing to push" ) ;
2017-10-31 19:34:58 -04:00
2017-10-29 22:22:30 -04:00
break ;
}
2017-11-02 20:48:02 -04:00
if ( sync . source _id === syncContext . sourceId ) {
2017-11-03 22:08:27 -04:00
logSync ( "Skipping sync " + sync . entity _name + " " + sync . entity _id + " because it originates from sync target" ) ;
2017-11-01 00:47:39 -04:00
}
else {
2017-11-03 22:08:27 -04:00
await readAndPushEntity ( sync , syncContext ) ;
2017-11-01 00:47:39 -04:00
}
2017-10-29 22:22:30 -04:00
2017-11-01 00:47:39 -04:00
lastSyncedPush = sync . id ;
2017-10-30 18:44:26 -04:00
2017-11-02 20:48:02 -04:00
await options . setOption ( 'last_synced_push' , lastSyncedPush ) ;
2017-10-29 22:22:30 -04:00
}
2017-10-26 20:31:31 -04:00
}
2017-10-25 22:39:21 -04:00
2017-11-03 22:08:27 -04:00
async function login ( ) {
2017-10-28 22:17:00 -04:00
const timestamp = utils . nowTimestamp ( ) ;
2017-11-02 20:48:02 -04:00
const documentSecret = await options . getOption ( 'document_secret' ) ;
2017-10-29 14:55:48 -04:00
const hash = utils . hmac ( documentSecret , timestamp ) ;
2017-10-28 22:17:00 -04:00
const cookieJar = rp . jar ( ) ;
2017-10-29 14:55:48 -04:00
try {
2017-11-01 23:16:21 -04:00
const resp = await rp ( {
2017-10-29 14:55:48 -04:00
method : 'POST' ,
uri : SYNC _SERVER + '/api/login' ,
body : {
timestamp : timestamp ,
dbVersion : migration . APP _DB _VERSION ,
hash : hash
} ,
json : true ,
2017-11-09 20:06:33 -05:00
timeout : SYNC _TIMEOUT ,
2017-10-29 14:55:48 -04:00
jar : cookieJar
} ) ;
2017-11-01 23:16:21 -04:00
return {
cookieJar : cookieJar ,
sourceId : resp . sourceId
} ;
2017-10-29 14:55:48 -04:00
}
catch ( e ) {
2017-11-03 22:08:27 -04:00
logSyncError ( "Can't login to API for sync, inner exception: " , e ) ;
2017-10-29 14:55:48 -04:00
}
2017-10-28 22:17:00 -04:00
}
2017-10-26 20:31:31 -04:00
async function sync ( ) {
2017-10-29 16:14:59 -04:00
if ( syncInProgress ) {
2017-11-03 22:08:27 -04:00
logSyncError ( "Sync already in progress" ) ;
2017-10-29 16:14:59 -04:00
2017-11-04 21:21:09 -04:00
return {
success : false ,
message : "Sync already in progress"
} ;
2017-10-29 16:14:59 -04:00
}
2017-10-25 22:39:21 -04:00
2017-10-26 20:31:31 -04:00
syncInProgress = true ;
2017-10-26 19:22:21 -04:00
2017-10-26 20:31:31 -04:00
try {
if ( ! await migration . isDbUpToDate ( ) ) {
2017-11-03 22:08:27 -04:00
logSyncError ( "DB not up to date" ) ;
2017-10-29 14:55:48 -04:00
2017-11-04 21:21:09 -04:00
return {
success : false ,
message : "DB not up to date"
} ;
2017-10-25 22:39:21 -04:00
}
2017-11-05 20:37:25 -05:00
let syncContext ;
2017-11-05 17:58:55 -05:00
try {
2017-11-05 20:37:25 -05:00
syncContext = await login ( ) ;
2017-11-05 17:58:55 -05:00
}
catch ( e ) {
if ( e . message . indexOf ( 'ECONNREFUSED' ) !== - 1 ) {
logSync ( "No connection to sync server." ) ;
return {
success : false ,
message : "No connection to sync server."
} ;
}
else {
throw e ;
}
}
2017-11-01 23:16:21 -04:00
2017-11-03 22:08:27 -04:00
await pushSync ( syncContext ) ;
2017-11-02 20:48:02 -04:00
2017-11-03 22:08:27 -04:00
await pullSync ( syncContext ) ;
2017-11-01 20:31:44 -04:00
2017-11-03 22:08:27 -04:00
await pushSync ( syncContext ) ;
2017-11-04 21:21:09 -04:00
return {
success : true
} ;
2017-10-25 22:39:21 -04:00
}
catch ( e ) {
2017-11-03 22:08:27 -04:00
logSync ( "sync failed: " + e . stack ) ;
2017-11-04 21:21:09 -04:00
return {
success : false ,
message : e . message
}
2017-10-25 22:39:21 -04:00
}
finally {
syncInProgress = false ;
}
2017-10-29 11:22:41 -04:00
}
2017-11-03 22:08:27 -04:00
function logSync ( message ) {
2017-10-29 11:22:41 -04:00
log . info ( message ) ;
2017-10-22 20:22:09 -04:00
}
2017-11-03 22:08:27 -04:00
function logSyncError ( message , e ) {
2017-11-01 00:39:07 -04:00
let completeMessage = message ;
if ( e ) {
completeMessage += ", inner exception: " + e . stack ;
}
throw new Error ( completeMessage ) ;
}
2017-11-03 22:08:27 -04:00
async function updateNote ( entity , links , sourceId ) {
2017-10-29 22:22:30 -04:00
const origNote = await sql . getSingleResult ( "select * from notes where note_id = ?" , [ entity . note _id ] ) ;
2017-10-26 21:16:21 -04:00
2017-11-03 20:50:48 -04:00
if ( ! origNote || origNote . date _modified <= entity . date _modified ) {
2017-10-29 22:22:30 -04:00
await sql . doInTransaction ( async ( ) => {
await sql . replace ( "notes" , entity ) ;
2017-10-26 21:16:21 -04:00
2017-10-29 22:22:30 -04:00
await sql . remove ( "links" , entity . note _id ) ;
2017-10-26 21:16:21 -04:00
2017-10-31 19:34:58 -04:00
for ( const link of links ) {
2017-10-29 22:22:30 -04:00
delete link [ 'lnk_id' ] ;
2017-10-26 21:16:21 -04:00
2017-10-29 22:22:30 -04:00
await sql . insert ( 'link' , link ) ;
}
2017-10-31 00:15:49 -04:00
2017-10-31 19:34:58 -04:00
await sql . addNoteSync ( entity . note _id , sourceId ) ;
2017-11-05 10:41:54 -05:00
await notes . addNoteAudits ( origNote , entity , sourceId ) ;
2017-11-03 22:08:27 -04:00
await eventLog . addNoteEvent ( entity . note _id , "Synced note <note>" ) ;
2017-10-29 22:22:30 -04:00
} ) ;
2017-10-26 23:21:31 -04:00
2017-11-03 22:08:27 -04:00
logSync ( "Update/sync note " + entity . note _id ) ;
2017-10-29 14:55:48 -04:00
}
2017-10-29 22:22:30 -04:00
else {
2017-11-05 21:56:42 -05:00
await eventLog . addNoteEvent ( entity . note _id , "Sync conflict in note <note>, " + utils . formatTwoTimestamps ( origNote . date _modified , entity . date _modified ) ) ;
2017-10-29 22:22:30 -04:00
}
}
2017-11-03 22:08:27 -04:00
async function updateNoteTree ( entity , sourceId ) {
2017-10-29 22:22:30 -04:00
const orig = await sql . getSingleResultOrNull ( "select * from notes_tree where note_id = ?" , [ entity . note _id ] ) ;
if ( orig === null || orig . date _modified < entity . date _modified ) {
2017-10-31 00:15:49 -04:00
await sql . doInTransaction ( async ( ) => {
await sql . replace ( 'notes_tree' , entity ) ;
2017-10-31 19:34:58 -04:00
await sql . addNoteTreeSync ( entity . note _id , sourceId ) ;
2017-11-01 22:36:26 -04:00
2017-11-05 10:41:54 -05:00
await sql . addAudit ( audit _category . UPDATE _TITLE , sourceId , entity . note _id ) ;
2017-10-31 00:15:49 -04:00
} ) ;
2017-10-29 22:22:30 -04:00
2017-11-03 22:08:27 -04:00
logSync ( "Update/sync note tree " + entity . note _id ) ;
2017-10-29 22:22:30 -04:00
}
else {
2017-11-05 21:56:42 -05:00
await eventLog . addNoteEvent ( entity . note _id , "Sync conflict in note tree <note>, " + utils . formatTwoTimestamps ( orig . date _modified , entity . date _modified ) ) ;
2017-10-29 22:22:30 -04:00
}
}
2017-11-03 22:08:27 -04:00
async function updateNoteHistory ( entity , sourceId ) {
2017-10-31 19:34:58 -04:00
const orig = await sql . getSingleResultOrNull ( "select * from notes_history where note_history_id = ?" , [ entity . note _history _id ] ) ;
2017-10-29 22:22:30 -04:00
if ( orig === null || orig . date _modified _to < entity . date _modified _to ) {
2017-10-31 00:15:49 -04:00
await sql . doInTransaction ( async ( ) => {
2017-10-31 19:34:58 -04:00
await sql . replace ( 'notes_history' , entity ) ;
2017-10-29 22:22:30 -04:00
2017-10-31 19:34:58 -04:00
await sql . addNoteHistorySync ( entity . note _history _id , sourceId ) ;
2017-10-31 00:15:49 -04:00
} ) ;
2017-10-29 22:22:30 -04:00
2017-11-03 22:08:27 -04:00
logSync ( "Update/sync note history " + entity . note _history _id ) ;
2017-10-29 22:22:30 -04:00
}
else {
2017-11-05 21:56:42 -05:00
await eventLog . addNoteEvent ( entity . note _id , "Sync conflict in note history for <note>, " + utils . formatTwoTimestamps ( orig . date _modified _to , entity . date _modified _to ) ) ;
2017-10-29 14:55:48 -04:00
}
2017-10-26 21:16:21 -04:00
}
2017-11-03 22:08:27 -04:00
async function updateNoteReordering ( entity , sourceId ) {
2017-11-02 22:55:22 -04:00
await sql . doInTransaction ( async ( ) => {
Object . keys ( entity . ordering ) . forEach ( async key => {
await sql . execute ( "UPDATE notes_tree SET note_pos = ? WHERE note_id = ?" , [ entity . ordering [ key ] , key ] ) ;
} ) ;
await sql . addNoteReorderingSync ( entity . note _pid , sourceId ) ;
2017-11-05 10:41:54 -05:00
await sql . addAudit ( audit _category . CHANGE _POSITION , sourceId , entity . note _pid ) ;
2017-11-02 22:55:22 -04:00
} ) ;
}
2017-11-03 22:08:27 -04:00
async function updateOptions ( entity , sourceId ) {
2017-11-02 20:48:02 -04:00
if ( ! options . SYNCED _OPTIONS . includes ( entity . opt _name ) ) {
return ;
}
const orig = await sql . getSingleResultOrNull ( "select * from options where opt_name = ?" , [ entity . opt _name ] ) ;
if ( orig === null || orig . date _modified < entity . date _modified ) {
await sql . doInTransaction ( async ( ) => {
await sql . replace ( 'options' , entity ) ;
await sql . addOptionsSync ( entity . opt _name , sourceId ) ;
} ) ;
2017-11-03 22:08:27 -04:00
await eventLog . addEvent ( "Synced option " + entity . opt _name ) ;
2017-11-02 20:48:02 -04:00
}
else {
2017-11-05 21:56:42 -05:00
await eventLog . addEvent ( "Sync conflict in options for " + entity . opt _name + ", " + utils . formatTwoTimestamps ( orig . date _modified , entity . date _modified ) ) ;
2017-11-02 20:48:02 -04:00
}
}
2017-11-05 00:16:02 -04:00
async function updateRecentNotes ( entity , sourceId ) {
const orig = await sql . getSingleResultOrNull ( "select * from recent_notes where note_id = ?" , [ entity . note _id ] ) ;
if ( orig === null || orig . date _accessed < entity . date _accessed ) {
await sql . doInTransaction ( async ( ) => {
await sql . replace ( 'recent_notes' , entity ) ;
await sql . addRecentNoteSync ( entity . note _id , sourceId ) ;
} ) ;
}
}
2017-11-09 20:06:33 -05:00
if ( isSyncSetup ) {
log . info ( "Setting up sync to " + SYNC _SERVER + " with timeout " + SYNC _TIMEOUT ) ;
2017-10-25 22:39:21 -04:00
2017-10-26 23:21:31 -04:00
setInterval ( sync , 60000 ) ;
// kickoff initial sync immediately
setTimeout ( sync , 1000 ) ;
}
else {
log . info ( "Sync server not configured, sync timer not running." )
}
2017-10-26 21:16:21 -04:00
module . exports = {
2017-10-29 11:22:41 -04:00
sync ,
2017-10-29 22:22:30 -04:00
updateNote ,
updateNoteTree ,
2017-11-01 22:36:26 -04:00
updateNoteHistory ,
2017-11-02 22:55:22 -04:00
updateNoteReordering ,
2017-11-02 20:48:02 -04:00
updateOptions ,
2017-11-05 00:16:02 -04:00
updateRecentNotes ,
2017-11-01 22:36:26 -04:00
isSyncSetup
2017-10-26 21:16:21 -04:00
} ;