PowerShell: Synchronizing a Folder (and Sub-Folders)

This is the latest version of my PowerShell folder synchronisation script.

I’ve revisited this script a few times with new additions and modifications.  If you want explanations about how the script was developed over time or how parts of the script work, have a look at the following posts;

Part 1 covers the basic script to sync two folders.

Part 2 adds the ability to specify exceptions that the sync process will skip.

In part 3 I added an XML configuration file so more complicated processes can be run.

I customised and validated parameter input (parameter sets) and added some proper error checking in part 4.

The script was updated to deal with non-standard paths and to output objects reporting on the changes it made in part 5.

In part 6 I add more use of literal paths (to prevent path errors) and generate statistics.

I add a filter option, turn strict-mode on and optimise the statistics generation in part 7 (plus fix a bug!).

In part 8 I’ve added some validity checks on the XML configuration file.

WhatIf functionality was added in Part 9 plus some corrections to make sure Filter and Exceptions worked correctly.

In Part 10 I added text file logging.

In Part 11 I fixed an odd bug where $Nulls were being added to arrays and added a -PassThru switch.

A zipped version of the script is here; Sync-Folder.

As above you can pass a configuration XML file to the script instead of just Source and Target parameters;

    
<Configuration>
	<SyncPair>
		<Source>C:\synctest\source1</Source>
		<Target>C:\synctest\dest1</Target>
        	<Filter>*.txt</Filter>
		<ExceptionList>
			<Exception>*p234*.txt</Exception>
		</ExceptionList>
	</SyncPair>
	<SyncPair>
		<Source>C:\synctest\source2</Source>
		<Target>C:\synctest\dest2</Target>
        	<Filter>*.txt</Filter>
	</SyncPair>
</Configuration>

Note that you need to use WildCards of some description in the Filter and Exception parts.  So if you want it to skip all paths with “Old” in it add “Old” to the exceptions list.

Here’s the script itself;

