import ServiceElectron from '../service.electron' ; import ServiceStreams from '../service.streams' ; import ServiceStreamSource from '../service.stream.sources' ; import ServiceHotkeys from '../service.hotkeys' ; import ServiceFileRecent from './service.file.recent' ; import ServiceNotifications from '../service.notifications' ; import ServiceRenderState from '../service.render.state' ; import Logger from '../../tools/env.logger' ; import { IPCMessages } from '../service.electron' ; import { ENotificationType } from '../service.notifications' ; import { IMapItem , ITicks } from '../../controllers/files.parsers/interface' ; import { dialog , OpenDialogReturnValue } from 'electron' ; import { getDefaultFileParser , AFileParser , getParserForFile , } from '../../controllers/files.parsers/index' ; import { Subscription } from '../../tools/index' ; import { IService } from '../../interfaces/interface.service' ; import { isHidden } from '../../tools/fs' ; import { app } from 'electron' ; import * as Tools from '../../tools/index' ; import * as fs from 'fs' ; import * as path from 'path' ; import * as Parsers from '../../controllers/files.parsers/index' ; interface IOpenFileResult { sourceId : number ; options : any ; } class description class ServiceFileOpener implements IService { private _logger : Logger = new Logger ( 'ServiceFileOpener' ) ; private _subscriptions : { [ key ] : Subscription ; } = { } ; private _active : Map < string , AFileParser > = new Map < string , AFileParser > ( ) ; private _recent : { path : string | undefined ; pending : boolean ; } = { path : undefined , pending : true , } ; constructor ( ) { app . on ( 'open-file' , this . _onOSRecentFileOpen . bind ( this ) ) ; ServiceRenderState . doOnInit ( Tools . guid ( ) , this . _onRenderReady . bind ( this ) ) ; } returns public init ( ) { return new Promise ( ( resolve , reject ) => { Promise . all ( [ ServiceElectron . IPC . subscribe ( IPCMessages . FileOpenRequest , this . _ipc_FileOpenRequest . bind ( this ) ) . then ( ( subscription ) => { this . _subscriptions . FileOpenRequest = subscription ; } ) , ServiceElectron . IPC . subscribe ( IPCMessages . FileListRequest , this . _ipc_FileListRequest . bind ( this ) ) . then ( ( subscription ) => { this . _subscriptions . FileListRequest = subscription ; } ) , ] ) . then ( ( ) => { this . _subscriptions . onSessionClosed = ServiceStreams . getSubjects ( ) . onSessionClosed . subscribe ( this . _onSessionClosed . bind ( this ) ) ; this . _subscriptions . openLocalFile = ServiceHotkeys . getSubject ( ) . openLocalFile . subscribe ( this . _hotkey_openLocalFile . bind ( this ) ) ; resolve ( ) ; } ) . catch ( ( error ) => { this . _logger . error ( `Fail to init module due error: ${ error . message }` ) ; reject ( error ) ; } ) ; } ) ; } public destroy ( ) { return new Promise ( ( resolve ) => { Object . keys ( this . _subscriptions ) . forEach ( ( key ) => { this . _subscriptions [ key ] . destroy ( ) ; } ) ; if ( this . _active . size === 0 ) { return resolve ( ) ; } Promise . all ( Array . from ( this . _active . values ( ) ) . map ( ( task ) => { return task . abort ( ) ; } ) ) . catch ( ( error ) => { this . _logger . error ( `Fail to abort active tasks due error: ${ error . message }` ) ; } ) . finally ( ( ) => { resolve ( ) ; } ) ; } ) ; } public getName ( ) { return 'ServiceFileOpener' ; } public open ( file , sessionId , parser ? , opts ? ) { return new Promise ( ( resolve , reject ) => { fs . stat ( file , ( error , stats ) => { if ( error ) { return reject ( error ) ; } getParserForFile ( file , parser ) . then ( ( detectedParser ) => { if ( detectedParser === undefined ) { detectedParser = getDefaultFileParser ( ) ; } if ( detectedParser === undefined ) { return reject ( new Error ( this . _logger . warn ( `Fail to find parser for file "${ file }"` ) ) ) ; } const instanceParser = detectedParser ; const open = ( options ) => { if ( typeof options === 'boolean' ) { this . _logger . debug ( `User canceled opening file.` ) ; return resolve ( { sourceId : - 1 , options : undefined } ) ; } const trackingId = Tools . guid ( ) ; this . _setProgress ( instanceParser , trackingId , file ) ; this . _incrProgress ( instanceParser , trackingId , sessionId , 0 ) ; this . _active . set ( sessionId , instanceParser ) ; ServiceElectron . IPC . send ( new IPCMessages . FileOpenInprogressEvent ( { session : sessionId , file : file , } ) ) . catch ( ( progressNotificationErr ) => { this . _logger . warn ( `Fail notify render about starting of opening file "${ file }" due error: ${ progressNotificationErr . message }` ) ; } ) ; this . _directReadWrite ( file , sessionId , instanceParser , options , trackingId ) . then ( ( sourceId ) => { instanceParser . destroy ( ) . then ( ( ) => { ServiceFileRecent . save ( file , stats . size ) ; const info = ServiceStreamSource . get ( sourceId ) ; if ( info !== undefined ) { ( info as IPCMessages . IStreamSourceNew ) . id = sourceId ; } ServiceStreams . addBoundFile ( sessionId , file ) ; ServiceElectron . IPC . send ( new IPCMessages . FileOpenDoneEvent ( { session : sessionId , file : file , stream : info as IPCMessages . IStreamSourceNew , options : options , } ) ) . catch ( ( confirmNotificationErr ) => { this . _logger . warn ( `Fail notify render about opening file "${ file }" due error: ${ confirmNotificationErr . message }` ) ; } ) ; resolve ( { sourceId : sourceId , options : options } ) ; } ) ; } ) . catch ( ( pipeError ) => { instanceParser . destroy ( ) . then ( ( ) => { reject ( new Error ( this . _logger . error ( `Fail to directly read file "${ file }" due error: ${ pipeError . message }` ) ) ) ; } ) ; } ) . cancel ( ( ) => { this . _logger . debug ( `Reading operation with "${ instanceParser . getAlias ( ) }" was canceled` ) ; } ) . finally ( ( ) => { this . _unsetProgress ( instanceParser , trackingId , sessionId ) ; ServiceStreams . reattachSessionFileHandle ( sessionId ) ; this . _active . delete ( sessionId ) ; } ) ; } ; if ( opts !== undefined ) { open ( opts ) ; } else { this . _getOptions ( file , path . basename ( file ) , instanceParser , stats . size ) . then ( ( options ) => { open ( options ) ; } ) . catch ( ( getOptionsError ) => { reject ( new Error ( this . _logger . error ( `File "${ file }" (${ instanceParser . getAlias ( ) }) will not be opened due error: ${ getOptionsError . message }` ) ) ) ; } ) ; } } ) . catch ( ( gettingParserError ) => { reject ( new Error ( this . _logger . warn ( `Fail to find parser due error: ${ gettingParserError . message }` ) ) ) ; } ) ; } ) ; } ) ; } public openAsNew ( file ) { return new Promise ( ( resolve , reject ) => { ServiceElectron . IPC . request ( new IPCMessages . RenderSessionAddRequest ( ) , IPCMessages . RenderSessionAddResponse ) . then ( ( response ) => { if ( response . error !== undefined ) { this . _logger . warn ( `Fail to add new session for file "${ file }" due error: ${ response . error }` ) ; return reject ( new Error ( response . error ) ) ; } this . open ( file , response . session ) . then ( ( ) => { resolve ( ) ; } ) . catch ( ( openFileErr ) => { this . _logger . warn ( `Fail to open file "${ file }" due error: ${ openFileErr . message }` ) ; reject ( openFileErr ) ; } ) ; } ) . catch ( ( addSessionErr ) => { this . _logger . warn ( `Fail to add new session for file "${ file }" due error: ${ addSessionErr . message }` ) ; reject ( addSessionErr ) ; } ) ; } ) ; } public selectAndOpenFile ( ) { return this . _openFile ( ) ; } private _onRenderReady ( ) { this . _recent . pending = false ; if ( this . _recent . path === undefined ) { return ; } const filename = this . _recent . path ; const subscription = ServiceStreams . getSubjects ( ) . onSessionCreated . subscribe ( ( session ) => { subscription . unsubscribe ( ) ; this . _logger . debug ( `Attempt to open recent ${ filename }` ) ; this . open ( filename , session . stream . guid ) . catch ( ( err ) => { ServiceNotifications . notify ( { message : this . _logger . warn ( `Fail open file "${ filename }" due error: ${ err . message }` ) , type : ENotificationType . warning , caption : 'Cannot open file' , } ) ; } ) ; } ) ; } private _ipc_FileListRequest ( request , response ) { const req = request as IPCMessages . FileListRequest ; Promise . all ( req . files . map ( ( file ) => { return this . _listFiles ( file ) ; } ) ) . then ( ( fileLists ) => { response ( new IPCMessages . FileListResponse ( { files : this . _concatFileList ( fileLists ) , } ) ) . catch ( ( error ) => { this . _logger . error ( `Fail to respond to files ${ req . files } due error: ${ error . message }` ) ; } ) ; } ) . catch ( ( error ) => { response ( new IPCMessages . FileListResponse ( { files : [ ] , error : error . message , } ) ) ; } ) ; } private _concatFileList ( fileLists ) { const cConcatFiles = [ ] ; return cConcatFiles . concat ( ... fileLists ) ; } private _onOSRecentFileOpen ( _event , filename ) { if ( this . _recent . pending ) { this . _logger . debug ( `Open recent file operation for: ${ filename } would be postponed` ) ; this . _recent . path = filename ; } else { this . _logger . debug ( `Attempt to open recent ${ filename }` ) ; this . openAsNew ( filename ) . catch ( ( err ) => { ServiceNotifications . notify ( { message : this . _logger . warn ( `Fail open file "${ filename }" due error: ${ err . message }` ) , type : ENotificationType . warning , caption : 'Cannot open file' , } ) ; } ) ; } } private _ipc_FileOpenRequest ( request , response ) { const req = request as IPCMessages . FileOpenRequest ; this . open ( req . file , req . session , undefined , req . options ) . then ( ( result ) => { const info = ServiceStreamSource . get ( result . sourceId ) ; if ( info !== undefined ) { ( info as IPCMessages . IStreamSourceNew ) . id = result . sourceId ; } response ( new IPCMessages . FileOpenResponse ( { stream : info as IPCMessages . IStreamSourceNew , options : result . options , } ) ) ; } ) . catch ( ( openError ) => { response ( new IPCMessages . FileOpenResponse ( { error : openError . message , stream : undefined , } ) ) ; } ) ; } private _listFiles ( startFile ) { return new Promise ( ( resolve , reject ) => { const allFiles = [ ] ; const self = this ; function listAllFiles ( file ) { return new Promise ( ( resolved , rejected ) => { fs . lstat ( file , ( lsErr , stats ) => { if ( lsErr ) { return resolved ( allFiles . push ( { hasParser : false , isHidden : false , lastModified : 0 , lastModifiedDate : new Date ( ) , name : path . basename ( file ) , path : file , size : 0 , type : 'file' , checked : false , disabled : true , features : { merge : false , concat : false , } , } ) ) ; } if ( stats . isFile ( ) ) { if ( stats . size === 0 ) { return resolved ( allFiles . push ( { hasParser : false , isHidden : false , lastModified : stats . mtimeMs , lastModifiedDate : stats . mtime , name : path . basename ( file ) , path : file , size : stats . size , type : 'file' , checked : false , disabled : true , features : { concat : true , merge : true , } , } ) ) ; } Promise . all ( [ getParserForFile ( file ) , isHidden ( file ) ] ) . then ( ( values ) => { const parser = values [ 0 ] ; const hideStatus = values [ 1 ] === undefined ? false : values [ 1 ] ; return resolved ( allFiles . push ( { hasParser : parser === undefined ? false : true , isHidden : hideStatus , lastModified : stats . mtimeMs , lastModifiedDate : stats . mtime , name : path . basename ( file ) , path : file , size : stats . size , type : 'file' , checked : parser === undefined || hideStatus ? false : true , disabled : false , features : { merge : parser === undefined ? false : parser . getAlias ( ) === Parsers . FileParserText . getAlias ( ) , concat : parser === undefined ? false : parser . getAlias ( ) === Parsers . FileParserText . getAlias ( ) , } , } ) ) ; } ) . catch ( ( error ) => { self . _logger . warn ( `Fail to indentify parser of ${ file } due to error: ${ error . message }` ) ; return resolved ( allFiles . push ( { hasParser : false , isHidden : false , lastModified : stats . mtimeMs , lastModifiedDate : stats . mtime , name : path . basename ( file ) , path : file , size : stats . size , type : 'file' , checked : false , disabled : true , features : { merge : false , concat : false , } , } ) ) ; } ) ; } else if ( ! stats . isDirectory ( ) ) { return rejected ( new Error ( self . _logger . warn ( `Fail to get file info of ${ file } because it is neither a file nor a directory` ) ) ) ; } else { return fs . readdir ( file , ( err , files ) => { if ( err ) { self . _logger . warn ( `Fail to list files of directory ${ file } due to error: ${ err . message }` ) ; return resolved ( [ ] ) ; } else { Promise . all ( files . map ( ( subFile ) => { return listAllFiles ( file + '/' + subFile ) ; } ) ) . then ( ( ) => { resolved ( allFiles ) ; } ) . catch ( ( error ) => { rejected ( new Error ( self . _logger . warn ( `Fail to list files of directory ${ file } info due to error: ${ error . message }` ) ) ) ; } ) ; } } ) ; } } ) ; } ) ; } listAllFiles ( startFile ) . then ( ( ) => { resolve ( allFiles ) ; } ) . catch ( ( error ) => { reject ( new Error ( self . _logger . warn ( `Fail to list files of directory ${ startFile } info due to error: ${ error . message }` ) ) ) ; } ) ; } ) ; } private _getOptions ( fullFileName , fileName , parser , size ) { return new Promise ( ( resolve , reject ) => { ServiceElectron . IPC . request ( new IPCMessages . FileGetOptionsRequest ( { fileName : fileName , fullFileName : fullFileName , type : parser . getAlias ( ) , size : size , session : ServiceStreams . getActiveStreamId ( ) , } ) , IPCMessages . FileGetOptionsResponse ) . then ( ( response ) => { if ( ! response . allowed ) { return resolve ( false ) ; } resolve ( response . options ) ; } ) . catch ( ( error ) => { reject ( error ) ; } ) ; } ) ; } private _directReadWrite ( file , sessionId , parser , options , trackingId ) { return new Tools . CancelablePromise < number , void > ( ( resolve , reject , cancel ) => { const sourceId = ServiceStreamSource . add ( { name : path . basename ( file ) , session : sessionId , meta : parser . getMeta ( ) , } ) ; const dest = ServiceStreams . getStreamFile ( ) ; if ( dest instanceof Error ) { return reject ( dest ) ; } if ( parser . parseAndIndex === undefined ) { return reject ( new Error ( `This case isn't possible, but typescript compile.` ) ) ; } parser . parseAndIndex ( file , dest . file , sourceId , options , ( map ) => { ServiceStreams . pushToStreamFileMap ( dest . streamId , map ) ; } , ( ticks ) => { this . _incrProgress ( parser , trackingId , dest . streamId , ticks . ellapsed / ticks . total ) ; } ) . then ( ( map ) => { resolve ( sourceId ) ; } ) . cancel ( ( ) => { cancel ( ) ; } ) . catch ( ( error ) => { reject ( error ) ; } ) ; } ) ; } private _openFile ( ) { return new Promise ( ( resolve , reject ) => { const win = ServiceElectron . getBrowserWindow ( ) ; if ( win === undefined ) { return reject ( new Error ( `No window found` ) ) ; } dialog . showOpenDialog ( win , { properties : [ 'openFile' , 'multiSelections' , 'showHiddenFiles' ] , filters : [ { name : 'All Files' , extensions : [ '*' ] } ] , } ) . then ( ( returnValue ) => { if ( ! ( returnValue . filePaths instanceof Array ) ) { return resolve ( undefined ) ; } else if ( returnValue . filePaths . length > 1 ) { return ServiceElectron . IPC . send ( new IPCMessages . FilesOpenEvent ( { files : returnValue . filePaths } ) ) . catch ( ( error ) => { this . _logger . error ( `Fail to open files due to error: ${ error . message }` ) ; reject ( new Error ( `Fail to open files due to error: ${ error . message }` ) ) ; } ) ; } const filename = returnValue . filePaths [ 0 ] ; getParserForFile ( filename ) . then ( ( _parser ) => { if ( _parser === undefined ) { this . _logger . error ( `Fail to find a parser for file: ${ filename }` ) ; return reject ( new Error ( `Fail to find a parser for file: ${ filename }` ) ) ; } ServiceElectron . IPC . request ( new IPCMessages . RenderSessionAddRequest ( ) , IPCMessages . RenderSessionAddResponse ) . then ( ( response ) => { if ( response . error !== undefined ) { this . _logger . warn ( `Fail to add new session for file "${ filename }" due error: ${ response . error }` ) ; return reject ( new Error ( `Fail to add new session for file "${ filename }" due error: ${ response . error }` ) ) ; } this . open ( filename , response . session , _parser ) . then ( ( ) => { resolve ( filename ) ; } ) . catch ( ( error ) => { this . _logger . warn ( `Fail open file "${ filename }" due error: ${ error . message }` ) ; reject ( new Error ( `Fail open file "${ filename }" due error: ${ error . message }` ) ) ; } ) ; } ) . catch ( ( addSessionErr ) => { this . _logger . warn ( `Fail to add new session for file "${ filename }" due error: ${ addSessionErr . message }` ) ; reject ( new Error ( `Fail to add new session for file "${ filename }" due error: ${ addSessionErr . message }` ) ) ; } ) ; } ) . catch ( ( error ) => { this . _logger . error ( `Error to open file "${ filename }" due error: ${ error . message }` ) ; reject ( new Error ( `Error to open file "${ filename }" due error: ${ error . message }` ) ) ; } ) ; } ) . catch ( ( error ) => { this . _logger . error ( `Fail open file due error: ${ error . message }` ) ; reject ( new Error ( `Fail open file due error: ${ error . message }` ) ) ; } ) ; } ) ; } private _hotkey_openLocalFile ( ) { this . _openFile ( ) . catch ( ( error ) => { this . _logger . error ( `Fail open file on CMD + P: ${ error . message }` ) ; } ) ; } private _setProgress ( parser , trackingId , file ) { ServiceStreams . addProgressSession ( trackingId , file ) ; } private _unsetProgress ( parser , trackingId , sessionId ) { ServiceStreams . removeProgressSession ( trackingId , sessionId ) ; } private _incrProgress ( parser , trackingId , sessionId , value ) { ServiceStreams . updateProgressSession ( trackingId , value , sessionId ) ; } private _onSessionClosed ( guid ) { const task = this . _active . get ( guid ) ; if ( task === undefined ) { return ; } task . abort ( ) . then ( ( ) => { this . _logger . debug ( `Session "${ guid }" is closed. Task "${ task . getAlias ( ) }" is aborted.` ) ; } ) ; } } export default new ServiceFileOpener ( ) ;