PowerShell FTP and SFTP: Transfer Files with WinSCP

PowerShell FTP and SFTP: Transfer Files with WinSCP

PowerShell Tips Editor 1 min read
PowerShell FTP and SFTP: Transfer Files with WinSCP

Daily file exchange with vendors via SFTP is a five-minute manual ritual that should be a zero-touch scheduled job. Automating PowerShell SFTP FTP file transfer is straightforward with either the WinSCP .NET assembly or the Posh-SSH module — both provide full upload, download, and synchronization capabilities from PowerShell scripts. This post covers both approaches with practical examples for the most common transfer scenarios including key-based authentication and error handling.

WinSCP .NET Assembly Setup

WinSCP is a free, open-source SFTP/FTP client that ships with a .NET assembly usable from PowerShell. Download WinSCP from winscp.net and note the installation path. Load the assembly before use:

# Load the WinSCP .NET assembly — adjust path to your WinSCP installation
$winscpDll = "C:\Program Files (x86)\WinSCP\WinSCPnet.dll"
if (-not (Test-Path $winscpDll)) {
    throw "WinSCP .NET assembly not found at $winscpDll"
}
Add-Type -Path $winscpDll

# Create session options
$sessionOptions = New-Object WinSCP.SessionOptions -Property @{
    Protocol              = [WinSCP.Protocol]::Sftp
    HostName              = "sftp.vendor.com"
    UserName              = "ftpuser"
    Password              = "s3cret"
    SshHostKeyFingerprint = "ssh-rsa 2048 xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx"
}

Upload a File via SFTP

Open a WinSCP session, transfer the file, and close the session cleanly in a try/finally block:

$session = New-Object WinSCP.Session

try {
    $session.Open($sessionOptions)

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

    $result = $session.PutFiles(
        "C:\Exports\data-$(Get-Date -Format 'yyyyMMdd').csv",
        "/incoming/",
        $false,   # do not delete local file
        $transferOptions
    )
    $result.Check()   # throws on any transfer failure
    Write-Host "Upload complete: $($result.Transfers.Count) file(s)"
}
catch {
    Write-Error "SFTP upload failed: $($_.Exception.Message)"
    throw
}
finally {
    $session.Dispose()
}

Download Files from SFTP

Download all files matching a pattern from a remote directory to a local folder:

$localDir  = "C:\Downloads\VendorFiles"
$remoteDir = "/outgoing/*.csv"

if (-not (Test-Path $localDir)) { New-Item -Path $localDir -ItemType Directory | Out-Null }

$session = New-Object WinSCP.Session
try {
    $session.Open($sessionOptions)
    $result = $session.GetFiles($remoteDir, "$localDir\", $false)
    $result.Check()
    Write-Host "Downloaded $($result.Transfers.Count) file(s) to $localDir"
    $result.Transfers | ForEach-Object { Write-Host "  $($_.FileName)" }
}
finally {
    $session.Dispose()
}

Sync a Local Folder to SFTP

WinSCP’s SynchronizeDirectories method uploads only new or changed files — ideal for daily batch jobs where you want to push incremental changes:

$session = New-Object WinSCP.Session
try {
    $session.Open($sessionOptions)

    $syncResult = $session.SynchronizeDirectories(
        [WinSCP.SynchronizationMode]::Remote,   # local to remote
        "C:\Reports",                           # local path
        "/reports",                             # remote path
        $false                                  # don't delete remote files not in local
    )
    $syncResult.Check()

    Write-Host "Sync complete:"
    Write-Host "  Uploaded: $($syncResult.Uploads.Count)"
    Write-Host "  Removed:  $($syncResult.Removals.Count)"
}
finally {
    $session.Dispose()
}

Key-Based Authentication

For automated scripts, SSH key authentication is more secure than passwords. Generate a key pair, upload the public key to the SFTP server, and reference the private key file in the session options:

# Key-based authentication — no password needed
$sessionOptions = New-Object WinSCP.SessionOptions -Property @{
    Protocol              = [WinSCP.Protocol]::Sftp
    HostName              = "sftp.vendor.com"
    UserName              = "ftpuser"
    SshPrivateKeyPath     = "C:\Keys\vendor_rsa.ppk"   # PuTTY format
    SshHostKeyFingerprint = "ssh-rsa 2048 xx:xx:xx:xx:xx:xx"
}
# If the private key has a passphrase:
# $sessionOptions.PrivateKeyPassphrase = "keypassphrase"

Error Handling for Failed Transfers

WinSCP’s $result.Check() throws an exception if any transfer failed. Catch it and log the specific file failures:

$session = New-Object WinSCP.Session
try {
    $session.Open($sessionOptions)
    $result = $session.PutFiles("C:\Exports\*.csv", "/incoming/", $false)

    foreach ($transfer in $result.Transfers) {
        if ($transfer.Error -ne $null) {
            Write-Warning "Failed: $($transfer.FileName) — $($transfer.Error.Message)"
        } else {
            Write-Host "OK: $($transfer.FileName)"
        }
    }
}
catch {
    Write-Error "Session failed: $($_.Exception.Message)"
}
finally {
    $session.Dispose()
}

Common Errors and Fixes

  • Host key verification failure on first connection. WinSCP refuses to connect to a server whose host key fingerprint is unknown. On first connection, get the fingerprint interactively from WinSCP’s GUI and record it in SshHostKeyFingerprint. Never use GiveUpSecurityAndAcceptAnySshHostKey = $true in production.
  • WinSCP path and .NET assembly path must be correct. The Add-Type -Path call fails silently or throws an error if the path is wrong or the WinSCP installation is a different version than expected. Verify the path with Test-Path before loading, and pin the WinSCP version in your deployment to avoid assembly version mismatches.

Related Cmdlets / See Also

Wrapping Up

WinSCP’s .NET assembly is the most capable and reliable SFTP automation option for PowerShell. Always open and dispose sessions in a try/finally block, use key-based authentication for unattended scripts, call $result.Check() to surface transfer errors, and schedule the script as a Task Scheduler job for fully automated file exchange.

Send-Item -To