<# .SYNOPSIS   Synchronises folders (and their contents) to target folders.  Uses a configuration XML file (default) or a pair of   folders passed as parameters. .DESCRIPTION   Reads in the Configuration xml file (passed as a parameter or defaults to    Sync-FolderConfiguration.xml in the script folder. .PARAMETER ConfigurationFile     Holds the configuration the script uses to run. .PARAMETER SourceFolder     Which folder to synchronise. .PARAMETER TargetFolder     Where to sync the source folder to. .PARAMETER Exceptions     An array of file paths to skip from synchronisation.  Accepts wild-cards. .PARAMETER LogFile     A logfile to write to.  Defaults to LogFile.txt in the script's folder. .NOTES            1.0         HerringsFishBait.com         17/11/2015     1.1         Fixed path check to use LiteralPath         Added returning status object throughout     1.2 4/Aug/2016         Added LiteralPath to the Get-ChildItem commands            Added totals to report on what was done      1.3 6/10/2016         Added StrictMode         Set $Changes to an empty collection on script run to reset statistics           Rewrote Statistics         Added $Filter option      1.4 4/11/2016         Added Get-PropertyExists function to make sure parts of the config XML are not missing.       1.5 13/01/2017         Fixed Type in Tee-Object that was preventing statistics showing correctly         1.6 20/01/2017         Fixed Filters not working if not specified in config file         Fixed Exceptions not working in some cases in Exception file              Added Write-Verbose on all the passed parameters to Sync-OneFolder            Added first pass at WhatIf     1.7  03/03/2017         Added Write-Log function to write output to file     1.8         Fixed bug in copying matched files              "$MatchingSourceFile= $SourceFiles | Where-Object {$_.Name -eq $TargetFile.Name}"         Made most logs not write to the file (for performance)         Fixed a bug where not all the statistics were recorded when a configuration XML was used.     1.9 09/05/2017         Corrected bug where an error was generated if there were no Changes         Added -PassThru switch to return objects .EXAMPLE   Sync-Folder -configurationfile:"d:\temp\Config.xml" .EXAMPLE   Sync-Folder -SourceFolder:c:\temp -TargetFolder:d:\temp -Exceptions:"*.jpg" #>
[CmdletBinding(DefaultParameterSetName="XMLFile")]
param
(
    [parameter(
        ParameterSetName="XMLFile")]
    [ValidateScript({Test-Path $_ -PathType leaf})]
    [string]$ConfigurationFile=$PSScriptRoot+"\Sync-FolderConfiguration.xml",
    [parameter(
        Mandatory=$True,
        ValueFromPipelineByPropertyName=$True,
        ParameterSetName="FolderPair")]
    [string]$SourceFolder,
    [parameter(
        Mandatory=$True,
        ValueFromPipelineByPropertyName=$True,
        ParameterSetName="FolderPair")]
    [string]$TargetFolder,
    [parameter(
        ParameterSetName="FolderPair")]
    [string[]]$Exceptions=$Null,
    [parameter(
        ParameterSetName="FolderPair")]
    [string]$Filter="*",
    [ValidateScript({Test-Path $_ -PathType leaf -IsValid})]
    [string]$LogFile=$PSScriptRoot+"\SyncFolderLog.txt",
    [switch]$PassThru=$False,
    [switch]$Whatif=$False

)
set-strictmode -version Latest
$Script:LogFileName=$LogFile
<# .SYNOPSIS This writes verbose or error output while also logging to a text file. .DESCRIPTION This writes verbose or error output while also logging to a text file. .PARAMETER Output The string to write to the log file and Error / Verbose streams. .PARAMETER IsError If this switch is specified the $Output string is written to the Error stream instead  of Verbose. .PARAMETER Heading Makes the passed string a heading (gives it a border) .PARAMETER Emphasis Puts an emphasis character on either side of the string to output. .PARAMETER WriteHost Writes the output to the host instead of the verbose stream .PARAMETER NoFileWrite Does not write this output to the files #>
function Write-Log
{
    [CmdletBinding()]
    param
    (
        [Parameter(
            ValueFromPipeline=$true)]
        [String]$Output="",
        [switch]$IsError=$False,
        [switch]$IsWarning=$False,
        [switch]$Heading=$False,
        [switch]$Emphasis=$False,
        [switch]$WriteHost=$False,
        [switch]$NoFileWrite=$False
    )
    BEGIN
    {
        $TitleChar="*"
    }
    PROCESS
    {
        $FormattedOutput=@()
        if ($Heading)
        {
            $TitleBar=""
            #Builds a line for use in a banner
            for ($i=0;$i -lt ($Output.Length)+2; $i++)
            {
                $TitleBar+=$TitleChar
            }
            $FormattedOutput=@($TitleBar,"$TitleChar$Output$TitleChar",$TitleBar,"")
        }elseif ($Emphasis)
        {
            $FormattedOutput+="","$TitleChar$Output$TitleChar",""
        }else
        {
            $FormattedOutput+=$Output
        }
        if ($IsError)
        {
            $PreviousFunction=(Get-PSCallStack)[1]
            $FormattedOutput+="Calling Function: $($PreviousFunction.Command) at line $($PreviousFunction.ScriptLineNumber)"
            $FormattedOutput=@($FormattedOutput | ForEach-Object {(Get-Date -Format HH:mm:ss.fff)+" : ERROR " + $_})
            $FormattedOutput | Write-Error
        }elseif ($IsWarning)
        {
            $FormattedOutput=@($FormattedOutput | ForEach-Object {(Get-Date -Format HH:mm:ss.fff)+" : WARNING " + $_})
            $FormattedOutput | Write-Warning
        }else
        {
            $FormattedOutput=$FormattedOutput | ForEach-Object {(Get-Date -Format HH:mm:ss.fff)+" : " + $_}
            if ($WriteHost)
            {
                $FormattedOutput | Write-Host
            }else
            {

                $FormattedOutput | Write-Verbose
            }
        }
        if (!$NoFileWrite)
        {
            if (($Script:LogFileName -ne $Null) -and ($Script:LogFileName -ne ""))
            {
                $FormattedOutput | Out-File -Append $Script:LogFileName
            }  

        }
    }
    END
    {
    }
}

