Two-way synchronization with delete with SFTP/FTP server

WinSCP synchronization functionality is state-less. So when there is a file on one side, which is absent on the opposite side, WinSCP is not able to determine, if the file was added on the first side, or removed on the latter. For this reason in the Both direction, WinSCP does not support the Delete files option.

This extension uses a cache file to remember the list of local files after the previous synchronization. It uses the cached list to determine, if the file was added or removed.

You can install this script as a WinSCP extension by using this page URL in the Add Extension command.

To run the script manually use:

powershell.exe -File C:\path\SynchronizeTwoWayDelete.ps1 -sessionUrl "sftp://user:password;[email protected]/" -localPath "C:\local" -remotePath "/remote" -listPath "C:\cache\example.txt"
# @name         Two-Way Synchronization with Delete...
# @command      powershell.exe -ExecutionPolicy Bypass -File "%EXTENSION_PATH%" ^
#                   -sessionUrl "!E" -localPath "%LocalPath%" -remotePath "%RemotePath%" ^
#                   -listPath "%ListPath%" -refresh -pause -sessionLogPath "%SessionLogPath%"
# @description  Synchronizes files on local and remote directories including file deletions ^
#                   by remembering a list of previous local files
# @version      2
# @homepage     https://beta.beta.winscp.net/eng/docs/library_example_two_way_synchronize_delete
# @require      WinSCP 5.18.1
# @option       - -run group "Directories"
# @option         LocalPath -run textbox "&Local directory:" "!\"
# @option         RemotePath -run textbox "&Remote directory:" "!/"
# @option       - -run group "Options"
# @option         ListPath -run textbox "Re&member previous local files in:" ^
#                     "%LOCALAPPDATA%\WinSCP\TwoWayDelete\!N.txt"
# @option       - -config group "Logging"
# @option         SessionLogPath -config sessionlogfile
# @optionspage  https://beta.beta.winscp.net/eng/docs/library_example_two_way_synchronize_delete#options
 
param (
    # Use Generate Session URL function to obtain a value for -sessionUrl parameter.
    $sessionUrl = "sftp://user:mypassword;[email protected]/",
    [Parameter(Mandatory = $True)]
    $localPath,
    [Parameter(Mandatory = $True)]
    $remotePath,
    [Parameter(Mandatory = $True)]
    $listPath,
    $sessionLogPath = $Null,
    [Switch]
    $pause,
    [Switch]
    $refresh
)
 
try
{
    # Load WinSCP .NET assembly
    $assemblyPath = if ($env:WINSCP_PATH) { $env:WINSCP_PATH } else { $PSScriptRoot }
    Add-Type -Path (Join-Path $assemblyPath "WinSCPnet.dll")
 
    # Setup session options from URL
    $sessionOptions = New-Object WinSCP.SessionOptions
    $sessionOptions.ParseUrl($sessionUrl)
 
    $listPath = [Environment]::ExpandEnvironmentVariables($listPath)
    $listDir = (Split-Path -Parent $listPath) 
    New-Item -ItemType directory -Path $listDir -Force | Out-Null 
 
    if (Test-Path $listPath)
    {
        Write-Host "Loading list of previous local files..."
        [string[]]$previousFiles = Get-Content $listPath
    }
    else
    {
        Write-Host "No list of previous local files"
        $previousFiles = @()
    }
 
    $needRefresh = $False
 
    $session = New-Object WinSCP.Session
 
    try
    {
        $session.SessionLogPath = $sessionLogPath
 
        Write-Host "Connecting..."
        $session.Open($sessionOptions)
 
        Write-Host "Comparing files..."
        $differences =
            $session.CompareDirectories(
                [WinSCP.SynchronizationMode]::Both, $localPath, $remotePath, $False)
 
        if ($differences.Count -eq 0)
        {
            Write-Host "No changes found."   
        }
        else
        {
            Write-Host "Synchronizing $($differences.Count) change(s)..."
 
            foreach ($difference in $differences)
            {
                $action = $difference.Action
                if ($action -eq [WinSCP.SynchronizationAction]::UploadNew)
                {
                    if ($previousFiles -contains $difference.Local.FileName)
                    {
                        $difference.Reverse()
                    }
                    else
                    {
                        $needRefresh = $True
                    }
                }
                elseif ($action -eq [WinSCP.SynchronizationAction]::DownloadNew)
                {
                    $localFilePath =
                        [WinSCP.RemotePath]::TranslateRemotePathToLocal(
                            $difference.Remote.FileName, $remotePath, $localPath)
                    if ($previousFiles -contains $localFilePath)
                    {
                        $difference.Reverse()
                        $needRefresh = $True
                    }
                    else
                    {
                        # noop
                    }
                }
                elseif ($action -eq [WinSCP.SynchronizationAction]::DownloadUpdate)
                {
                    # noop
                }
                elseif ($action -eq [WinSCP.SynchronizationAction]::UploadUpdate)
                {
                    $needRefresh = $True
                }
                else
                {
                    throw "Unexpected difference $action"
                }
 
                Write-Host