free web stats
Link Search Menu Expand Document

SFTP Automation for Windows PowerShell with WinSCP .NET assembly

The MFT Gateway allows you the ability to easily send and receive files over the secure AS2 protocol for Internet based file transfer. While the MFT Gateway offers many integration options such as a REST API with webhooks and direct AWS S3 based file integration, many organizations might still choose to integrate existing systems using SFTP.

While SFTP is natively available on Linux and similar operating systems, automation of SFTP on Windows usually requires purchased software. This article explains how you can use the popular free and open-source software WinSCP as a library and use Windows PowerShell scripting to automate a file exchange.

The MFT Gateway SFTP File Structure

The folder structure exposed for SFTP integration is of the following simplified form, and since a single AS2 message can contain multiple files, or the same file name can be repeated by a partner, the MFT Gateway creates a unique directory to contain files of each AS2 message. At a high level, this results in an inbox folder structure as follows.

../inbox/<unique-directory>/<file-name>

Now an SFTP script must list contents of each unique directory within the inbox and download one or more files that might exist under it.

WinSCP Scripting

WinSCP has very good support for scripting, along with detailed documentation on how to use those capabilities. While using the scripting interface directly is recommended for simple tasks not requiring any control structures, WinSCP recommends the use of the WinSCP .NET assembly and the COM library, for more complex automation needs.

Using WinSCP with Windows PowerShell

To get started, download the WinSCP .NET assembly from the download page, and refer to the installation instructions. It is best to install the distribution to a location without spaces in the path. Note that by default, executing PowerShell scripts will be disabled. Trying to run a PowerShell script without an execution policy will usually result in an error like below.

PS C:\opt\WinSCP-5.21.5-Automation\Demo> .\mftg-sftp-download.ps1
.\mftg-sftp-download.ps1 : File C:\opt\WinSCP-5.21.5-Automation\Demo\mftg-sftp-download.ps1 cannot be loaded because running scripts is disabled on this system. For more information, see about_Execution_Policies at
https:/go.microsoft.com/fwlink/?LinkID=135170.

You can choose to lift this restriction by executing the following cmdlet from a PowerShell administrative console.

Set-ExecutionPolicy Unrestricted

Or you may bypass this by executing scripts as follows.

PS C:\opt\WinSCP-5.21.5-Automation\Demo> powershell -ExecutionPolicy Bypass -File .\mftg-sftp-download.ps1

Loading the Assembly

To load the assembly, use the ‘Add-Type’ cmdlet, along with the full path to the WinSCP DLL.

Load WinSCP .NET assembly

Add-Type -Path "C:\opt\WinSCP-5.21.5-Automation\WinSCPnet.dll"

Setting up a connection

To setup a connection, create a sessionOptions variable and set the hostname, username and the SSH keypath and fingerprint etc. You would also want to define a variable transferOptions, to select he ‘Binary’ mode of transfer for files.

# Setup session options
$sessionOptions = New-Object WinSCP.SessionOptions -Property @{
    Protocol = [WinSCP.Protocol]::Sftp
    HostName = "sftp.mftgateway.com"
    UserName = "asankha"
    SshPrivateKeyPath = "C:\opt\WinSCP-5.21.5-Automation\Demo\asankha.ppk"
    SshHostKeyFingerprint = "ssh-rsa 2048 12:b7:cf:7a:55:9a:77:43:f2:87:d6:43:09:92:88:15"
}

$transferOptions = New-Object WinSCP.TransferOptions
$transferOptions.TransferMode = [WinSCP.TransferMode]::Binary

Iterating directories and getting a file count

Remember our main requirement to be able to handle files within unique subdirectories containing one or more files? So, we will list the root directory and get a count of the total number of files, and also the individual path names.

try {
    # Connect to obtain file enumeration
    Write-Host "Connecting..."
    $session = New-Object WinSCP.Session
    $session.Open($sessionOptions)

    $remotePath = "/mftg-asankha.com/AS2/files/ACP_PROD/P1/inbox/"
    $localPath  = "C:\opt\WinSCP-5.21.5-Automation\Demo\inbox\"

    Write-Host "Starting files enumeration..."
    $directory = $session.ListDirectory($remotePath)
    $subdirs   = $directory.Files.FullName
}    
finally
{
    # Disconnect, clean up
    $session.Dispose()
}