<# .SYNOPSIS   Checks a file doesn't match any of a passed array of exceptions. .PARAMETER TestPath     The full path to the file to compare to the exceptions list. .PARAMETER PassedExceptions     An array of all the exceptions passed to be checked. #>
function Check-Exceptions
{
    param
    (
        [parameter(Mandatory=$True)]
        [ValidateScript({Test-Path $_ -IsValid })]
        [string]$TestPath,
        [string[]]$PassedExceptions
    )
    $Result=$False
    $MatchingException=""
    if ($PassedExceptions -eq $Null)
    {
        Return $False
    }
    Write-Log "Checking $TestPath against exceptions" -NoFileWrite
    $PassedExceptions | ForEach-Object {if($TestPath -like $_)
        {
            $Result=$True;$MatchingException=$_
        }}
    If ($Result)
    {
        Write-Log "Matched Exception : $MatchingException, skipping."
    }
    $Result
}

<# .SYNOPSIS   Creates an object to be used to report on the success of an action #>
function New-ReportObject
{
    New-Object -typename PSObject| Add-Member NoteProperty "Successful" $False -PassThru |
    Add-Member NoteProperty "Process" "" -PassThru |
    Add-Member NoteProperty "Message" "" -PassThru
}

<# .SYNOPSIS     Returns if a property of an object exists. .PARAMETER Queryobject     The object to check the property on. .PARAMETER PropertyName     The name of the property to check the existance of. #>
function Get-PropertyExists
{
    param
    (
        [PSObject]$Queryobject,
        [string]$PropertyName
    )
    Return (($Queryobject | Get-Member -MemberType Property | Select-Object -ExpandProperty Name) -contains $PropertyName)
}
<# .SYNOPSIS   Synchronises the contents of one folder to another.  It recursively calls itself   to do the same for sub-folders.  Each file and folder is checked to make sure   it doesn't match any of the entries in the passed exception list.  if it does,    the item is skipped. .PARAMETER SourceFolder     The full path to the folder to be synchronised. .PARAMETER SourceFolder     The full path to the target folder that the source should be synched to. .PARAMETER PassedExceptions     An array of all the exceptions passed to be checked. .PARAMETER Filter     Only files matching this parameter will be synced. #>
function Sync-OneFolder
{
    param
    (
        [parameter(Mandatory=$True)]
        [ValidateScript({Test-Path -LiteralPath $_ -PathType Container})]
        [string]$SourceFolder,
        [parameter(Mandatory=$True)]
        [ValidateScript({Test-Path -LiteralPath $_ -IsValid })]
        [string]$TargetFolder,
        [string[]]$PassedExceptions,
        [string]$Filter="*",
        [switch]$WhatIf=$False

    )
    Write-Log "Source Folder : $SourceFolder"
    Write-Log "Target Folder : $TargetFolder"
    Write-Log "Filter : $Filter"
    if ($PassedExceptions -ne $Null)
    {
        Write-Log "Exceptions:"
        $PassedExceptions | ForEach-Object{Write-Log $_}
    }
    Write-Log "Checking For Folders to Create" -NoFileWrite
    if (!(Test-Path -LiteralPath $TargetFolder -PathType Container))
    {
        $Output=New-ReportObject
        Write-Log "Creating Folder : $($TargetFolder)"
        $Output.Process="Create Folder"
        try
        {
            $Output.Message="Adding folder missing from Target : $TargetFolder"
            Write-Log $Output.Message
            New-Item $TargetFolder -ItemType "Directory" -WhatIf:$WhatIf > $null
            $Output.Successful=$True
        }
        catch
        {
            $Output.Message="Error adding folder $TargetFolder)"
            Write-Log $Output.Message -IsError
            Write-Log $_ -IsError
        }
        $Output
    }
    Write-Log "Getting File Lists" -NoFileWrite
    $SourceFiles=$TargetFiles=$TargetList=@()
    $SourceFolders=$TargetFolders=@()
    $SourceList=Get-ChildItem -LiteralPath $SourceFolder
    if (Test-Path $TargetFolder)
    {
        $TargetList=Get-ChildItem -LiteralPath $TargetFolder
    }
    $SourceFiles+=$SourceList | Where-Object {$_.PSIsContainer -eq $False -and $_.FullName -like $Filter -and
        !(Check-Exceptions $_.FullName $PassedExceptions)}
    $TargetFiles+=$TargetList | Where-Object {$_.PSIsContainer -eq $False -and $_.FullName -like $Filter -and
        !(Check-Exceptions $_.FullName $PassedExceptions)}
    $SourceFolders+=$SourceList | Where-Object {$_.PSIsContainer -eq $True -and !(Check-Exceptions $_.FullName $PassedExceptions)}
    $TargetFolders+=$TargetList | Where-Object {$_.PSIsContainer -eq $True -and !(Check-Exceptions $_.FullName $PassedExceptions)}
    $MissingFiles=Compare-Object $SourceFiles $TargetFiles -Property Name
    $MissingFolders=Compare-Object $SourceFolders $TargetFolders -Property Name
    Write-Log "Comparing Missing File Lists" -NoFileWrite
    foreach ($MissingFile in $MissingFiles)
    {
        $Output=New-ReportObject
        if($MissingFile.SideIndicator -eq "<=")         {             $Output.Process="Copy File"             try             {                           $Output.Message="Copying missing file : $($TargetFolder+"\"+$MissingFile.Name)"                  Write-Log $Output.Message                 Copy-Item -LiteralPath ($SourceFolder+"\"+$MissingFile.Name) -Destination ($TargetFolder+"\"+$MissingFile.Name) -WhatIf:$WhatIf                 $Output.Successful=$True             }             catch             {                 $Output.Message="Error copying missing file $($TargetFolder+"\"+$MissingFile.Name)"                 Write-Log $Output.Message -IsError                 Write-Log $_ -IsError             }         }elseif ($MissingFile.SideIndicator="=>")
        {
            $Output.Process="Remove File"
            try
            {
                $Output.Message="Removing file missing from Source : $($TargetFolder+"\"+$MissingFile.Name)"
                Write-Log $Output.Message
                Remove-Item -LiteralPath ($TargetFolder+"\"+$MissingFile.Name) -WhatIf:$WhatIf
                $Output.Successful=$True
            }
            catch
            {
                $Output.Message="Error removing file $($TargetFolder+"\"+$MissingFile.Name)"
                Write-Log $Output.Message -IsError
                Write-Log $_ -IsError
            }
        }
        $Output

    }
    Write-Log "Comparing Missing Folder Lists" -NoFileWrite
    foreach ($MissingFolder in $MissingFolders)
    {
        if ($MissingFolder.SideIndicator -eq "=>")
        {
            $Output=New-ReportObject
            $Output.Process="Remove Folder"
            try
            {
                $Output.Message="Removing folder missing from Source : $($TargetFolder+"\"+$MissingFolder.Name)"
                Write-Log $Output.Message
                Remove-Item -LiteralPath ($TargetFolder+"\"+$MissingFolder.Name) -Recurse -WhatIf:$WhatIf
                $Output.Successful=$True
            }
            catch
            {
                $Output.Message="Error removing folder $($TargetFolder+"\"+$MissingFolder.Name)"
                Write-Log $Output.Message -IsError
                Write-Log $_ -IsError
            }
            $Output
        }
    }
    Write-Log "Copying Changed Files : $($TargetFiles.Count) to check" -NoFileWrite
    ForEach ($TargetFile in $TargetFiles)
    {
        Write-Log "Getting Matching Files for $($TargetFile.Name)" -NoFileWrite
        $MatchingSourceFile= $SourceFiles | Where-Object {$_.Name -eq $TargetFile.Name}
        If ($MatchingSourceFile -ne $Null)
        {
            If ($MatchingSourceFile.LastWriteTime -gt $TargetFile.LastWriteTime)
            {
                $Output=New-ReportObject
                $Output.Process="Update File"
                try
                {
                    $Output.Message="Copying updated file : $($TargetFolder+"\"+$MatchingSourceFile.Name)"
                    Write-Log $Output.Message
                    Copy-Item -LiteralPath ($SourceFolder+"\"+$MatchingSourceFile.Name) -Destination ($TargetFolder+"\"+$MatchingSourceFile.Name) -Force -WhatIf:$WhatIf
                    $Output.Successful=$True
                }
                catch
                {
                    $Output.Message="Error copying updated file $($TargetFolder+"\"+$MatchingSourceFile.Name)"
                    Write-Log $Output.Message -IsError
                    Write-Log $_ -IsError
                }
                $Output
            }

        }
    }
    Write-Log "Comparing Sub-Folders" -NoFileWrite
    foreach($SingleFolder in $SourceFolders)
    {
        Sync-OneFolder -SourceFolder $SingleFolder.FullName -TargetFolder ($TargetFolder+"\"+$SingleFolder.Name) -PassedExceptions $PassedExceptions -Filter $Filter -WhatIf:$WhatIf #
    }
}
$ResultObjects=$Changes=$CurrentExceptions=@()
$CurrentFilter="*"
Write-Log "Running Sync-Folder Script" -NoFileWrite
If ($WhatIf)
{
    Write-Host "WhatIf Switch specified;  no changes will be made." -WriteHost
}
if ($PSBoundParameters.ContainsKey("SourceFolder"))
{
    Write-Log "Syncing folder pair passed as parameters."
    $ResultObjects=Sync-OneFolder -SourceFolder $SourceFolder -TargetFolder $TargetFolder -PassedExceptions $Exceptions -Filter $Filter -WhatIf:$WhatIf |
    Tee-Object -Variable Changes
}else
{
    Write-Log "Running with Configuration File : $ConfigurationFile"
    $Config=[xml](Get-Content $ConfigurationFile)
    $FolderChanges=$Null
    foreach ($Pair in $Config.Configuration.SyncPair)
    {
        Write-Log "Processing Pair $($Pair.Source) $($Pair.Target)"
        $CurrentExceptions=$Null
        If(Get-PropertyExists -Queryobject $Pair -PropertyName ExceptionList)
        {
            $CurrentExceptions=@($Pair.ExceptionList.Exception)
        }
        If(Get-PropertyExists -Queryobject $Pair -PropertyName Filter)
        {
            if (($Pair.Filter -ne $Null) -and ($Pair.Filter -ne ""))
            {
                $CurrentFilter=$Pair.Filter
            }
        }
        $ResultObjects=Sync-OneFolder -SourceFolder $Pair.Source -TargetFolder $Pair.Target -PassedExceptions $CurrentExceptions -Filter $CurrentFilter -WhatIf:$WhatIf |
        Tee-Object -Variable FolderChanges
        if($FolderChanges -ne $Null)
        {
            $Changes+=$FolderChanges
        }
    }

}
Write-Log "Writing Statistics"
$FolderCreations=$FileCopies=$FileRemovals=$FolderRemovals=$FileUpdates=0
Foreach ($Change in $Changes)
{
    switch ($Change.Process)
    {
        "Create Folder"
        {
            $FolderCreations+=1
        }
        "Copy File"
        {
            $FileCopies+=1
        }
        "Remove File"
        {
            $FileRemovals+=1
        }
        "Remove Folder"
        {
            $FolderRemovals+=1
        }
        "Update File"
        {
            $FileUpdates+=1
        }
    }
}
Write-Log "Statistics" -WriteHost
Write-Log "" -WriteHost
Write-Log "Folder Creations: `t$FolderCreations" -WriteHost
Write-Log "Folder Removals: `t$FolderRemovals" -WriteHost
Write-Log "File Copies: `t`t$FileCopies" -WriteHost
Write-Log "File Removals: `t`t$FileRemovals" -WriteHost
Write-Log "File Updates: `t`t$FileUpdates`n" -WriteHost
If ($PassThru)
{
    $ResultObjects
}

57 thoughts on “PowerShell: Synchronizing a Folder (and Sub-Folders)”

  1. like the look of this and looks like what I’ve been looking for……. Previously used robocopy.
    Would you set this as a scheduled task to keep the files/folders uptodate?

      1. Yep that’s cool, id like to see if I can get this working to sync every hour our data offsite. I’ll give it a go and let you know how I get on.

  2. ok I’m probably being a bit thick here, well that and the heat is frying my brain. But where do I put the paths for the $SourceFolder and $TargetFolder

    1. You can do it in a couple of ways. The script is for a function, so it needs to be included in another script and called or . sourced into your current shell. So if you save that script to c:\temp\TestScript.ps1 you’d run;

      . c:\temp\TestScript.ps1

      Then in your PowerShell window you’d have a new function available, Sync-Folder. You could then run;

      Sync-Folder -SourceFolder:”c:\temp” -TargetFolder:”d:\temp” -Verbose (this last if you want to see what it’s doing).

      If you include it within an existing script just call the script with the previous line.

      You could modify it by removing the initial function statement (and enclosing braces) and run the entire thing as a script;

      c:\Temp\ScriptName -SourceFolder:”C:\temp” -TargetFolder:”D:\temp”

      Hope this helps!

  3. The link at the top of the page to “Part 3” doesn’t work. I was interested in seeing your xml sample but it redirects to a wordpress login page.

  4. Hi, I modified line 149 to “Remove-Item ($TargetFolder+”\”+$MissingFolder.Name) -Recurse”. I needed the -Recurse to delete removed items from subfolders without a prompt to run it as a scheduled task.

    It works perfect! So thanks a lot!

  5. Hi There,

    I’m testing this out and I’ve noticed line 46 has an “=” instead of the powershell “-eq” comparision. I think the elseif should look like this –

    elseif ($MissingFile.SideIndicator -eq “=>”)

    instead of

    elseif ($MissingFile.SideIndicator=”=>”)

    1. Thanks; have corrected it. I struggle to keep the code formatting in shape going from the ISE to WordPress so I sometimes miss things like that. Thanks again!

  6. I should have thanked you for the function also! Very useful. I’m using it in a script I’m building. I had to translate some of the other sections of code due to wordpress, nothing that find and replace and a bit of testing can’t fix! Cheers again

  7. Hi ,nice script.Unfortunately I have this issue:
    Compare-Object : Cannot bind argument to parameter ‘ReferenceObject’ because it is null.
    + $MissingFiles=Compare-Object <<<< $SourceFiles $TargetFiles -Property Name
    + CategoryInfo : InvalidData: (:) [Compare-Object], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.CompareObjectCommand
    Any idea? Thanks

  8. Guys any idea about this error:

    Compare-Object : Cannot bind argument to parameter ‘ReferenceObject’ because it is null.
    At C:\script\script.ps1:144 char:35
    + $MissingFolders=Compare-Object <<<< $SourceFolders $TargetFolders -Property Name
    + CategoryInfo : InvalidData: (:) [Compare-Object], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.CompareObjectCommand

    1. Hiya! Sorry there’s a problem. I’m not near a machine with PowerShell for a while but looking at the error it’s probably because $Sourcefolders is $null which is probably because there are no subfolders in the root folder you want to sync; maybe it’s just got files in it?

      To fix it you’ll need to check if Sourcefolders is null before running the command OR maybe forcing Sourcefolders to be an array; you could wrap the assignment with @().

      I really need to make catch the cases where there are no files or folders but one of those might stop the error!

  9. Compare-Object : Cannot bind argument to parameter ‘ReferenceObject’ because it is null.
    At C:\script\script.ps1:143 char:33
    + $MissingFiles=Compare-Object <<<< $SourceFiles $TargetFiles -Property Name
    + CategoryInfo : InvalidData: (:) [Compare-Object], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.CompareObjectCommand

  10. Excellent and inspiring work. I have spent the last few hours learning about the Compare-object command and I stumbled across this blog post. I really appreciate how you took the time to break down the script and explain what it does.
    As I speak it is syncing my documents and music to a backup drive, good work and thanks again for sharing.

    1. You’re very welcome! It’s great that so many people have found it useful, plus I find it far easier to learn about things myself if I have a project to apply it to.

  11. Looks cool! Will this grab files and folders from the target directory? Or just folder contents recursively? Will it get the files in the root (target directory)?

    1. Hi! No it won’t copy files from the Target to the Source; it there’s a file like that it’ll assume it’s been deleted from the Source and delete it from the Target.

      I might see how difficult it would be to add that as an option though…

      1. What about keeping a filelist from the previous sync and comparing to a current one (for both sides). The missing files must be the deletions.

      2. Hi. Yes, that or write an attribute to the file itself somewhere. I think a manifest file would be better but I’m trying to think of an efficient way to use them (as it could get quite large / slow with a lot of files + directories).

        Thanks for the comment!

    1. Hi! No, it won’t “sync back”; if there’s a file in the target folder that isn’t there in the source it’ll remove it (so that it syncs deletions).

      It might be an interesting option to add though…

  12. Hi! tested your Script.
    Syncing works awesome!

    Only the statistics aren’t working. Showing = 0 every time on every stat.
    Even if the script has devinitly copied files and created folders.

    Statistics

    Folder Creations: 0
    Folder Removals: 0
    File Copies: 0
    File Removals: 0
    File Updates: 0

    Successful Process Message
    ———- ——- ——-
    True Remove Folder Removing folder missing from Source : E:\Sync Target\6
    True Create Folder Adding folder missing from Target : E:\Sync Target\5
    True Create Folder Adding folder missing from Target : E:\Sync Target\5\e1
    True Copy File Copying missing file : E:\Sync Target\5\e1\78a3e8c5314e0dae41c531cb19366085.gif
    True Copy File Copying missing file : E:\Sync Target\5\e1\876aadb57de1d589c196ee1842c6ea0c.gif
    True Copy File Copying missing file : E:\Sync Target\5\e1\a982a30cc67bb6934834395239572800.gif
    True Create Folder Adding folder missing from Target : E:\Sync Target\5\e2
    True Copy File Copying missing file : E:\Sync Target\5\e2\78a3e8c5314e0dae41c531cb19366085.gif
    True Copy File Copying missing file : E:\Sync Target\5\e2\876aadb57de1d589c196ee1842c6ea0c.gif
    True Copy File Copying missing file : E:\Sync Target\5\e2\a982a30cc67bb6934834395239572800.gif
    True Create Folder Adding folder missing from Target : E:\Sync Target\5\e3
    True Copy File Copying missing file : E:\Sync Target\5\e3\78a3e8c5314e0dae41c531cb19366085.gif
    True Copy File Copying missing file : E:\Sync Target\5\e3\876aadb57de1d589c196ee1842c6ea0c.gif
    True Copy File Copying missing file : E:\Sync Target\5\e3\a982a30cc67bb6934834395239572800.gif

  13. Fixed the statistics not showing! There was a typo;

    Sync-OneFolder -SourceFolder $Pair.Source -TargetFolder $Pair.Target -PassedExceptions $CurrentExceptions -Filter $CurrentFilter |
    Tee-Object -Variable Changess

    should be;

    Sync-OneFolder -SourceFolder $Pair.Source -TargetFolder $Pair.Target -PassedExceptions $CurrentExceptions -Filter $CurrentFilter |
    Tee-Object -Variable Changes

    I’ve updated the script above.

  14. Awesome script, but I have 2 issues…
    1. I could never get the script to work with mapped network drives or UNC paths when using the Sync-FolderConfiguration.xml file, is this possible?

    2. and more importantly I cannot get the -Exceptions to work. I simply want to exclude a directory from the source. my command is… .\Sync-Folders.ps1 -SourceFolder:”\\192.168.1.102\music” -TargetFolder:”e:\” -Exceptions:”.\excludedfolder”

    Thanks

    1. I had a look and I tested UNC names in the Config file and passed as a parameter and they worked when I tried them. That said, there were some bugs with Filter and Exceptions I found that meant they wouldn’t work in some situations so it may have been that (fixed in the latest version). 🙂

      Also, Exceptions and Filters need to be wild-carded. The script works by checking the filepath of the file to be processed against each entry in Exceptions with a -Like command. So the Exception needs to have wild-cards in it. So if you have a file d:\temp\old\test.txt that you don’t want synced add *.txt, *test.txt or *old* to the exceptions list.

      I’ve added that to the docs, cheers!

  15. Line 118 has some issues with misplaced “try”. Also, is there a download link for the script, the copy/paste from the website is lacking.

  16. Hi,
    Can you update the script with email functionality (including the output.message info)?
    For now I have the following but I’m not able to catch the output.message info.

    $to = “bla@bla.com”
    $subject = “backup mail”
    $body = “`nBackup Statistics`nFolder Creations: `t$FolderCreations `nFolder Removals: `t$FolderRemovals `nFile Copies: `t`t$FileCopies `nFile Removals: `t`t$FileRemovals `nFile Updates: `t`t$FileUpdates `n”
    $smtpServer = “smtp.bla.bla”
    $smtpFrom = “info@bla.com”
    $smtpTo = $to
    $messageSubject = $subject
    $messageBody = $body

    $smtp = New-Object Net.Mail.SmtpClient($smtpServer)
    $smtp.Send($smtpFrom,$smtpTo,$messagesubject,$messagebody)

    And, if possible, with an option to log the output in a logfile?

  17. Keep getting the below error on a simple sync test and the second sync-pair never get’s synced…

    The property Exception cannot be found on this object. Verify that the property exists.
    At C:\Source\Repos\PowerShell Scripts\Sync-Folder.ps1:435 char:34
    $CurrentExceptions=@($Pair.ExceptionList.Exception)

    1. Hi! Can you post an example of the XML file you’re using (if you are)? It’s a problem in that bit of code so I’ll see if I can duplicate and fix it!

      Cheers

      1. It was a pretty simple file like the below:

        C:\synctest\source1
        C:\synctest\dest1
        *.txt

        p234*

        C:\synctest\source2
        C:\synctest\dest2

      2. Hi. I ran it with the following XML file which seems to be close to what you want;

        C:\synctest\source1
        C:\synctest\dest1
        *.txt

        *p234*.txt

        C:\synctest\source2
        C:\synctest\dest2

        I did find a slightly related bug which I’ve fixed though! I’ll upload a new version (1.9) as well.

      3. I’ve just noticed WordPress is stripping all the XML from the XML configuration file! I’ve updated the example with the one I posted above.

  18. Excellent script. Adding a two way sync where the most recent “date modified” wins would be an awesome addition.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s