2017-07-10 22:03:46 +02:00
import { FileApi } from 'lib/file-api.js' ;
import { FileApiDriverOneDrive } from 'lib/file-api-driver-onedrive.js' ;
import { FileApiDriverMemory } from 'lib/file-api-driver-memory.js' ;
import { FileApiDriverLocal } from 'lib/file-api-driver-local.js' ;
import { OneDriveApiNodeUtils } from './onedrive-api-node-utils.js' ;
import { JoplinDatabase } from 'lib/joplin-database.js' ;
import { Database } from 'lib/database.js' ;
import { DatabaseDriverNode } from 'lib/database-driver-node.js' ;
import { BaseModel } from 'lib/base-model.js' ;
import { Folder } from 'lib/models/folder.js' ;
import { BaseItem } from 'lib/models/base-item.js' ;
import { Note } from 'lib/models/note.js' ;
import { Setting } from 'lib/models/setting.js' ;
import { Synchronizer } from 'lib/synchronizer.js' ;
import { Logger } from 'lib/logger.js' ;
import { sprintf } from 'sprintf-js' ;
import { vorpalUtils } from 'vorpal-utils.js' ;
import { reg } from 'lib/registry.js' ;
import { fileExtension } from 'lib/path-utils.js' ;
2017-07-18 20:04:47 +02:00
import { _ , setLocale } from 'lib/locale.js' ;
2017-07-10 22:03:46 +02:00
import os from 'os' ;
import fs from 'fs-extra' ;
class Application {
constructor ( ) {
this . showPromptString _ = true ;
this . logger _ = new Logger ( ) ;
this . dbLogger _ = new Logger ( ) ;
this . syncLogger _ = new Logger ( ) ;
this . synchronizers _ = { } ;
}
vorpal ( ) {
return this . vorpal _ ;
}
currentFolder ( ) {
return this . currentFolder _ ;
}
2017-07-16 00:47:11 +02:00
async refreshCurrentFolder ( ) {
let newFolder = null ;
if ( this . currentFolder _ ) newFolder = await Folder . load ( this . currentFolder _ . id ) ;
if ( ! newFolder ) newFolder = await Folder . defaultFolder ( ) ;
this . switchCurrentFolder ( newFolder ) ;
}
2017-07-10 22:03:46 +02:00
updatePrompt ( ) {
if ( ! this . showPromptString _ ) return '' ;
2017-07-11 20:17:23 +02:00
let path = '' ;
2017-07-10 22:03:46 +02:00
if ( this . currentFolder ( ) ) {
path += '/' + this . currentFolder ( ) . title ;
}
const prompt = Setting . value ( 'appName' ) + ':' + path + '$ ' ;
this . vorpal ( ) . delimiter ( prompt ) ;
}
switchCurrentFolder ( folder ) {
this . currentFolder _ = folder ;
Setting . setValue ( 'activeFolderId' , folder ? folder . id : '' ) ;
this . updatePrompt ( ) ;
}
2017-07-17 21:19:01 +02:00
async guessTypeAndLoadItem ( pattern , options = null ) {
let type = BaseModel . TYPE _NOTE ;
if ( pattern . indexOf ( '/' ) === 0 ) {
type = BaseModel . TYPE _FOLDER ;
pattern = pattern . substr ( 1 ) ;
}
return this . loadItem ( type , pattern , options ) ;
}
2017-07-15 17:35:40 +02:00
async loadItem ( type , pattern , options = null ) {
let output = await this . loadItems ( type , pattern , options ) ;
2017-07-10 22:03:46 +02:00
return output . length ? output [ 0 ] : null ;
}
2017-07-11 20:17:23 +02:00
async loadItems ( type , pattern , options = null ) {
2017-07-15 17:35:40 +02:00
if ( type == BaseModel . TYPE _FOLDER && ( pattern == Folder . conflictFolderTitle ( ) || pattern == Folder . conflictFolderId ( ) ) ) return [ Folder . conflictFolder ( ) ] ;
2017-07-11 20:17:23 +02:00
if ( ! options ) options = { } ;
2017-07-13 23:26:45 +02:00
2017-07-11 20:17:23 +02:00
const parent = options . parent ? options . parent : app ( ) . currentFolder ( ) ;
const ItemClass = BaseItem . itemClass ( type ) ;
if ( type == BaseModel . TYPE _NOTE && pattern . indexOf ( '*' ) >= 0 ) { // Handle it as pattern
if ( ! parent ) throw new Error ( _ ( 'No notebook selected.' ) ) ;
return await Note . previews ( parent . id , { titlePattern : pattern } ) ;
} else { // Single item
let item = null ;
if ( type == BaseModel . TYPE _NOTE ) {
if ( ! parent ) throw new Error ( _ ( 'No notebook has been specified.' ) ) ;
item = await ItemClass . loadFolderNoteByField ( parent . id , 'title' , pattern ) ;
} else {
item = await ItemClass . loadByTitle ( pattern ) ;
}
if ( item ) return [ item ] ;
item = await ItemClass . load ( pattern ) ; // Load by id
if ( item ) return [ item ] ;
if ( pattern . length >= 4 ) {
item = await ItemClass . loadByPartialId ( pattern ) ;
if ( item ) return [ item ] ;
}
2017-07-10 22:47:01 +02:00
}
return [ ] ;
2017-07-10 22:03:46 +02:00
}
// Handles the initial flags passed to main script and
// returns the remaining args.
async handleStartFlags _ ( argv ) {
let matched = { } ;
argv = argv . slice ( 0 ) ;
argv . splice ( 0 , 2 ) ; // First arguments are the node executable, and the node JS file
while ( argv . length ) {
let arg = argv [ 0 ] ;
let nextArg = argv . length >= 2 ? argv [ 1 ] : null ;
if ( arg == '--profile' ) {
if ( ! nextArg ) throw new Error ( _ ( 'Usage: --profile <dir-path>' ) ) ;
matched . profileDir = nextArg ;
argv . splice ( 0 , 2 ) ;
continue ;
}
if ( arg == '--env' ) {
if ( ! nextArg ) throw new Error ( _ ( 'Usage: --env <dev|prod>' ) ) ;
matched . env = nextArg ;
argv . splice ( 0 , 2 ) ;
continue ;
}
if ( arg == '--redraw-disabled' ) {
vorpalUtils . setRedrawEnabled ( false ) ;
argv . splice ( 0 , 1 ) ;
continue ;
}
2017-07-10 22:59:58 +02:00
if ( arg == '--update-geolocation-disabled' ) {
Note . updateGeolocationEnabled _ = false ;
argv . splice ( 0 , 1 ) ;
continue ;
}
2017-07-10 22:03:46 +02:00
if ( arg == '--stack-trace-enabled' ) {
vorpalUtils . setStackTraceEnabled ( true ) ;
argv . splice ( 0 , 1 ) ;
continue ;
}
if ( arg == '--log-level' ) {
if ( ! nextArg ) throw new Error ( _ ( 'Usage: --log-level <none|error|warn|info|debug>' ) ) ;
matched . logLevel = Logger . levelStringToId ( nextArg ) ;
argv . splice ( 0 , 2 ) ;
continue ;
}
if ( arg . length && arg [ 0 ] == '-' ) {
throw new Error ( _ ( 'Unknown flag: %s' , arg ) ) ;
} else {
break ;
}
}
if ( ! matched . logLevel ) matched . logLevel = Logger . LEVEL _INFO ;
2017-07-11 01:17:03 +02:00
if ( ! matched . env ) matched . env = 'prod' ;
2017-07-10 22:03:46 +02:00
return {
matched : matched ,
argv : argv ,
} ;
}
escapeShellArg ( arg ) {
if ( arg . indexOf ( '"' ) >= 0 && arg . indexOf ( "'" ) >= 0 ) throw new Error ( _ ( 'Command line argument "%s" contains both quotes and double-quotes - aborting.' , arg ) ) ; // Hopeless case
let quote = '"' ;
if ( arg . indexOf ( '"' ) >= 0 ) quote = "'" ;
if ( arg . indexOf ( ' ' ) >= 0 || arg . indexOf ( "\t" ) >= 0 ) return quote + arg + quote ;
return arg ;
}
shellArgsToString ( args ) {
let output = [ ] ;
for ( let i = 0 ; i < args . length ; i ++ ) {
output . push ( this . escapeShellArg ( args [ i ] ) ) ;
}
return output . join ( ' ' ) ;
}
2017-07-18 20:49:47 +02:00
onLocaleChanged ( ) {
let currentCommands = this . vorpal ( ) . commands ;
for ( let i = 0 ; i < currentCommands . length ; i ++ ) {
let cmd = currentCommands [ i ] ;
if ( cmd . _name == 'help' ) {
cmd . description ( _ ( 'Provides help for a given command.' ) ) ;
} else if ( cmd . _name == 'exit' ) {
cmd . description ( _ ( 'Exits the application.' ) ) ;
} else if ( cmd . _ _commandObject ) {
cmd . description ( cmd . _ _commandObject . description ( ) ) ;
}
}
}
2017-07-18 21:27:10 +02:00
2017-07-10 22:03:46 +02:00
loadCommands _ ( ) {
2017-07-18 20:49:47 +02:00
this . onLocaleChanged ( ) ; // Ensures that help and exit commands are translated
2017-07-10 22:03:46 +02:00
fs . readdirSync ( _ _dirname ) . forEach ( ( path ) => {
if ( path . indexOf ( 'command-' ) !== 0 ) return ;
const ext = fileExtension ( path )
if ( ext != 'js' ) return ;
let CommandClass = require ( './' + path ) ;
let cmd = new CommandClass ( ) ;
let vorpalCmd = this . vorpal ( ) . command ( cmd . usage ( ) , cmd . description ( ) ) ;
2017-07-18 20:49:47 +02:00
vorpalCmd . _ _commandObject = cmd ;
2017-07-10 22:03:46 +02:00
2017-07-13 23:26:45 +02:00
// TODO: maybe remove if the PR is not merged
if ( 'disableTypeCasting' in vorpalCmd ) vorpalCmd . disableTypeCasting ( ) ;
2017-07-10 22:03:46 +02:00
for ( let i = 0 ; i < cmd . aliases ( ) . length ; i ++ ) {
vorpalCmd . alias ( cmd . aliases ( ) [ i ] ) ;
}
for ( let i = 0 ; i < cmd . options ( ) . length ; i ++ ) {
let options = cmd . options ( ) [ i ] ;
if ( options . length == 2 ) vorpalCmd . option ( options [ 0 ] , options [ 1 ] ) ;
if ( options . length == 3 ) vorpalCmd . option ( options [ 0 ] , options [ 1 ] , options [ 2 ] ) ;
if ( options . length > 3 ) throw new Error ( 'Invalid number of option arguments' ) ;
}
if ( cmd . autocomplete ( ) ) vorpalCmd . autocomplete ( cmd . autocomplete ( ) ) ;
let actionFn = async function ( args , end ) {
try {
const fn = cmd . action . bind ( this ) ;
await fn ( args ) ;
} catch ( error ) {
this . log ( error ) ;
}
vorpalUtils . redrawDone ( ) ;
end ( ) ;
} ;
vorpalCmd . action ( actionFn ) ;
let cancelFn = async function ( ) {
const fn = cmd . cancel . bind ( this ) ;
await fn ( ) ;
} ;
vorpalCmd . cancel ( cancelFn ) ;
2017-07-18 20:21:03 +02:00
if ( cmd . hidden ( ) ) vorpalCmd . hidden ( ) ;
2017-07-10 22:03:46 +02:00
} ) ;
2017-07-18 21:27:10 +02:00
this . vorpal ( ) . catch ( '[args...]' , 'Catches undefined commands' ) . action ( function ( args , end ) {
args = args . args ;
function delayExec ( command ) {
setTimeout ( ( ) => {
app ( ) . vorpal ( ) . exec ( command ) ;
} , 100 ) ;
}
if ( ! args . length ) {
end ( ) ;
delayExec ( 'help' ) ;
return ;
}
let commandName = args . splice ( 0 , 1 ) ;
let aliases = Setting . value ( 'aliases' ) . trim ( ) ;
aliases = aliases . length ? JSON . parse ( aliases ) : [ ] ;
for ( let i = 0 ; i < aliases . length ; i ++ ) {
const alias = aliases [ i ] ;
if ( alias . name == commandName ) {
let command = alias . command + ' ' + app ( ) . shellArgsToString ( args ) ;
end ( ) ;
delayExec ( command ) ;
return ;
}
}
this . log ( _ ( "Invalid command. Showing help:" ) ) ;
end ( ) ;
delayExec ( 'help' ) ;
} ) ;
2017-07-10 22:03:46 +02:00
}
async synchronizer ( syncTarget ) {
if ( this . synchronizers _ [ syncTarget ] ) return this . synchronizers _ [ syncTarget ] ;
let fileApi = null ;
2017-07-16 14:53:59 +02:00
// TODO: create file api based on syncTarget
2017-07-10 22:03:46 +02:00
if ( syncTarget == 'onedrive' ) {
const oneDriveApi = reg . oneDriveApi ( ) ;
let driver = new FileApiDriverOneDrive ( oneDriveApi ) ;
let auth = Setting . value ( 'sync.onedrive.auth' ) ;
if ( ! oneDriveApi . auth ( ) ) {
const oneDriveApiUtils = new OneDriveApiNodeUtils ( oneDriveApi ) ;
auth = await oneDriveApiUtils . oauthDance ( this . vorpal ( ) ) ;
Setting . setValue ( 'sync.onedrive.auth' , auth ? JSON . stringify ( auth ) : auth ) ;
if ( ! auth ) return ;
}
let appDir = await oneDriveApi . appDirectory ( ) ;
this . logger _ . info ( 'App dir: ' + appDir ) ;
fileApi = new FileApi ( appDir , driver ) ;
fileApi . setLogger ( this . logger _ ) ;
} else if ( syncTarget == 'memory' ) {
fileApi = new FileApi ( 'joplin' , new FileApiDriverMemory ( ) ) ;
fileApi . setLogger ( this . logger _ ) ;
2017-07-16 18:06:05 +02:00
} else if ( syncTarget == 'filesystem' ) {
let syncDir = Setting . value ( 'sync.filesystem.path' ) ;
2017-07-10 22:03:46 +02:00
if ( ! syncDir ) syncDir = Setting . value ( 'profileDir' ) + '/sync' ;
this . vorpal ( ) . log ( _ ( 'Synchronizing with directory "%s"' , syncDir ) ) ;
await fs . mkdirp ( syncDir , 0o755 ) ;
fileApi = new FileApi ( syncDir , new FileApiDriverLocal ( ) ) ;
fileApi . setLogger ( this . logger _ ) ;
} else {
throw new Error ( 'Unknown backend: ' + syncTarget ) ;
}
this . synchronizers _ [ syncTarget ] = new Synchronizer ( this . database _ , fileApi , Setting . value ( 'appType' ) ) ;
this . synchronizers _ [ syncTarget ] . setLogger ( this . syncLogger _ ) ;
return this . synchronizers _ [ syncTarget ] ;
}
async start ( ) {
this . vorpal _ = require ( 'vorpal' ) ( ) ;
vorpalUtils . initialize ( this . vorpal ( ) ) ;
let argv = process . argv ;
let startFlags = await this . handleStartFlags _ ( argv ) ;
argv = startFlags . argv ;
let initArgs = startFlags . matched ;
if ( argv . length ) this . showPromptString _ = false ;
const profileDir = initArgs . profileDir ? initArgs . profileDir : os . homedir ( ) + '/.config/' + Setting . value ( 'appName' ) ;
const resourceDir = profileDir + '/resources' ;
const tempDir = profileDir + '/tmp' ;
Setting . setConstant ( 'env' , initArgs . env ) ;
Setting . setConstant ( 'profileDir' , profileDir ) ;
Setting . setConstant ( 'resourceDir' , resourceDir ) ;
Setting . setConstant ( 'tempDir' , tempDir ) ;
await fs . mkdirp ( profileDir , 0o755 ) ;
await fs . mkdirp ( resourceDir , 0o755 ) ;
await fs . mkdirp ( tempDir , 0o755 ) ;
this . logger _ . addTarget ( 'file' , { path : profileDir + '/log.txt' } ) ;
this . logger _ . setLevel ( initArgs . logLevel ) ;
reg . setLogger ( this . logger _ ) ;
this . dbLogger _ . addTarget ( 'file' , { path : profileDir + '/log-database.txt' } ) ;
this . dbLogger _ . setLevel ( initArgs . logLevel ) ;
this . syncLogger _ . addTarget ( 'file' , { path : profileDir + '/log-sync.txt' } ) ;
this . syncLogger _ . setLevel ( initArgs . logLevel ) ;
const packageJson = require ( './package.json' ) ;
this . logger _ . info ( sprintf ( 'Starting %s %s (%s)...' , packageJson . name , packageJson . version , Setting . value ( 'env' ) ) ) ;
this . logger _ . info ( 'Profile directory: ' + profileDir ) ;
this . database _ = new JoplinDatabase ( new DatabaseDriverNode ( ) ) ;
this . database _ . setLogger ( this . dbLogger _ ) ;
await this . database _ . open ( { name : profileDir + '/database.sqlite' } ) ;
BaseModel . db _ = this . database _ ;
await Setting . load ( ) ;
2017-07-18 20:04:47 +02:00
setLocale ( Setting . value ( 'locale' ) ) ;
2017-07-18 20:49:47 +02:00
this . loadCommands _ ( ) ;
2017-07-10 22:03:46 +02:00
let currentFolderId = Setting . value ( 'activeFolderId' ) ;
this . currentFolder _ = null ;
if ( currentFolderId ) this . currentFolder _ = await Folder . load ( currentFolderId ) ;
if ( ! this . currentFolder _ ) this . currentFolder _ = await Folder . defaultFolder ( ) ;
Setting . setValue ( 'activeFolderId' , this . currentFolder _ ? this . currentFolder _ . id : '' ) ;
2017-07-17 20:46:09 +02:00
if ( this . currentFolder _ ) await this . vorpal ( ) . exec ( 'use ' + this . escapeShellArg ( this . currentFolder _ . title ) ) ;
2017-07-10 22:03:46 +02:00
// If we still have arguments, pass it to Vorpal and exit
if ( argv . length ) {
let cmd = this . shellArgsToString ( argv ) ;
await this . vorpal ( ) . exec ( cmd ) ;
} else {
this . updatePrompt ( ) ;
this . vorpal ( ) . show ( ) ;
this . vorpal ( ) . history ( Setting . value ( 'appId' ) ) ; // Enables persistent history
if ( ! this . currentFolder ( ) ) {
this . vorpal ( ) . log ( _ ( 'No notebook is defined. Create one with `mkbook <notebook>`.' ) ) ;
}
}
}
}
let application _ = null ;
function app ( ) {
if ( application _ ) return application _ ;
application _ = new Application ( ) ;
return application _ ;
}
export { app } ;