[System.Collections.ArrayList]$msgs = @()
foreach ($sub in $subdirs)
{
    [void]$msgs.Add($sub)
}

$count = $msgs.Count
Write-Host "Total messages found for download: $count"   

Downloading individual files

Finally, we are ready to start downloading individual files, and we will create a new session for this task, and we can optionally delete the files after we successfully download them — by uncommenting the ‘$downloadSession.RemoveFiles($file)’ line below.

try {
  Write-Host "Starting download ..."
  $downloadSession = New-Object WinSCP.Session
  $downloadSession.Open($sessionOptions)

  while ($True)
  {
   $count = ($msgs).Count 
   if ($count -eq 0) {
    break
   }
     
   $file = ($msgs)[0]
   ($msgs).RemoveAt(0)

   if ($file -match '/..$') {
    Write-Host "Skipping parent directory"
    break
   }

   $count = ($msgs).Count
  
   $remote = -join($file, "/");

   Write-Host "Downloading $remote to $localPath ..."
   $transferResult = $downloadSession.GetFiles($remote, $localPath, $False, $transferOptions)

   # Did the download succeeded?
   if (!$transferResult.IsSuccess)
   {
    # Print error (but continue with other files)
    Write-Host ("Error downloading file $file " + "$($transferResult.Failures[0].Message)")
   } else {
    ($stats).count++
    # Delete remote file and directory after successful download
    # $downloadSession.RemoveFiles($file)
   }
  }

  Write-Host "Download completed"
 }
 finally
 {
  $downloadSession.Dispose()
 }

Sample Execution Output

Execution of this script will yield an output as below. Notice that we have bypassed the PowerShell execution policy for simplicity. Once you have tested your script, you may choose to schedule it for production use.

PS C:\opt\WinSCP-5.21.5-Automation\Demo> powershell -ExecutionPolicy Bypass -File .\mftg-sftp-download.ps1
Connecting...
Starting files enumeration...
Total messages found for download: 5
Starting download ...
Downloading /mftg-asankha.com/AS2/files/ACP_PROD/P1/inbox/956246844612951/ to C:\opt\WinSCP-5.21.5-Automation\Demo\inbox\ ...
Downloading /mftg-asankha.com/AS2/files/ACP_PROD/P1/inbox/956251031647737/ to C:\opt\WinSCP-5.21.5-Automation\Demo\inbox\ ...
Downloading /mftg-asankha.com/AS2/files/ACP_PROD/P1/inbox/956251096571533/ to C:\opt\WinSCP-5.21.5-Automation\Demo\inbox\ ...
Downloading /mftg-asankha.com/AS2/files/ACP_PROD/P1/inbox/956251102345887/ to C:\opt\WinSCP-5.21.5-Automation\Demo\inbox\ ...
Skipping parent directory
Download completed
Took 00:00:19.7675095
Downloaded 4 files

Complete working script

Here is the complete working script for my Windows 11 laptop where this was developed.

# Sample WinSCP script to download MFT Gateway received files using SFTP
#
# @author Asankha - Aayu Technologies LLC - 11 Jan 2022
#
# Download WinSCP .Net assembly from https://winscp.net/eng/downloads.php#additional and extract to "C:\opt\WinSCP-5.21.5-Automation" as an example
#
# Execution example
#    PS C:\opt\WinSCP-5.21.5-Automation\Demo> powershell -ExecutionPolicy Bypass -File .\mftg-sftp-download.ps1
#    Connecting...
#    Starting files enumeration...
#    Total messages found for download: 5
#    Starting download ...
#    Downloading /mftg-asankha.com/AS2/files/ACP_PROD/AdvanceAutoParts/inbox/956246844612951/ to C:\opt\WinSCP-5.21.5-Automation\Demo\inbox\ ...
#    Downloading /mftg-asankha.com/AS2/files/ACP_PROD/AdvanceAutoParts/inbox/956251031647737/ to C:\opt\WinSCP-5.21.5-Automation\Demo\inbox\ ...
#    Downloading /mftg-asankha.com/AS2/files/ACP_PROD/AdvanceAutoParts/inbox/956251096571533/ to C:\opt\WinSCP-5.21.5-Automation\Demo\inbox\ ...
#    Downloading /mftg-asankha.com/AS2/files/ACP_PROD/AdvanceAutoParts/inbox/956251102345887/ to C:\opt\WinSCP-5.21.5-Automation\Demo\inbox\ ...
#    Skipping parent directory
#    Download completed
#    Took 00:00:19.7675095
#    Downloaded 4 files

