import * as path from 'path' ; import * as FS from '../../tools/fs' ; import * as fs from 'fs' ; import * as Tools from '../../tools/index' ; import * as semver from 'semver' ; import Logger from '../../tools/env.logger' ; import ServicePaths from '../../services/service.paths' ; import ServiceElectronService from '../../services/service.electron.state' ; import ServicePackage from '../../services/service.package' ; import GitHubClient , { IReleaseAsset , IReleaseData , GitHubAsset } from '../../tools/env.github.client' ; import { getPlatform , EPlatforms } from '../../tools/env.os' ; import { download } from '../../tools/env.net' ; import { CommonInterfaces } from '../../interfaces/interface.common' ; import { getValidPluginReleaseInfo } from './plugins.validator' ; const CSettings = { user : 'esrlabs' , repo : 'chipmunk-plugins-store' , registerListFile : 'releases-{platform}.json' } ; const CFileNamePattern = /[\w\d-_\.]*@\d*\.\d*\.\d*-\d*\.\d*.\d*-\w*\.tgz$/gi ; class description export default class ControllerPluginStore { private _logger : Logger = new Logger ( `ControllerPluginStore ("${ CSettings . user }/${ CSettings . repo }")` ) ; private _remote : Map < string , CommonInterfaces . Plugins . IPlugin > | undefined = undefined ; private _local : Map < string , CommonInterfaces . Plugins . IPlugin > = new Map ( ) ; private _downloads : Map < string , boolean > = new Map ( ) ; public destroy ( ) { return new Promise ( ( resolve , reject ) => { resolve ( ) ; } ) ; } public local ( ) { return new Promise ( ( resolve ) => { this . _setupLocalReleaseNotes ( ) . then ( ( plugins ) => { this . _local = plugins ; resolve ( ) ; } ) . catch ( ( error ) => { this . _logger . warn ( `Fail to get plugin's register due error: ${ error . message }` ) ; resolve ( ) ; } ) ; } ) ; } public remote ( ) { return new Promise ( ( resolve , reject ) => { this . _setupRemoteReleaseNotes ( ) . then ( ( plugins ) => { this . _remote = plugins ; resolve ( ) ; } ) . catch ( ( error ) => { this . _logger . warn ( `Fail to get plugin's register due error: ${ error . message }` ) ; resolve ( ) ; } ) ; } ) ; } public download ( name , version ) { return new Promise ( ( resolve , reject ) => { ServiceElectronService . logStateToRender ( `Downloading plugin "${ name }"...` ) ; const plugin = this . getInfo ( name ) ; if ( plugin === undefined ) { return reject ( new Error ( this . _logger . warn ( `Plugin "${ name }" isn't found.` ) ) ) ; } this . delivery ( plugin . name , version ) . then ( ( filename ) => { resolve ( filename ) ; } ) . catch ( ( deliveryErr ) => { reject ( new Error ( this . _logger . warn ( `Fail to delivery plugin "${ plugin . name }" due error: ${ deliveryErr . message }` ) ) ) ; } ) ; } ) ; } public getDefaults ( exclude ) { return Array . from ( this . getRegister ( ) . values ( ) ) . filter ( ( plugin ) => { return exclude . indexOf ( plugin . name ) === - 1 && plugin . default ; } ) . map ( ( plugin ) => { return plugin . name ; } ) ; } public isDefault ( name ) { let defaults = false ; Array . from ( this . getRegister ( ) . values ( ) ) . filter ( ( plugin ) => { if ( defaults ) { return ; } if ( plugin . name === name ) { defaults = plugin . default ; } } ) ; return defaults ; } public getAvailable ( ) { return Array . from ( this . getRegister ( ) . values ( ) ) . map ( ( plugin ) => { return Object . assign ( { } , plugin ) ; } ) ; } public getSuitableVersions ( name ) { const plugin = this . getPluginReleaseInfo ( name ) ; if ( plugin === undefined ) { return [ ] ; } const history = plugin . history . filter ( ( record ) => { if ( ! semver . valid ( record . version ) ) { this . _logger . warn ( `Plugin "name" has bad record in history. Version "${ record . version }" is invalid.` ) ; return false ; } return record . hash === ServicePackage . getHash ( ) ; } ) . map ( ( record ) => { return Object . assign ( { } , record ) ; } ) ; if ( history . length === 0 && plugin . hash === ServicePackage . getHash ( ) ) { history . push ( { hash : plugin . hash , phash : plugin . phash , version : plugin . version , url : plugin . url , } ) ; } return history ; } public getLatestVersion ( name ) { const versions = this . getSuitableVersions ( name ) ; if ( versions . length === 0 ) { return undefined ; } return versions [ 0 ] ; } public getRegister ( ) { return this . _remote === undefined ? this . _local : this . _remote ; } public delivery ( name , version ) { return new Promise ( ( resolve , reject ) => { const plugin = this . getRegister ( ) . get ( name ) ; if ( plugin === undefined ) { return reject ( new Error ( `Fail to find plugin in register` ) ) ; } const url = this . _getUrl ( plugin . name , version ) ; if ( url === undefined ) { return reject ( new Error ( `Fail to find plugin's url for "${ plugin . name }@${ version }"` ) ) ; } const urlfilename = this . _getFileNameFromURL ( url ) ; const filename = urlfilename instanceof Error ? plugin . file : urlfilename ; const target = path . resolve ( ServicePaths . getDownloads ( ) , filename ) ; this . _findLocally ( filename ) . then ( ( file ) => { resolve ( file ) ; } ) . catch ( ( error ) => { if ( this . _downloads . has ( target ) ) { return reject ( new Error ( `Plugin "${ plugin . name }" (${ target }) is already downloading.` ) ) ; } this . _downloads . set ( target , true ) ; this . _logger . debug ( `Plugin "${ plugin . name }" isn't found and will be downloaded: ${ error . message }` ) ; const temp = path . resolve ( ServicePaths . getDownloads ( ) , Tools . guid ( ) ) ; FS . unlink ( target ) . then ( ( ) => { download ( url , temp ) . then ( ( ) => { fs . stat ( temp , ( statErr , stat ) => { if ( statErr ) { return reject ( new Error ( this . _logger . warn ( `Fail download file "${ temp }" due error: ${ statErr . message }` ) ) ) ; } this . _logger . debug ( `Plugin "${ name }" is downloaded:\n\t- file: ${ temp } (${ target }) \n\t- size: ${ stat . size } bytes` ) ; fs . rename ( temp , target , ( renameErr ) => { if ( renameErr ) { return reject ( new Error ( this . _logger . warn ( `Fail rename file "${ temp }" to "${ target }" due error: ${ renameErr . message }` ) ) ) ; } resolve ( target ) ; } ) ; } ) ; } ) . catch ( ( downloadErr ) => { reject ( new Error ( this . _logger . warn ( `Fail to download file "${ target }" due error: ${ downloadErr . message }` ) ) ) ; } ) ; } ) . catch ( ( unlinkErr ) => { reject ( new Error ( this . _logger . warn ( `Fail to remove file "${ target }" due error: ${ unlinkErr . message }` ) ) ) ; } ) ; } ) ; } ) ; } public getPluginReleaseInfo ( name ) { const register = this . getRegister ( ) ; return register . get ( name ) ; } public getInfo ( name ) { return this . _remote === undefined ? this . _local . get ( name ) : this . _remote . get ( name ) ; } private _getUrl ( name , version ) { const info = this . getInfo ( name ) ; if ( info === undefined ) { return undefined ; } if ( info . version === version && info . hash === ServicePackage . getHash ( ) ) { return info . url ; } let url ; info . history . forEach ( ( record ) => { if ( record . version === version && record . hash === ServicePackage . getHash ( ) ) { url = record . url ; } } ) ; return url ; } private _getFileNameFromURL ( url ) { const match = url . match ( CFileNamePattern ) ; if ( match === null || match . length !== 1 ) { return new Error ( `Fail to extract filename from url: ${ url }` ) ; } return match [ 0 ] ; } private _findLocally ( filename ) { return new Promise ( ( resolve , reject ) => { this . _findIn ( filename , 'includes' ) . then ( ( file ) => { resolve ( file ) ; } ) . catch ( ( inclErr ) => { this . _logger . debug ( `Fail to find a plugin "${ filename }" in included due error: ${ inclErr . message }` ) ; this . _findIn ( filename , 'downloads' ) . then ( ( file ) => { resolve ( file ) ; } ) . catch ( ( downloadsErr ) => { this . _logger . debug ( `Fail to find a plugin "${ filename }" in downloads due error: ${ downloadsErr . message }` ) ; reject ( new Error ( `Plugin "${ filename }" isn't found.` ) ) ; } ) ; } ) ; } ) ; } private _findIn ( filename , source ) { return new Promise ( ( resolve , reject ) => { const folder = source === 'includes' ? ServicePaths . getIncludedPlugins ( ) : ServicePaths . getDownloads ( ) ; const fullname = path . resolve ( folder , filename ) ; FS . exist ( fullname ) . then ( ( exist ) => { if ( ! exist ) { return reject ( new Error ( `Plugin "${ fullname }" isn't found.` ) ) ; } resolve ( fullname ) ; } ) . catch ( ( error ) => { reject ( error ) ; } ) ; } ) ; } private _setupRemoteReleaseNotes ( ) { return new Promise ( ( resolve , reject ) => { ServiceElectronService . logStateToRender ( `Getting plugin's store state.` ) ; GitHubClient . getLatestRelease ( { user : CSettings . user , repo : CSettings . repo } ) . then ( ( release ) => { if ( release . map === undefined ) { return reject ( new Error ( this . _logger . warn ( `Plugins-store repo doesn't have any assets in latest release.` ) ) ) ; } const filename = this . _getRegisterFileName ( ) ; const asset = release . map . get ( filename ) ; if ( asset === undefined ) { return reject ( new Error ( this . _logger . warn ( `Fail to find "${ filename }" in assets of latest release.` ) ) ) ; } asset . raw ( ) . then ( ( raw ) => { try { const list = JSON . parse ( raw ) ; if ( ! ( list instanceof Array ) ) { return reject ( new Error ( this . _logger . warn ( `Incorrect format of asseets` ) ) ) ; } const plugins = new Map ( ) ; list . forEach ( ( plugin ) => { const valid = getValidPluginReleaseInfo ( plugin ) ; if ( valid instanceof Error ) { this . _logger . warn ( `Fail to validate plugin: ${ JSON . stringify ( plugin ) }` ) ; } else { plugins . set ( valid . name , valid ) ; } } ) ; ServiceElectronService . logStateToRender ( `Information of last versions of plugins has been gotten` ) ; resolve ( plugins ) ; } catch ( e ) { return reject ( new Error ( this . _logger . warn ( `Fail parse asset to JSON due error: ${ e . message }` ) ) ) ; } } ) . catch ( ( assetErr ) => { this . _logger . warn ( `Fail get asset due error: ${ assetErr . message }` ) ; reject ( assetErr ) ; } ) ; } ) . catch ( ( error ) => { reject ( new Error ( this . _logger . warn ( `Fail get latest release due error: ${ error . message }` ) ) ) ; } ) ; } ) ; } private _setupLocalReleaseNotes ( ) { return new Promise ( ( resolve , reject ) => { const local = path . resolve ( ServicePaths . getIncludedPlugins ( ) , this . _getRegisterFileName ( ) ) ; FS . exist ( local ) . then ( ( exist ) => { if ( ! exist ) { return reject ( new Error ( `Fail to find local register "${ local }"` ) ) ; } FS . readTextFile ( local ) . then ( ( json ) => { try { const list = JSON . parse ( json ) ; if ( ! ( list instanceof Array ) ) { return reject ( new Error ( this . _logger . warn ( `Incorrect format of asseets` ) ) ) ; } const plugins = new Map ( ) ; list . forEach ( ( plugin ) => { const valid = getValidPluginReleaseInfo ( plugin ) ; if ( valid instanceof Error ) { this . _logger . warn ( `Fail to validate plugin: ${ JSON . stringify ( plugin ) }` ) ; } else { plugins . set ( valid . name , valid ) ; } } ) ; ServiceElectronService . logStateToRender ( this . _logger . debug ( `Information of last versions of plugins has been gotten` ) ) ; resolve ( plugins ) ; } catch ( e ) { return reject ( new Error ( this . _logger . warn ( `Fail parse asset (from local store) to JSON due error: ${ e . message }` ) ) ) ; } } ) . catch ( ( readErr ) => { reject ( readErr ) ; } ) ; } ) . catch ( ( exErr ) => { reject ( exErr ) ; } ) ; } ) ; } private _getRegisterFileName ( ) { return CSettings . registerListFile . replace ( '{platform}' , getPlatform ( true ) ) ; } }