2021-11-22 14:46:57 +02:00
import { _ } from '@joplin/lib/locale' ;
import Setting from '@joplin/lib/models/Setting' ;
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry' ;
import MigrationHandler from '@joplin/lib/services/synchronizer/MigrationHandler' ;
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher' ;
import Synchronizer from '@joplin/lib/Synchronizer' ;
import { masterKeysWithoutPassword } from '@joplin/lib/services/e2ee/utils' ;
2021-12-13 11:32:22 +02:00
import { appTypeToLockType } from '@joplin/lib/services/synchronizer/LockHandler' ;
2022-11-01 17:28:14 +02:00
const BaseCommand = require ( './base-command' ) . default ;
2017-11-03 02:09:34 +02:00
const { app } = require ( './app.js' ) ;
2020-11-07 17:59:37 +02:00
const { OneDriveApiNodeUtils } = require ( '@joplin/lib/onedrive-api-node-utils.js' ) ;
2023-02-26 14:11:48 +02:00
import { reg } from '@joplin/lib/registry' ;
2017-11-03 02:09:34 +02:00
const { cliUtils } = require ( './cli-utils.js' ) ;
const md5 = require ( 'md5' ) ;
2023-02-26 14:11:48 +02:00
import * as locker from 'proper-lockfile' ;
import { pathExists , writeFile } from 'fs-extra' ;
2017-07-10 22:03:46 +02:00
class Command extends BaseCommand {
2021-11-22 14:46:57 +02:00
private syncTargetId_ : number = null ;
private releaseLockFn_ : Function = null ;
private oneDriveApiUtils_ : any = null ;
2017-07-17 20:46:09 +02:00
2023-03-06 16:22:01 +02:00
public usage() {
2017-07-10 22:03:46 +02:00
return 'sync' ;
}
2023-03-06 16:22:01 +02:00
public description() {
2017-07-26 23:27:03 +02:00
return _ ( 'Synchronises with remote storage.' ) ;
2017-07-10 22:03:46 +02:00
}
2023-03-06 16:22:01 +02:00
public 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.' ) ] ,
2021-06-19 12:41:36 +02:00
[ '--use-lock <value>' , 'Disable local locks that prevent multiple clients from synchronizing at the same time (Default = 1)' ] ,
2020-08-02 13:28:50 +02:00
] ;
2017-07-10 22:03:46 +02:00
}
2023-02-26 14:11:48 +02:00
private static async lockFile ( filePath : string ) {
2022-11-13 13:37:05 +02:00
return locker . lock ( filePath , { stale : 1000 * 60 * 5 } ) ;
2017-07-17 21:37:59 +02:00
}
2023-02-26 14:11:48 +02:00
private static async isLocked ( filePath : string ) {
return locker . check ( filePath ) ;
2017-07-17 21:37:59 +02:00
}
2023-03-06 16:22:01 +02:00
public 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 ( {
2021-11-22 14:46:57 +02:00
log : ( . . . s : any [ ] ) = > {
2019-07-30 09:35:42 +02:00
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
}
2023-03-06 16:22:01 +02:00
public cancelAuth() {
2017-11-24 01:10:55 +02:00
if ( this . oneDriveApiUtils_ ) {
this . oneDriveApiUtils_ . cancelOAuthDance ( ) ;
return ;
}
}
2023-03-06 16:22:01 +02:00
public doingAuth() {
2017-11-24 01:10:55 +02:00
return ! ! this . oneDriveApiUtils_ ;
}
2023-03-06 16:22:01 +02:00
public async action ( args : any ) {
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
2023-02-26 14:11:48 +02:00
if ( ! ( await pathExists ( lockFilePath ) ) ) await writeFile ( lockFilePath , 'synclock' ) ;
2017-07-14 21:06:01 +02:00
2021-06-19 12:41:36 +02:00
const useLock = args . options . useLock !== 0 ;
if ( useLock ) {
try {
if ( await Command . isLocked ( lockFilePath ) ) throw new Error ( _ ( 'Synchronisation is already in progress.' ) ) ;
this . releaseLockFn_ = await Command . lockFile ( lockFilePath ) ;
} catch ( error ) {
2022-07-23 09:31:32 +02:00
if ( error . code === 'ELOCKED' ) {
2021-06-19 12:41:36 +02:00
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 ) ;
this . stdout ( msg ) ;
return ;
}
throw error ;
2017-08-04 18:50:12 +02:00
}
}
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
2021-11-22 14:46:57 +02:00
const options : any = {
onProgress : ( report : any ) = > {
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
} ,
2021-11-22 14:46:57 +02:00
onMessage : ( msg : string ) = > {
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 ( ) ,
2021-08-12 17:54:10 +02:00
reg . db ( ) ,
2020-08-02 13:28:50 +02:00
sync . lockHandler ( ) ,
2021-12-13 11:32:22 +02:00
appTypeToLockType ( Setting . value ( 'appType' ) ) ,
2020-08-02 13:28:50 +02:00
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 ) {
2022-07-23 09:31:32 +02:00
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 ( ) ;
}
2021-11-22 14:46:57 +02:00
const noPasswordMkIds = await masterKeysWithoutPassword ( ) ;
if ( noPasswordMkIds . length ) this . stdout ( ` /! \\ ${ _ ( 'Your password is needed to decrypt some of your data. Type `:e2ee decrypt` to set it.' ) } ` ) ;
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
}
2023-03-06 16:22:01 +02:00
public 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
}
2023-03-06 16:22:01 +02:00
public cancellable() {
2017-08-20 16:29:18 +02:00
return true ;
}
2017-07-10 22:03:46 +02:00
}
2019-07-30 09:35:42 +02:00
module .exports = Command ;