This is an old revision of the document!

Keep local directory up to date (download changed files from remote SFTP/FTP server)

This script periodically synchronizes changes from the remote server to a local directory.

WinSCP supports natively keeping a remote directory up to date function thanks to a possibility to get notified by Windows (as a local operating system) about changes in a local directory. None of the supported file transfer protocols unfortunately offer a functionality to watch for changes in a remote directory. So the only solution is to run full remote to local synchronization in regular intervals.

The script is distributed in WinSCP installer as a WinSCP extension.

Advertisement

To run the script manually, use:

powershell.exe -File KeepLocalUpToDate.ps1 -sessionUrl "sftp://username:password@example.com/" -remotePath "/remote/path" -localPath "C:\local\path" [-delete]

If you just want to scan for changes, but not download them, see Watching for changes in SFTP/FTP server.

# @name         &Keep Local Directory up to Date...
# @command      powershell.exe -ExecutionPolicy Bypass -File "%EXTENSION_PATH%" ^
#                   -sessionUrl "!S" -localPath "%LocalPath%" -remotePath "%RemotePath%" ^
#                   %Delete% %Beep% %ContinueOnError% -interval "%Interval%" -pause ^
#                   -sessionLogPath "%SessionLogPath%"
# @description  Periodically scans for changes in a remote directory and ^
#                   reflects them on a local directory
# @version      5
# @homepage     https://winscp.net/eng/docs/library_example_keep_local_directory_up_to_date
# @require      WinSCP 5.12
# @option       - -run group "Directories"
# @option         RemotePath -run textbox "&Watch for changes in the remote directory:" "!/"
# @option         LocalPath -run textbox ^
#                     "... &and automatically reflect them on the local directory:" "!\"
# @option       - -config -run group "Options"
# @option         Delete -config -run checkbox "&Delete files" "" -delete 
# @option         Beep -config -run checkbox "&Beep on change" "" -beep
# @option         ContinueOnError -config -run checkbox "Continue on &error" "" -continueOnError
# @option         Interval -config -run textbox "&Interval (in seconds):" "30"
# @option       - -config group "Logging"
# @option         SessionLogPath -config sessionlogfile
# @optionspage  ^
#     https://winscp.net/eng/docs/library_example_keep_local_directory_up_to_date#options
 
param (
    # Use Generate Session URL function to obtain a value for -sessionUrl parameter.
    $sessionUrl = "sftp://user:mypassword;fingerprint=ssh-rsa-xx-xx-xx@example.com/",
    [Parameter(Mandatory = $True)]
    $localPath,
    [Parameter(Mandatory = $True)]
    $remotePath,
    [Switch]
    $delete,
    [Switch]
    $beep,
    [Switch]
    $continueOnError,
    $sessionLogPath = $Null,
    $interval = 30,
    [Switch]
    $pause
)
 
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
    $sessionOptions = New-Object WinSCP.SessionOptions
    $sessionOptions.ParseUrl($sessionUrl)
 
    $session = New-Object WinSCP.Session
    
    # Optimization
    # (do not waste time enumerating files, if you do not need to scan for deleted files)
    if ($delete) 
    {
        $localFiles = Get-ChildItem -Recurse -Path $localPath
    }
 
    try
    {
        $session.SessionLogPath = $sessionLogPath
 
        Write-Host "Connecting..."
        $session.Open($sessionOptions)
 
        while ($True)
        {
            Write-Host "Synchronizing changes..."
            $result =
                $session.SynchronizeDirectories(
                    [WinSCP.SynchronizationMode]::Local, $localPath, $remotePath, $delete)
 
            $changed = $False
 
            if (!$result.IsSuccess)
            {
              if ($continueOnError)
              {
                Write-Host "Error: $($result.Failures[0].Message)"
                $changed = $True
              }
              else
              {
                $result.Check()
              }
            }
 
            # Print updated files
            foreach ($download in $result.Downloads)
            {
                Write-Host "$($download.Destination) <= $($download.FileName)"
                $changed = $True
            }
 
            if ($delete)
            {
                # scan for removed local files (the $result does not include them)
                $localFiles2 = Get-ChildItem -Recurse -Path $localPath
 
                if ($localFiles)
                {
                    $changes =
                        Compare-Object -DifferenceObject $localFiles2 `
                            -ReferenceObject $localFiles
                
                    $removedFiles =
                        $changes |
                        Where-Object -FilterScript { $_.SideIndicator -eq "<=" } |
                        Select-Object -ExpandProperty InputObject
 
                    # Print removed local files
                    foreach ($removedFile in $removedFiles)
                    {
                        Write-Host "$removedFile deleted"
                        $changed = $True
                    }
                }
 
                $localFiles = $localFiles2
            }
 
            if ($changed)
            {
                if ($beep)
                {
                    [System.Console]::Beep()
                }
            }
            else
            {
                Write-Host "No change."
            }
            
            Write-Host "Waiting for $interval seconds, press Ctrl+C to abort..."
            $wait = [int]$interval
            # Wait for 1 second in a loop, to make the waiting breakable
            while ($wait -gt 0)
            {
                Start-Sleep -Seconds 1
                $wait--
            }
 
            Write-Host
        }
    }
    finally
    {
        Write-Host "Disconnecting..."
        # Disconnect, clean up
        $session.Dispose()
    }
}
catch
{
    Write-Host "Error: $($_.Exception.Message)"
}
 
# Pause if -pause switch was used
if ($pause)
{
    Write-Host "Press any key to exit..."
    [System.Console]::ReadKey() | Out-Null
}
 
# Never exits cleanly
exit 1

Advertisement

Options

Enter root directories for the synchronization into the two directory boxes. By default the current working directories will be used. The directories can be specified, when executing the extension only.

The Delete files checkbox makes the extension delete files and subdirectories in local directory that you delete in a corresponding remote directory. Before using this, learn how it works, so you know what you are doing.

Check the Beep on change to make the extension beep when a change is detected.

Check the Continue on error not to interrupt watching for changes, when an error occurs. Typically you want to enable the option to skip files opened for writing and similar errors.

In the Interval box, specify an interval between the checks for changes.

In the Session log file, you can specify a path to a session log file. The option is available on the Preferences dialog only.

In the Keyboard shortcut, you can specify a keyboard shortcut for the extension. The option is available on the Preferences dialog only.

Scripting

Or you can simply use the synchronize scripting command.

To make the synchronization run periodically, you can either schedule the script or run the script in a loop. The latter may be more convenient, as it allows you to see all runs in a single console window, so that you can review the updates easily.

To run the script in the loop, you can use following batch file:1

@echo off
:loop
winscp.com /script=full_remote_to_local_synchronization.txt
timeout /t 30
goto loop

Advertisement

  1. Note that the timeout command is available since Windows 7 only. For alternatives in older versions of Windows, refer to How to sleep for 5 seconds in Windows’s Command Prompt? article.Back

Last modified: by martin