/**
 * This file is part of the vscode-remote-workspace distribution.
 * Copyright (c) Marcel Joachim Kloubert.
 *
 * vscode-remote-workspace is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, version 3.
 *
 * vscode-remote-workspace is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */ import * as _ from 'lodash' ; import * as FS from 'fs' ; import * as FSExtra from 'fs-extra' ; const FTP = require ( '@icetee/ftp' ) ; const FTP_Legacy = require ( 'ftp' ) ; import * as Moment from 'moment' ; import * as MomentTZ from 'moment-timezone' ; import * as Path from 'path' ; import * as vscode from 'vscode' ; import * as vscode_helpers from 'vscode-helpers' ; import * as vscrw from '../extension' ; import * as vscrw_fs from '../fs' ; interface FTPsConnection { cache : FTPsConnectionCache ; client : any ; followSymLinks : boolean ; noQueue : boolean ; queue : any ; } interface FTPsConnectionCache { stats : any ; } interface FTPsFileStat extends vscode . FileStat { } /**
 * Secure FTP file system.
 */ export class FTPsFileSystem extends vscrw_fs . FileSystemBase { private readonly _EXECUTE_REMOTE_COMMAND_LISTENER : vscrw . EventListener ; /**
     * Initializes a new instance of that class.
     */ public constructor ( ) { super ( ) ; this . _EXECUTE_REMOTE_COMMAND_LISTENER = ( execArgs ) => { execArgs . increaseExecutionCounter ( ) ; ( async ( ) => { try { if ( FTPsFileSystem . scheme === vscode_helpers . normalizeString ( execArgs . uri . scheme ) ) { const RESPONSE = await this . executeRemoteCommand ( execArgs ) ; if ( execArgs . callback ) { execArgs . callback ( null , { stdOut : RESPONSE , } ) ; } } } catch ( e ) { if ( execArgs . callback ) { execArgs . callback ( e ) ; } else { throw e ; } } } ) ( ) . then ( ( ) => { } , ( err ) => { vscrw . showError ( err ) ; } ) ; } ; vscode_helpers . EVENTS . on ( vscrw . EVENT_EXECUTE_REMOTE_COMMAND , this . _EXECUTE_REMOTE_COMMAND_LISTENER ) ; } inheritdoc public createDirectory ( uri ) { return this . forConnection ( uri , async ( conn ) => { return new Promise < void > ( ( resolve , reject ) => { const COMPLETED = vscode_helpers . createCompletedAction ( resolve , reject ) ; try { conn . client . mkdir ( vscrw . normalizePath ( uri . path ) , true , ( err ) => { COMPLETED ( err ) ; } ) ; } catch ( e ) { COMPLETED ( e ) ; } } ) ; } ) ; } inheritdoc public delete ( uri , options ) { return this . forConnection ( uri , async ( conn ) => { const STAT = await this . statInner ( uri ) ; return new Promise < void > ( ( resolve , reject ) => { const COMPLETED = vscode_helpers . createCompletedAction ( resolve , reject ) ; try { const PATH = vscrw . normalizePath ( uri . path ) ; if ( vscode . FileType . Directory === STAT . type ) { conn . client . rmdir ( PATH , options . recursive , ( err ) => { COMPLETED ( err ) ; } ) ; } else { conn . client . delete ( PATH , ( err ) => { COMPLETED ( err ) ; } ) ; } } catch ( e ) { COMPLETED ( e ) ; } } ) ; } ) ; } private async executeRemoteCommand ( execArgs ) { const CONN = await this . openConnection ( execArgs . uri , true ) ; try { const CONN = await this . openConnection ( execArgs . uri , true ) ; try { await executeServerCommand ( CONN . client , 'CWD ' + vscrw . normalizePath ( execArgs . uri . path ) ) ; return await executeServerCommand ( CONN . client , execArgs . command ) ; } finally { tryCloseConnection ( CONN ) ; } } finally { tryCloseConnection ( CONN ) ; } } private async forConnection < TResult = any > ( uri , action , existingConn ? ) { const DO_IT = async ( connectionToUse ) => { if ( action ) { return await Promise . resolve ( action ( connectionToUse ) ) ; } } ; const USE_EXISTING_CONN = ! _ . isNil ( existingConn ) ; const CONNECTION_TO_USE = USE_EXISTING_CONN ? existingConn : await this . openConnection ( uri ) ; try { if ( CONNECTION_TO_USE . noQueue || USE_EXISTING_CONN ) { return await DO_IT ( CONNECTION_TO_USE ) ; } return await CONNECTION_TO_USE . queue . add ( async ( ) => { return DO_IT ( CONNECTION_TO_USE ) ; } ) ; } catch ( e ) { this . logger . trace ( e , 'fs.ftps.FTPsFileSystem.forConnection()' ) ; throw e ; } finally { if ( ! USE_EXISTING_CONN ) { tryCloseConnection ( CONNECTION_TO_USE ) ; } } } private list ( uri , existingConn ? ) { return this . forConnection ( uri , ( conn ) => { return listDirectory ( conn . client , vscrw . normalizePath ( uri . path ) ) ; } , existingConn ) ; } inheritdoc protected onDispose ( ) { vscode_helpers . tryRemoveListener ( vscode_helpers . EVENTS , vscrw . EVENT_EXECUTE_REMOTE_COMMAND , this . _EXECUTE_REMOTE_COMMAND_LISTENER ) ; } private openConnection ( uri , noCache ? ) { noCache = vscode_helpers . toBooleanSafe ( noCache ) ; const CACHE_KEY = vscrw . getConnectionCacheKey ( uri ) ; const PARAMS = vscrw . getUriParams ( uri ) ; return new Promise < FTPsConnection > ( async ( resolve , reject ) => { const COMPLETED = vscode_helpers . createCompletedAction ( resolve , reject ) ; try { let conn = false ; if ( ! noCache ) { conn = await this . testConnection ( CACHE_KEY ) ; } if ( false === conn ) { const FOLLOW = vscrw . isTrue ( PARAMS [ 'follow' ] , true ) ; const HOST_AND_CRED = await vscrw . extractHostAndCredentials ( uri , 21 ) ; const IS_SECURE = vscrw . isTrue ( PARAMS [ 'secure' ] , true ) ; const LEGACY = vscrw . isTrue ( PARAMS [ 'legacy' ] ) ; let secureOpts ; if ( IS_SECURE ) { secureOpts = { rejectUnauthorized : vscrw . isTrue ( PARAMS [ 'rejectunauthorized' ] , false ) , } ; } const KEEP_ALIVE = parseFloat ( vscode_helpers . toStringSafe ( PARAMS [ 'keepalive' ] ) . trim ( ) ) ; let queueSize = parseInt ( vscode_helpers . toStringSafe ( PARAMS [ 'queuesize' ] ) . trim ( ) ) ; if ( isNaN ( queueSize ) ) { queueSize = 1 ; } const CLIENT = LEGACY ? new FTP_Legacy ( ) : new FTP ( ) ; CLIENT . once ( 'error' , function ( err ) { if ( err ) { COMPLETED ( err ) ; } } ) ; CLIENT . once ( 'ready' , ( ) => { const NEW_CONN = { cache : { stats : { } , } , client : CLIENT , followSymLinks : FOLLOW , noQueue : ! vscrw . isTrue ( PARAMS [ 'queue' ] , true ) , queue : vscode_helpers . createQueue ( { concurrency : queueSize , } ) , } ; COMPLETED ( null , NEW_CONN ) ; } ) ; CLIENT . connect ( { host : HOST_AND_CRED . host , port : HOST_AND_CRED . port , user : HOST_AND_CRED . user , password : HOST_AND_CRED . password , secure : IS_SECURE , secureOptions : secureOpts , keepalive : Math . floor ( ( isNaN ( KEEP_ALIVE ) ? 10.0 : KEEP_ALIVE ) * 1000.0 ) , } ) ; } else { COMPLETED ( null , conn ) ; } } catch ( e ) { COMPLETED ( e ) ; } } ) ; } inheritdoc public readDirectory ( uri ) { return this . forConnection ( uri , async ( conn ) => { const RESULT = [ ] ; for ( const ITEM of await this . list ( uri , conn ) ) { const STAT = await toFileStat ( ITEM , uri , conn ) ; RESULT . push ( [ ITEM . name , STAT . type ] ) ; } return RESULT ; } ) ; } inheritdoc public readFile ( uri ) { return this . forConnection ( uri , ( conn ) => { return new Promise < Uint8Array > ( async ( resolve , reject ) => { const COMPLETED = vscode_helpers . createCompletedAction ( resolve , reject ) ; try { const STREAM = await openRead ( conn . client , uri . path ) ; STREAM . once ( 'error' , ( err ) => { if ( err ) { COMPLETED ( err ) ; } } ) ; const TEMP_FILE = await vscode_helpers . tempFile ( ( tf ) => { return tf ; } , { keep : true , } ) ; const TRY_REMOVE_TEMP_FILE = async ( ) => { try { if ( await vscode_helpers . exists ( TEMP_FILE ) ) { await FSExtra . unlink ( TEMP_FILE ) ; } } catch ( e ) { this . logger . warn ( e , 'fs.ftps.FTPsFileSystem.readFile.TRY_REMOVE_TEMP_FILE()' ) ; } } ; const DOWNLOAD_COMPLETED = async ( ) => { try { return await FSExtra . readFile ( TEMP_FILE ) ; } finally { await TRY_REMOVE_TEMP_FILE ( ) ; } } ; try { STREAM . once ( 'close' , function ( ) { DOWNLOAD_COMPLETED ( ) . then ( ( data ) => { COMPLETED ( null , data ) ; } , ( err ) => { COMPLETED ( err ) ; } ) ; } ) . once ( 'error' , function ( err ) { TRY_REMOVE_TEMP_FILE ( ) . then ( ( ) => { COMPLETED ( err ) ; } , ( e ) => { COMPLETED ( err ) ; } ) ; } ) ; const WRITE_STREAM = FS . createWriteStream ( TEMP_FILE ) ; WRITE_STREAM . once ( 'error' , ( err ) => { if ( err ) { COMPLETED ( err ) ; } } ) ; STREAM . pipe ( WRITE_STREAM ) ; STREAM . resume ( ) ; } catch ( e ) { TRY_REMOVE_TEMP_FILE ( ) . then ( ( ) => { COMPLETED ( e ) ; } , ( err ) => { COMPLETED ( e ) ; } ) ; } } catch ( e ) { COMPLETED ( e ) ; } } ) ; } ) ; } param { vscode . ExtensionContext } context return { FTPsFileSystem } public static register ( context ) { const NEW_FS = new FTPsFileSystem ( ) ; try { context . subscriptions . push ( vscode . workspace . registerFileSystemProvider ( FTPsFileSystem . scheme , NEW_FS , { isCaseSensitive : true } ) ) ; } catch ( e ) { vscode_helpers . tryDispose ( NEW_FS ) ; throw e ; } return NEW_FS ; } inheritdoc public rename ( oldUri , newUri , options ) { return this . forConnection ( oldUri , ( conn ) => { return new Promise < void > ( async ( resolve , reject ) => { const COMPLETED = vscode_helpers . createCompletedAction ( resolve , reject ) ; try { const OLD_STAT = await this . statInner ( oldUri , conn ) ; const NEW_STAT = await this . tryGetStat ( newUri , conn ) ; if ( false !== NEW_STAT ) { if ( ! options . overwrite ) { throw vscode . FileSystemError . FileExists ( newUri ) ; } } conn . client . rename ( vscrw . normalizePath ( oldUri . path ) , vscrw . normalizePath ( newUri . path ) , ( err ) => { COMPLETED ( err ) ; } ) ; } catch ( e ) { COMPLETED ( e ) ; } } ) ; } ) ; } /**
     * Stores the name of the scheme.
     */ public static readonly scheme = 'ftps' ; inheritdoc public async stat ( uri ) { return this . statInner ( uri ) ; } private async statInner ( uri , existingConn ? ) { if ( '/' === vscrw . normalizePath ( uri . path ) ) { return { type : vscode . FileType . Directory , ctime : 0 , mtime : 0 , size : 0 , } ; } return this . forConnection ( uri , async ( conn ) => { let stat = false ; try { const URI_PATH = vscrw . normalizePath ( uri . path ) ; const NAME = Path . basename ( URI_PATH ) ; const DIR = vscrw . normalizePath ( Path . dirname ( URI_PATH ) ) ; const PARENT_URI = uriWithNewPath ( uri , DIR ) ; const LIST = await this . list ( PARENT_URI , conn ) ; for ( const ITEM of LIST ) { if ( ITEM . name === NAME ) { stat = await toFileStat ( ITEM , uriWithNewPath ( uri , DIR ) , conn ) ; break ; } } } catch { } if ( false === stat ) { throw vscode . FileSystemError . FileNotFound ( uri ) ; } return stat ; } , existingConn ) ; } private async testConnection ( cacheKey ) { return false ; } private async tryGetStat ( uri , existingConn ? ) { let stat ; try { stat = await this . statInner ( uri , existingConn ) ; } catch { stat = false ; } return stat ; } inheritdoc public watch ( uri , options ) { return { dispose : ( ) => { } } ; } inheritdoc public writeFile ( uri , content , options ) { return this . forConnection ( uri , ( conn ) => { return new Promise < void > ( async ( resolve , reject ) => { const COMPLETED = vscode_helpers . createCompletedAction ( resolve , reject ) ; try { this . throwIfWriteFileIsNotAllowed ( await this . tryGetStat ( uri , conn ) , options , uri ) ; conn . client . put ( vscrw . asBuffer ( content ) , vscrw . normalizePath ( uri . path ) , ( err ) => { COMPLETED ( err ) ; } ) ; } catch ( e ) { COMPLETED ( e ) ; } } ) ; } ) ; } } function executeServerCommand ( conn , cmd ) { cmd = vscode_helpers . toStringSafe ( cmd ) ; return new Promise < Buffer > ( ( resolve , reject ) => { const COMPLETED = vscode_helpers . createCompletedAction ( resolve , reject ) ; try { conn [ '_send' ] ( cmd , function ( err , responseText , responseCode ) { if ( err ) { COMPLETED ( err ) ; } else { COMPLETED ( null , new Buffer ( `[${ responseCode }] '${ vscode_helpers . toStringSafe ( responseText ) }'` , 'ascii' ) ) ; } } ) ; } catch ( e ) { COMPLETED ( e ) ; } } ) ; } function listDirectory ( client , path ) { return new Promise < any [ ] > ( ( resolve , reject ) => { const COMPLETED = vscode_helpers . createCompletedAction ( resolve , reject ) ; try { client . list ( vscrw . normalizePath ( path ) , ( err , items ) => { COMPLETED ( err , items ) ; } ) ; } catch ( e ) { COMPLETED ( e ) ; } } ) ; } function openRead ( client , path ) { return new Promise < NodeJS . ReadableStream > ( ( resolve , reject ) => { const COMPLETED = vscode_helpers . createCompletedAction ( resolve , reject ) ; try { client . get ( vscrw . normalizePath ( path ) , ( err , stream ) => { COMPLETED ( err , stream ) ; } ) ; } catch ( e ) { COMPLETED ( e ) ; } } ) ; } async function toFileStat ( item , uri , conn ) { if ( item ) { const STAT = { ctime : undefined , mtime : undefined , size : undefined , type : vscode . FileType . Unknown , } ; switch ( vscode_helpers . normalizeString ( item . type ) ) { case '-' : STAT . type = vscode . FileType . File ; break ; case 'd' : STAT . type = vscode . FileType . Directory ; break ; case 'l' : { STAT . type = vscode . FileType . SymbolicLink ; if ( conn . followSymLinks ) { try { const FILE_OR_FOLDER = vscrw . normalizePath ( Path . join ( uri . path , item . name ) ) ; const CACHED_VALUE = conn . cache . stats [ FILE_OR_FOLDER ] ; if ( _ . isNil ( CACHED_VALUE ) ) { let type = false ; try { const STREAM = await openRead ( conn . client , FILE_OR_FOLDER ) ; try { if ( _ . isFunction ( STREAM . destroy ) ) { STREAM . destroy ( ) ; } } catch { } type = vscode . FileType . File ; } catch { try { await listDirectory ( conn . client , FILE_OR_FOLDER ) ; type = vscode . FileType . Directory ; } catch { } } if ( false !== type ) { STAT . type = type ; } } } catch { STAT . type = vscode . FileType . SymbolicLink ; } } } break ; } if ( vscode . FileType . File === STAT . type ) { let date ; if ( Moment . isDate ( item . date ) ) { date = vscode_helpers . asUTC ( Moment ( date ) ) . unix ( ) ; } STAT . ctime = date ; STAT . mtime = date ; STAT . size = parseInt ( vscode_helpers . toStringSafe ( item . size ) . trim ( ) ) ; } if ( isNaN ( STAT . ctime ) ) { STAT . ctime = 0 ; } if ( isNaN ( STAT . mtime ) ) { STAT . mtime = 0 ; } if ( isNaN ( STAT . size ) ) { STAT . size = 0 ; } return STAT ; } } function tryCloseConnection ( conn ) { try { if ( conn ) { conn . client . destroy ( ) ; } return true ; } catch { return false ; } } function uriWithNewPath ( uri , newPath ) { if ( uri ) { return vscode . Uri . parse ( `ftps://${ uri . authority }${ vscode_helpers . toStringSafe ( newPath ) }${ vscode_helpers . isEmptyString ( uri . query ) ? '' : ( '?' + uri . query ) }` ) ; } }