2017-11-03 02:09:34 +02:00
const { BaseCommand } = require ( './base-command.js' ) ;
const { app } = require ( './app.js' ) ;
2020-11-07 17:59:37 +02:00
const { _ } = require ( '@joplin/lib/locale' ) ;
const { OneDriveApiNodeUtils } = require ( '@joplin/lib/onedrive-api-node-utils.js' ) ;
const Setting = require ( '@joplin/lib/models/Setting' ) . default ;
2021-01-22 19:41:11 +02:00
const ResourceFetcher = require ( '@joplin/lib/services/ResourceFetcher' ) . default ;
2020-11-07 17:59:37 +02:00
const Synchronizer = require ( '@joplin/lib/Synchronizer' ) . default ;
const { reg } = require ( '@joplin/lib/registry.js' ) ;
2017-11-03 02:09:34 +02:00
const { cliUtils } = require ( './cli-utils.js' ) ;
const md5 = require ( 'md5' ) ;
2017-07-17 21:37:59 +02:00
const locker = require ( 'proper-lockfile' ) ;
const fs = require ( 'fs-extra' ) ;
2020-11-07 17:59:37 +02:00
const SyncTargetRegistry = require ( '@joplin/lib/SyncTargetRegistry' ) ;
const MigrationHandler = require ( '@joplin/lib/services/synchronizer/MigrationHandler' ) . default ;
2017-07-10 22:03:46 +02:00
class Command extends BaseCommand {
2017-07-17 20:46:09 +02:00
constructor ( ) {
super ( ) ;
2017-11-24 01:10:55 +02:00
this . syncTargetId _ = null ;
2017-07-17 21:37:59 +02:00
this . releaseLockFn _ = null ;
2017-10-24 20:03:12 +02:00
this . oneDriveApiUtils _ = null ;
2017-07-17 20:46:09 +02:00
}
2017-07-10 22:03:46 +02:00
usage ( ) {
return 'sync' ;
}
description ( ) {
2017-07-26 23:27:03 +02:00
return _ ( 'Synchronises with remote storage.' ) ;
2017-07-10 22:03:46 +02:00
}
options ( ) {
2020-08-02 13:28:50 +02:00
return [
[ '--target <target>' , _ ( 'Sync to provided target (defaults to sync.target config value)' ) ] ,
[ '--upgrade' , _ ( 'Upgrade the sync target to the latest version.' ) ] ,
] ;
2017-07-10 22:03:46 +02:00
}
2017-07-17 21:37:59 +02:00
static lockFile ( filePath ) {
return new Promise ( ( resolve , reject ) => {
2017-07-19 21:15:55 +02:00
locker . lock ( filePath , { stale : 1000 * 60 * 5 } , ( error , release ) => {
2017-07-17 21:37:59 +02:00
if ( error ) {
reject ( error ) ;
return ;
}
resolve ( release ) ;
} ) ;
} ) ;
}
static isLocked ( filePath ) {
return new Promise ( ( resolve , reject ) => {
locker . check ( filePath , ( error , isLocked ) => {
if ( error ) {
reject ( error ) ;
return ;
}
resolve ( isLocked ) ;
} ) ;
} ) ;
}
2018-01-25 21:01:14 +02:00
async doAuth ( ) {
2017-11-24 01:10:55 +02:00
const syncTarget = reg . syncTarget ( this . syncTargetId _ ) ;
2018-01-25 21:01:14 +02:00
const syncTargetMd = SyncTargetRegistry . idToMetadata ( this . syncTargetId _ ) ;
2019-07-30 09:35:42 +02:00
if ( this . syncTargetId _ === 3 || this . syncTargetId _ === 4 ) {
// OneDrive
2018-01-25 21:01:14 +02:00
this . oneDriveApiUtils _ = new OneDriveApiNodeUtils ( syncTarget . api ( ) ) ;
const auth = await this . oneDriveApiUtils _ . oauthDance ( {
2019-07-30 09:35:42 +02:00
log : ( ... s ) => {
return this . stdout ( ... s ) ;
} ,
2018-01-25 21:01:14 +02:00
} ) ;
this . oneDriveApiUtils _ = null ;
2019-07-30 09:35:42 +02:00
2019-09-19 23:51:18 +02:00
Setting . setValue ( ` sync. ${ this . syncTargetId _ } .auth ` , auth ? JSON . stringify ( auth ) : null ) ;
2018-01-25 21:01:14 +02:00
if ( ! auth ) {
this . stdout ( _ ( 'Authentication was not completed (did not receive an authentication token).' ) ) ;
return false ;
}
2018-03-26 19:33:55 +02:00
return true ;
2019-07-30 09:35:42 +02:00
} else if ( syncTargetMd . name === 'dropbox' ) {
// Dropbox
2018-03-26 19:33:55 +02:00
const api = await syncTarget . api ( ) ;
const loginUrl = api . loginUrl ( ) ;
this . stdout ( _ ( 'To allow Joplin to synchronise with Dropbox, please follow the steps below:' ) ) ;
this . stdout ( _ ( 'Step 1: Open this URL in your browser to authorise the application:' ) ) ;
this . stdout ( loginUrl ) ;
const authCode = await this . prompt ( _ ( 'Step 2: Enter the code provided by Dropbox:' ) , { type : 'string' } ) ;
if ( ! authCode ) {
this . stdout ( _ ( 'Authentication was not completed (did not receive an authentication token).' ) ) ;
return false ;
}
const response = await api . execAuthToken ( authCode ) ;
2019-09-19 23:51:18 +02:00
Setting . setValue ( ` sync. ${ this . syncTargetId _ } .auth ` , response . access _token ) ;
2018-03-26 19:33:55 +02:00
api . setAuthToken ( response . access _token ) ;
2018-01-25 21:01:14 +02:00
return true ;
}
2018-03-26 19:33:55 +02:00
this . stdout ( _ ( 'Not authentified with %s. Please provide any missing credentials.' , syncTargetMd . label ) ) ;
2018-01-25 21:01:14 +02:00
return false ;
2017-11-24 01:10:55 +02:00
}
cancelAuth ( ) {
if ( this . oneDriveApiUtils _ ) {
this . oneDriveApiUtils _ . cancelOAuthDance ( ) ;
return ;
}
}
doingAuth ( ) {
return ! ! this . oneDriveApiUtils _ ;
}
2017-07-10 22:03:46 +02:00
async action ( args ) {
2017-07-17 21:37:59 +02:00
this . releaseLockFn _ = null ;
2017-07-17 20:46:09 +02:00
2017-07-24 20:01:40 +02:00
// Lock is unique per profile/database
2019-09-19 23:51:18 +02:00
const lockFilePath = ` ${ require ( 'os' ) . tmpdir ( ) } /synclock_ ${ md5 ( escape ( Setting . value ( 'profileDir' ) ) ) } ` ; // https://github.com/pvorb/node-md5/issues/41
2019-07-30 09:35:42 +02:00
if ( ! ( await fs . pathExists ( lockFilePath ) ) ) await fs . writeFile ( lockFilePath , 'synclock' ) ;
2017-07-14 21:06:01 +02:00
2017-08-04 18:50:12 +02:00
try {
if ( await Command . isLocked ( lockFilePath ) ) throw new Error ( _ ( 'Synchronisation is already in progress.' ) ) ;
2017-07-13 20:09:47 +02:00
2017-08-04 18:50:12 +02:00
this . releaseLockFn _ = await Command . lockFile ( lockFilePath ) ;
} catch ( error ) {
if ( error . code == 'ELOCKED' ) {
const msg = _ ( 'Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at "%s" and resume the operation.' , error . file ) ;
2017-10-07 18:30:27 +02:00
this . stdout ( msg ) ;
2017-08-04 18:50:12 +02:00
return ;
}
throw error ;
}
2017-07-10 22:03:46 +02:00
2017-10-24 20:03:12 +02:00
const cleanUp = ( ) => {
cliUtils . redrawDone ( ) ;
if ( this . releaseLockFn _ ) {
this . releaseLockFn _ ( ) ;
this . releaseLockFn _ = null ;
}
} ;
2017-07-17 21:37:59 +02:00
try {
2017-11-24 01:10:55 +02:00
this . syncTargetId _ = Setting . value ( 'sync.target' ) ;
if ( args . options . target ) this . syncTargetId _ = args . options . target ;
const syncTarget = reg . syncTarget ( this . syncTargetId _ ) ;
2017-07-24 21:47:01 +02:00
2019-07-30 09:35:42 +02:00
if ( ! ( await syncTarget . isAuthenticated ( ) ) ) {
2020-08-02 13:28:50 +02:00
app ( ) . gui ( ) . showConsole ( ) ;
app ( ) . gui ( ) . maximizeConsole ( ) ;
2017-11-24 01:10:55 +02:00
2018-01-25 21:01:14 +02:00
const authDone = await this . doAuth ( ) ;
if ( ! authDone ) return cleanUp ( ) ;
2017-07-24 21:47:01 +02:00
}
2019-07-30 09:35:42 +02:00
2017-11-24 01:10:55 +02:00
const sync = await syncTarget . synchronizer ( ) ;
2017-07-10 22:03:46 +02:00
2020-03-14 01:46:14 +02:00
const options = {
2020-05-21 10:14:33 +02:00
onProgress : report => {
2020-03-14 01:46:14 +02:00
const lines = Synchronizer . reportToLines ( report ) ;
2017-08-04 18:50:12 +02:00
if ( lines . length ) cliUtils . redraw ( lines . join ( ' ' ) ) ;
2017-07-17 21:37:59 +02:00
} ,
2020-05-21 10:14:33 +02:00
onMessage : msg => {
2017-08-04 18:50:12 +02:00
cliUtils . redrawDone ( ) ;
2017-10-07 18:30:27 +02:00
this . stdout ( msg ) ;
2017-07-17 21:37:59 +02:00
} ,
} ;
2017-11-24 01:10:55 +02:00
this . stdout ( _ ( 'Synchronisation target: %s (%s)' , Setting . enumOptionLabel ( 'sync.target' , this . syncTargetId _ ) , this . syncTargetId _ ) ) ;
2017-07-17 21:37:59 +02:00
2020-06-20 13:18:41 +02:00
if ( ! sync ) throw new Error ( _ ( 'Cannot initialise synchroniser.' ) ) ;
2017-07-17 21:37:59 +02:00
2020-08-02 13:28:50 +02:00
if ( args . options . upgrade ) {
let migrationError = null ;
try {
const migrationHandler = new MigrationHandler (
sync . api ( ) ,
sync . lockHandler ( ) ,
Setting . value ( 'appType' ) ,
Setting . value ( 'clientId' )
) ;
migrationHandler . setLogger ( cliUtils . stdoutLogger ( this . stdout . bind ( this ) ) ) ;
await migrationHandler . upgrade ( ) ;
} catch ( error ) {
migrationError = error ;
}
if ( ! migrationError ) {
Setting . setValue ( 'sync.upgradeState' , Setting . SYNC _UPGRADE _STATE _IDLE ) ;
await Setting . saveAll ( ) ;
}
if ( migrationError ) throw migrationError ;
return cleanUp ( ) ;
}
2017-10-07 18:30:27 +02:00
this . stdout ( _ ( 'Starting synchronisation...' ) ) ;
2017-07-17 21:37:59 +02:00
2019-09-19 23:51:18 +02:00
const contextKey = ` sync. ${ this . syncTargetId _ } .context ` ;
2017-08-19 22:56:28 +02:00
let context = Setting . value ( contextKey ) ;
2017-07-18 21:57:49 +02:00
context = context ? JSON . parse ( context ) : { } ;
options . context = context ;
2017-07-24 21:47:01 +02:00
try {
2020-03-14 01:46:14 +02:00
const newContext = await sync . start ( options ) ;
2017-08-19 22:56:28 +02:00
Setting . setValue ( contextKey , JSON . stringify ( newContext ) ) ;
2017-07-24 21:47:01 +02:00
} catch ( error ) {
if ( error . code == 'alreadyStarted' ) {
2017-10-07 18:30:27 +02:00
this . stdout ( error . message ) ;
2017-07-24 21:47:01 +02:00
} else {
throw error ;
}
}
2017-07-17 21:37:59 +02:00
2018-10-08 20:11:53 +02:00
// When using the tool in command line mode, the ResourceFetcher service is
// not going to be running in the background, so the resources need to be
2019-10-29 11:02:42 +02:00
// explicitly downloaded below.
2018-10-08 20:11:53 +02:00
if ( ! app ( ) . hasGui ( ) ) {
2018-10-30 02:17:50 +02:00
this . stdout ( _ ( 'Downloading resources...' ) ) ;
2018-10-08 20:11:53 +02:00
await ResourceFetcher . instance ( ) . fetchAll ( ) ;
await ResourceFetcher . instance ( ) . waitForAllFinished ( ) ;
}
2017-07-17 21:37:59 +02:00
await app ( ) . refreshCurrentFolder ( ) ;
} catch ( error ) {
2017-10-24 20:03:12 +02:00
cleanUp ( ) ;
2017-07-17 21:37:59 +02:00
throw error ;
}
2017-07-16 00:47:11 +02:00
2020-08-02 13:28:50 +02:00
if ( Setting . value ( 'sync.upgradeState' ) > Setting . SYNC _UPGRADE _STATE _IDLE ) {
this . stdout ( ` /! \\ ${ _ ( 'Sync target must be upgraded! Run `%s` to proceed.' , 'sync --upgrade' ) } ` ) ;
app ( ) . gui ( ) . showConsole ( ) ;
app ( ) . gui ( ) . maximizeConsole ( ) ;
}
2017-10-24 20:03:12 +02:00
cleanUp ( ) ;
2017-07-10 22:03:46 +02:00
}
async cancel ( ) {
2017-11-24 01:10:55 +02:00
if ( this . doingAuth ( ) ) {
this . cancelAuth ( ) ;
2017-10-24 20:03:12 +02:00
return ;
}
2017-11-24 01:10:55 +02:00
const syncTargetId = this . syncTargetId _ ? this . syncTargetId _ : Setting . value ( 'sync.target' ) ;
2017-07-17 20:46:09 +02:00
2017-08-04 18:50:12 +02:00
cliUtils . redrawDone ( ) ;
2017-10-07 18:30:27 +02:00
this . stdout ( _ ( 'Cancelling... Please wait.' ) ) ;
2017-07-26 22:09:33 +02:00
2017-11-24 01:10:55 +02:00
const syncTarget = reg . syncTarget ( syncTargetId ) ;
2018-03-26 19:33:55 +02:00
if ( await syncTarget . isAuthenticated ( ) ) {
2017-11-24 01:10:55 +02:00
const sync = await syncTarget . synchronizer ( ) ;
2017-10-14 20:03:23 +02:00
if ( sync ) await sync . cancel ( ) ;
2017-07-26 22:09:33 +02:00
} else {
if ( this . releaseLockFn _ ) this . releaseLockFn _ ( ) ;
this . releaseLockFn _ = null ;
}
2017-07-17 20:46:09 +02:00
2017-11-24 01:10:55 +02:00
this . syncTargetId _ = null ;
2017-07-10 22:03:46 +02:00
}
2017-08-20 16:29:18 +02:00
cancellable ( ) {
return true ;
}
2017-07-10 22:03:46 +02:00
}
2019-07-30 09:35:42 +02:00
module . exports = Command ;