try {
    [Console]::TreatControlCAsInput = $True
    Start-Sleep -Seconds 1
    $Host.UI.RawUI.FlushInputBuffer()

    # Load WinSCP .NET assembly
    Add-Type -Path "C:\opt\WinSCP-5.21.5-Automation\WinSCPnet.dll"
 
    # Setup session options
    $sessionOptions = New-Object WinSCP.SessionOptions -Property @{
        Protocol = [WinSCP.Protocol]::Sftp
        HostName = "sftp.mftgateway.com"
        UserName = "asankha"
        SshPrivateKeyPath = "C:\opt\WinSCP-5.21.5-Automation\Demo\asankha.ppk"
        SshHostKeyFingerprint = "ssh-rsa 2048 12:b7:cf:7a:55:9a:77:43:f2:87:d6:43:09:92:88:15"
    }
 
    $transferOptions = New-Object WinSCP.TransferOptions
    $transferOptions.TransferMode = [WinSCP.TransferMode]::Binary

    try {
        # Connect to obtain file enumeration
        Write-Host "Connecting..."
        $session = New-Object WinSCP.Session
        $session.Open($sessionOptions)

        $remotePath = "/mftg-asankha.com/AS2/files/ACP_PROD/AdvanceAutoParts/inbox/"
        $localPath  = "C:\opt\WinSCP-5.21.5-Automation\Demo\inbox\"
 
        Write-Host "Starting files enumeration..."
        $directory = $session.ListDirectory($remotePath)
        $subdirs   = $directory.Files.FullName
    }    
    finally
    {
        # Disconnect, clean up
        $session.Dispose()
    }

    [System.Collections.ArrayList]$msgs = @()
    foreach ($sub in $subdirs)
    {
        [void]$msgs.Add($sub)
    }

    $count = $msgs.Count

    Write-Host "Total messages found for download: $count"   

    $started = Get-Date
    $stats = @{
        count = 0
        bytes = [long]0
    }

    try {
  Write-Host "Starting download ..."
  $downloadSession = New-Object WinSCP.Session
  $downloadSession.Open($sessionOptions)

  while ($True)
  {
   $count = ($msgs).Count 
   if ($count -eq 0) {
    break
   }
     
   $file = ($msgs)[0]
   ($msgs).RemoveAt(0)

   if ($file -match '/..$') {
    Write-Host "Skipping parent directory"
    break
   }

   $count = ($msgs).Count
  
   $remote = -join($file, "/");

   Write-Host "Downloading $remote to $localPath ..."
   $transferResult = $downloadSession.GetFiles($remote, $localPath, $False, $transferOptions)

   # Did the download succeeded?
   if (!$transferResult.IsSuccess)
   {
    # Print error (but continue with other files)
    Write-Host ("Error downloading file $file " + "$($transferResult.Failures[0].Message)")
   } else {
    ($stats).count++
    # Delete remote file and directory after successful download
    # $downloadSession.RemoveFiles($file)
   }
  }

  Write-Host "Download completed"
 }
 finally
 {
  $downloadSession.Dispose()
 }

    $ended = Get-Date
    Write-Host "Took $(New-TimeSpan -Start $started -End $ended)"
    Write-Host ("Downloaded $($stats.count) files")
    exit 0
}
catch
{
    Write-Host "Error: $($_.Exception.Message)"
    exit 1
}