import { readdir , unlink , mkdir as _mkdir } from 'fs' ; import { promisify } from 'util' ; import { join , extname } from 'path' ; import { AnyFunction } from '../../shared/UtilFunctions' ; import { devLog } from '../../shared/ElectronUtilFunctions' ; import * as node7z from 'node-7z' ; import * as zipBin from '7zip-bin' ; import * as unrarjs from 'node-unrar-js' ; import { FailReason } from 'node-unrar-js/dist/js/extractor' ; import { DownloadError } from './ChartDownload' ; const mkdir = promisify ( _mkdir ) ; type EventCallback = { 'start' : ( filename : string ) => void ; 'extractProgress' : ( percent : number , fileCount : number ) => void ; 'error' : ( err : DownloadError , retry : ( ) => void | Promise < void > ) => void ; 'complete' : ( ) => void ; } ; type Callbacks = { [ E in keyof EventCallback ] : EventCallback [ E ] ; } ; const extractErrors = { readError : ( err ) => { return { header : `Failed to read file (${ err . code })` , body : `${ err . name }: ${ err . message }` } ; } , emptyError : ( ) => { return { header : 'Failed to extract archive' , body : 'File archive was downloaded but could not be found' } ; } , rarmkdirError : ( err , sourceFile ) => { return { header : `Extracting archive failed. (${ err . code })` , body : `${ err . name }: ${ err . message } (${ sourceFile })` } ; } , rarextractError : ( result , sourceFile ) => { return { header : `Extracting archive failed: ${ result . reason }` , body : `${ result . msg } (${ sourceFile })` } ; } } ; export class FileExtractor { private callbacks = { } as Callbacks ; private wasCanceled = false ; constructor ( private sourceFolder ) { } /**
     * Calls `callback` when `event` fires. (no events will be fired after `this.cancelExtract()` is called)
     */ on < E extends keyof EventCallback > ( event , callback ) { this . callbacks [ event ] = callback ; } /**
     * Extract the chart from `this.sourceFolder`. (assumes there is exactly one archive file in that folder)
     */ beginExtract ( ) { setTimeout ( this . cancelable ( ( ) => { readdir ( this . sourceFolder , ( err , files ) => { if ( err ) { this . callbacks . error ( extractErrors . readError ( err ) , ( ) => this . beginExtract ( ) ) ; } else if ( files . length == 0 ) { this . callbacks . error ( extractErrors . emptyError ( ) , ( ) => this . beginExtract ( ) ) ; } else { this . callbacks . start ( files [ 0 ] ) ; this . extract ( join ( this . sourceFolder , files [ 0 ] ) , extname ( files [ 0 ] ) == '.rar' ) ; } } ) ; } ) , 100 ) ; } /**
     * Extracts the file at `fullPath` to `this.sourceFolder`.
     */ private async extract ( fullPath , useRarExtractor ) { if ( useRarExtractor ) { await this . extractRar ( fullPath ) ; } else { this . extract7z ( fullPath ) ; } } throws private async extractRar ( fullPath ) { const extractor = unrarjs . createExtractorFromFile ( fullPath , this . sourceFolder ) ; const fileList = extractor . getFileList ( ) ; if ( fileList [ 0 ] . state != 'FAIL' ) { const headers = fileList [ 1 ] . fileHeaders ; for ( const header of headers ) { if ( header . flags . directory ) { try { await mkdir ( join ( this . sourceFolder , header . name ) , { recursive : true } ) ; } catch ( err ) { this . callbacks . error ( extractErrors . rarmkdirError ( err , fullPath ) , ( ) => this . extract ( fullPath , extname ( fullPath ) == '.rar' ) ) ; return ; } } } } const extractResult = extractor . extractAll ( ) ; if ( extractResult [ 0 ] . state == 'FAIL' ) { this . callbacks . error ( extractErrors . rarextractError ( extractResult [ 0 ] , fullPath ) , ( ) => this . extract ( fullPath , extname ( fullPath ) == '.rar' ) ) ; } else { this . deleteArchive ( fullPath ) ; } } /**
     * Extracts a .zip or .7z archive found at `fullPath` and puts the extracted results in `this.sourceFolder`.
     */ private extract7z ( fullPath ) { const zipBinPath = zipBin . path7za . replace ( 'app.asar' , 'app.asar.unpacked' ) ; const stream = node7z . extractFull ( fullPath , this . sourceFolder , { $progress : true , $bin : zipBinPath } ) ; stream . on ( 'progress' , this . cancelable ( ( progress ) => { this . callbacks . extractProgress ( progress . percent , isNaN ( progress . fileCount ) ? 0 : progress . fileCount ) ; } ) ) ; let extractErrorOccured = false ; stream . on ( 'error' , this . cancelable ( ( ) => { extractErrorOccured = true ; devLog ( `Failed to extract [${ fullPath }]; retrying with .rar extractor...` ) ; this . extract ( fullPath , true ) ; } ) ) ; stream . on ( 'end' , this . cancelable ( ( ) => { if ( ! extractErrorOccured ) { this . deleteArchive ( fullPath ) ; } } ) ) ; } /**
     * Tries to delete the archive at `fullPath`.
     */ private deleteArchive ( fullPath ) { unlink ( fullPath , this . cancelable ( ( err ) => { if ( err && err . code != 'ENOENT' ) { devLog ( `Warning: failed to delete archive at [${ fullPath }]` ) ; } this . callbacks . complete ( ) ; } ) ) ; } /**
     * Stop the process of extracting the file. (no more events will be fired after this is called)
     */ cancelExtract ( ) { this . wasCanceled = true ; } /**
     * Wraps a function that is able to be prevented if `this.cancelExtract()` was called.
     */ private cancelable < F extends AnyFunction > ( fn ) { return ( ... args ) : ReturnType < F > => { if ( this . wasCanceled ) { return ; } return fn ( ... Array . from ( args ) ) ; } ; } }