import { createHash } from 'crypto' ; import { makeRe } from 'minimatch' ; import * as fs from 'fs' ; import { basename , join , dirname } from 'path' ; import { createGunzip } from 'zlib' ; import axios from 'axios' ; import { app , BrowserWindow } from 'electron' ; const request = axios . create ( { adapter : require ( "axios/lib/adapters/http" ) } ) ; import { hotPublishConfig } from '../config/hotPublish' ; import { gt } from 'semver' ; import { version } from '../../../package.json' ; import { spawn } from 'child_process' ; type HashedFolderAndFileType = HashedFile | HashedFolder ; class HashedFile { name : string ; hash : string ; } type HashedFolderAndFileTypeObject = { [ index ] : HashedFolderAndFileType ; } ; class HashedFolder extends HashedFile { children ? : HashedFolderAndFileType [ ] ; childrenObject ? : HashedFolderAndFileTypeObject ; } param { HashedFolder } data function handleHashedFolderChildrenToObject ( data ) { if ( data && ( data as HashedFolder ) . children ) { ( data as HashedFolder ) . childrenObject = ( data as HashedFolder ) . children . reduce ( ( prev , next ) => { if ( ( next as HashedFolder ) . children ) { handleHashedFolderChildrenToObject ( next ) ; } prev [ next . name ] = next ; return prev ; } , { } as HashedFolderAndFileTypeObject ) ; } } class diffVersionHashResultItem { file_path : string ; hash : string ; } class diffVersionHashResult { constructor ( ) { this . added = [ ] ; this . changed = [ ] ; } added : diffVersionHashResultItem [ ] ; changed : diffVersionHashResultItem [ ] ; } param { HashedFolder } oldVersion param { HashedFolder } newVersion param { * } result param { "" } path returns function diffVersionHash ( oldVersion , newVersion , result = new diffVersionHashResult ( ) , path = "." ) { if ( newVersion . hash === oldVersion . hash ) { return result ; } else if ( newVersion . children ) { newVersion . children . forEach ( item => { if ( oldVersion . childrenObject && oldVersion . childrenObject [ item . name ] ) { diffVersionHash ( oldVersion . childrenObject [ item . name ] , item , result , path + "/" + item . name ) ; } else { pushdiffVersionHashResult ( item , path + "/" + item . name , result . added ) ; } } ) ; } else { result . changed . push ( { file_path : path , hash : newVersion . hash } ) ; } return result ; } param { HashedFolderAndFileType } item param { string } path param { diffVersionHashResultItem [ ] } resultAdd function pushdiffVersionHashResult ( item , path , resultAdd ) { if ( ( item as HashedFolder ) . children ) { ( item as HashedFolder ) . children . forEach ( child => { pushdiffVersionHashResult ( child , path + "/" + child . name , resultAdd ) ; } ) ; } else { resultAdd . push ( { file_path : path , hash : item . hash } ) ; } } param { ( string [ ] | undefined | ( ( str : string ) => boolean ) ) } globs param { string } param returns { boolean } function reduceGlobPatterns ( globs , param ) { if ( typeof globs === 'function' ) { return globs ( param ) ; } else if ( ! globs || ! Array . isArray ( globs ) || globs . length === 0 ) { return false ; } else { const regex = new RegExp ( globs . reduce ( ( acc , exclude ) => { return acc + '|' + makeRe ( exclude ) . source ; } , '' ) . substr ( 1 ) ) ; return regex . test ( param ) ; } } interface interface hashElementOptions { folders ? : { exclude ? : string [ ] ; } ; files ? : { exclude ? : string [ ] ; } ; } param { ( Buffer | string ) } data returns function hash256 ( data ) { return createHash ( 'sha256' ) . update ( data ) . digest ( 'hex' ) ; } param { string } dir_or_file_path param { hashElementOptions } options returns { HashedFolderAndFileType } function hashElement ( dir_or_file_path , options = { } ) { let file_or_dir = fs . statSync ( dir_or_file_path ) ; let base_name = basename ( dir_or_file_path ) ; if ( file_or_dir . isFile ( ) ) { if ( reduceGlobPatterns ( options ?. files ?. exclude , base_name ) ) { return null ; } let result = new HashedFile ( ) ; result . name = base_name ; result . hash = hash256 ( fs . readFileSync ( dir_or_file_path ) ) ; return result ; } else if ( file_or_dir . isDirectory ( ) ) { if ( reduceGlobPatterns ( options ?. folders ?. exclude , base_name ) ) { return null ; } let result = new HashedFolder ( ) ; result . name = base_name ; result . children = fs . readdirSync ( dir_or_file_path ) . map ( item => { return hashElement ( join ( dir_or_file_path , item ) , options ) ; } ) . filter ( item => item !== null ) ; result . hash = hash256 ( result . children . map ( item => item . hash ) . join ( '' ) ) ; return result ; } return null ; } const updateInfo = { status : 'init' , message : '' } ; param { string } path export async function updater ( windows ? ) { const dirDirectory = dirname ( app . getPath ( 'exe' ) ) ; const tempDirectory = join ( dirDirectory , hotPublishConfig . tempDirectory ) ; if ( ! fs . existsSync ( tempDirectory ) ) { fs . mkdirSync ( tempDirectory ) ; } try { const res = await request ( { url : `${ hotPublishConfig . url }/${ hotPublishConfig . configName }.json?time=${ new Date ( ) . getTime ( ) }` , } ) ; if ( gt ( res . data . version , version ) ) { updateInfo . status = 'downloading' ; const options = { files : { } , } ; let hash = hashElement ( dirDirectory , options ) ; handleHashedFolderChildrenToObject ( hash ) ; let diff_result = diffVersionHash ( hash , res . data . hash as HashedFolder ) ; fs . writeFileSync ( join ( tempDirectory , hotPublishConfig . configName + '.json' ) , JSON . stringify ( diff_result , null , 2 ) ) ; console . log ( diff_result ) ; if ( windows ) windows . webContents . send ( 'hot-update-status' , updateInfo ) ; await Promise . all ( diff_result . changed . map ( item => { return downAndungzip ( item . hash , join ( tempDirectory , item . hash ) , res . data . version ) ; } ) ) ; await Promise . all ( diff_result . added . map ( item => { return downAndungzip ( item . hash , join ( tempDirectory , item . hash ) , res . data . version ) ; } ) ) ; console . log ( 'complete' ) ; updateInfo . status = 'finished' ; if ( windows ) windows . webContents . send ( 'hot-update-status' , updateInfo ) ; spawn ( "updater" , { detached : true , env : { exe_path : app . getPath ( "exe" ) , update_temp_path : tempDirectory , update_config_file_name : hotPublishConfig . configName + ".json" } , stdio : 'ignore' } ) ; } } catch ( error ) { console . log ( error ) ; updateInfo . status = 'failed' ; updateInfo . message = error ; if ( windows ) windows . webContents . send ( 'hot-update-status' , updateInfo ) ; } } param { string } source param { string } targetPath returns function downAndungzip ( source , targetPath , version ) { const sourceUrl = `${ hotPublishConfig . url }/${ hotPublishConfig . gzipDirectory + version }/${ source }.gz?time=${ new Date ( ) . getTime ( ) }` ; return new Promise ( ( resolve , reject ) => { if ( fs . existsSync ( targetPath ) && ( hash256 ( fs . readFileSync ( targetPath ) ) === source ) ) { resolve ( true ) ; } else { const hash = createHash ( 'sha256' ) ; let ungz = createGunzip ( ) ; let writeStream = fs . createWriteStream ( targetPath ) ; request ( { method : 'get' , url : sourceUrl , responseType : 'stream' , } ) . then ( response => { response . data . pipe ( ungz ) ; } , err => { reject ( false ) ; return ; } ) ; ungz . on ( "error" , ( ) => { ungz . close ( ) ; writeStream . close ( ) ; hash . destroy ( ) ; reject ( false ) ; return ; } ) ; ungz . on ( 'data' , ( chunk ) => { hash . write ( chunk ) ; writeStream . write ( chunk ) ; } ) ; ungz . on ( "end" , ( ) => { if ( hash . digest ( 'hex' ) !== source ) { hash . destroy ( ) ; reject ( false ) ; } else { resolve ( true ) ; } } ) ; } } ) ; }