Sunil Kumar
Sunil Kumar

Reputation: 361

How to keep 2 folders in sync using powershell script

We have two folders:

Now, I want to keep FolderA and FolderB in sync (i.e. when a user changes/adds/removes a file/directory in FolderA then same changes should happen in FolderB).

I tried :

$Date = Get-Date 
$Date2Str = $Date.ToString("yyyMMdd") 
$Files = gci "D:\Powershell\Original" 
ForEach ($File in $Files){
        $FileDate = $File.LastWriteTime
        $CTDate2Str = $FileDate.ToString("yyyyMMdd")
        if ($CTDate2Str -eq $Date2Str) { 
           copy-item "D:\Powershell\Original" "D:\Powershell\copy" -recurse    
           -ErrorVariable capturedErrors -ErrorAction SilentlyContinue; 
        } 
}

But this would require a similar powershell script for deletion of files in FolderA and changes in FolderB.

Upvotes: 32

Views: 78308

Answers (6)

bielawski
bielawski

Reputation: 1702

I kind of liked the simplicity of Zhisong Zhang answer but it had several problems. For example creating files to be deleted later was unnecessary. It was incapable of telling which directory a file resided within so if a file of a given name existed in one directory in the source and a different directory in the target it didn't copy. Also, it didn't support delete or recursive directory copies. Fixing all these only took a couple minor tweaks so the majority of the credit still belongs to Zhisong Zhang.

$source="\\DS920p\home\WindowsHome\Documents\OpenSCAD" 
$target="\\DS920p\FileShare\MakerFolders\3DPrinter\OpenSCAD" 
 
enum SyncMode {
    CopyOnly      = 1
    CopyDelete    = 2
    CopyBothWays  = 3
}
 
$sourceFiles=@(Get-ChildItem -Path $source -Recurse|%{$_.FullName.Remove(0,$source.Length)})
$targetFiles=@(Get-ChildItem -Path $target -Recurse|%{$_.FullName.Remove(0,$target.Length)})
 
[SyncMode]$syncMode=[SyncMode]::CopyDelete
 
try{
    $diff=Compare-Object -ReferenceObject $sourceFiles -DifferenceObject $targetFiles
 
    foreach($f in $diff) {
        
        if($f.SideIndicator -eq "<=") {
            $fullSourceObject=$source+$f.InputObject
            $fullTargetObject=$target+$f.InputObject
 
            Write-Host "Copying: $fullSourceObject `nTo     : $fullTargetObject"
            Copy-Item -Path $fullSourceObject -Destination $fullTargetObject -Recurse
        }
 
 
        if($f.SideIndicator -eq "=>") {
            $fullSourceObject=$target+$f.InputObject
            $fullTargetObject=$source+$f.InputObject
 
            if($syncMode -eq [SyncMode]::CopyDelete){
                Write-Host "Deleting: $fullSourceObject"
                Remove-Item -Path $fullSourceObject -Recurse
            }
 
            if($syncMode -eq [SyncMode]::CopyBothWays){
                Write-Host "Copying: $fullSourceObject `nTo     : $fullTargetObject"
                Copy-Item -Path $fullSourceObject -Destination $fullTargetObject -Recurse
            }
        }
 
    }
}      
catch {
    Write-Error -Message "something bad happened!`n$($Error.Item(0).Message)" -ErrorAction Stop
}

Be forewarned that unlike the original code which would not copy something that was only moved because it did exist somewhere. This will see them as separate files.

Upvotes: 0

LTusche
LTusche

Reputation: 11

I couldn't find a solution that works in both directions, so I created my own.

The main issue with two-way synchronization is that you can't determine if a file was created in one folder or deleted in the other because there is nothing to compare to.

The solution to this problem is to compare the directories, not the files. The modification timestamp of a directory changes only when a file or subfolder is added, renamed, or deleted.

This script compares the existing files and folders in both directories. If one folder is more recent because a file has been added, renamed, or deleted, the folder is mirrored.

By making a recursion before mirroring, I copy modified files back before they get overwritten. This way we always retain the most recent files while still synchronizing the file structure.

$source = "D:\Homework"
$target = "C:\Users\User\Documents\Homework"

function Sync-Folders ($src, $dst) {
  Get-ChildItem $src | ForEach-Object {
    $srcPath = Join-Path $src $_.Name
    $dstPath = Join-Path $dst $_.Name

    if ($_.PSIsContainer) {
      if (Test-Path $dstPath) {
        $srcTime = (Get-Item $srcPath).LastWriteTime
        $dstTime = (Get-Item $dstPath).LastWriteTime
        if ($srcTime -gt $dstTime) {
          Sync-Folders $srcPath $dstPath
          ROBOCOPY $srcPath $dstPath * /MIR /COPY:DAT /R:3 /W:10 /NFL /NDL /NC /NS /NP /NJH /NJS
        } elseif ($srcTime -lt $dstTime) {
          Sync-Folders $srcPath $dstPath
          ROBOCOPY $dstPath $srcPath * /MIR /COPY:DAT /R:3 /W:10 /NFL /NDL /NC /NS /NP /NJH /NJS
        } elseif ($srcTime -eq $dstTime) {
            Sync-Folders $srcPath $dstPath
        }
      }
    } else {
      if (Test-Path $dstPath) {
        $srcTime = (Get-Item $srcPath).LastWriteTime
        $dstTime = (Get-Item $dstPath).LastWriteTime
        if ($srcTime -gt $dstTime) {
          Copy-Item $srcPath $dstPath
        } elseif ($srcTime -lt $dstTime) {
          Copy-Item $dstPath $srcPath 
        }
      }
    }
  }
}

Sync-Folders $source $target

Pause

Upvotes: 0

Rovshan Musayev
Rovshan Musayev

Reputation: 144

Here is a full-scale CLI for keeping one way of sync

# Script will sync $source_folder into $target_folder and delete non relevant files. 
# If $target_folder doesn't exist it will be created. 

param ($source_folder, $target_folder, $cleanup_target = "TRUE", $log_file = "sync.log")

function Write-Log {
    Param ([string]$log_string, [string]$log_level = "INFO")
    $time_stamp = (Get-Date).toString("dd-MM-yyyy HH:mm:ss")
    $log_message = "$time_stamp [$log_level] $log_string"
    Add-content $log_file -value $log_message
    if ($log_level = "INFO") {
        Write-Host $log_message
    }
    elseif ($log_level = "ERROR") {
        Write-Error $log_message
    }
    elseif ($log_level = "WARNING") {
        Write-Warning $log_message
    }
    else {
        Write-Error "Wrong log level: $log_level"
        exit 1
    }
}

if (!(Test-Path -Path $source_folder -PathType Container)) {
    Write-Log "Source folder doesn't exist: $source_folder" "ERROR"
    exit 1
}

if (Test-Path -Path $target_folder -PathType Leaf) {
    Write-Log"Target object is file. Can't create target folder with the same name: $target_folder" "ERROR"
    exit 1
}

$source_content = Get-ChildItem -Path $source_folder -Recurse
if ($null -eq $source_content) { 
    $source_content = [array]
}

$target_content = Get-ChildItem -Path $target_folder -Recurse
if ($null -eq $target_content) { 
    $target_content = [array]
}

Write-Log "************************** Started syncing $source_folder >> $target_folder **************************"

$differences = Compare-Object -ReferenceObject $source_content -DifferenceObject $target_content


foreach ($difference in $differences) {
    if ($difference.SideIndicator -eq "<=") {
        $source_object_path = $difference.InputObject.FullName
        $target_object_path = $source_object_path.Replace($source_folder, $target_folder)
        if (Test-Path -Path $source_object_path -PathType Leaf) {
            $hash_source_file = (Get-FileHash $source_object_path -Algorithm SHA256).Hash
            if (Test-Path -Path $target_object_path -PathType Leaf) {
                $hash_target_file = (Get-FileHash $target_object_path -Algorithm SHA256).Hash
            }
            else {
                $hash_target_file = $null
            }
            if ( $hash_source_file -ne $hash_target_file ) {
                Write-Log "Synced file $source_object_path >> $target_object_path"
                Copy-Item -Path $source_object_path -Destination $target_object_path
            }
            else {
                Write-Log "Same file, will not sync $source_object_path >> $target_object_path"
            }
        }
        elseif (Test-Path -Path $target_object_path -PathType Container) {
            Write-Log "Folder already exists, will not sync $source_object_path >> $target_object_path"
        }
        else {
            Write-Log "Synced folder $source_object_path >> $target_object_path"
            Copy-Item -Path $source_object_path -Destination $target_object_path
        }        
    }
    elseif (($difference.SideIndicator -eq "=>") -and $cleanup_target -eq "TRUE") {
        $target_object_path = $difference.InputObject.FullName
        $source_object_path = $target_object_path.Replace($target_folder, $source_folder)
        if (!(Test-Path -Path $source_object_path) -and (Test-Path -Path $target_object_path)) {
            Remove-Item -Path $target_object_path -Recurse -Force
            Write-Log "Removed $target_object_path"
        }
    }
}
Write-Log "************************** Ended syncing $source_folder >> $target_folder **************************"

Upvotes: 0

Zhisong Zhang
Zhisong Zhang

Reputation: 81

I think you should try the following, it works for me change the syncMode base on your requirement. 1 is for one-way sync source to target, 2 is dual-way sync

$source="The source folder" 
$target="The target folder" 

touch $source'initial.initial'
touch $target'initial.initial'

$sourceFiles=Get-ChildItem -Path $source -Recurse
$targetFiles=Get-ChildItem -Path $target -Recurse

$syncMode=2 

    try{
    $diff=Compare-Object -ReferenceObject $sourceFiles -DifferenceObject $targetFiles

    foreach($f in $diff) {
        if($f.SideIndicator -eq "<=") {
            $fullSourceObject=$f.InputObject.FullName
            $fullTargetObject=$f.InputObject.FullName.Replace($source, $target)

            Write-Host "Attemp to copy the following: " $fullSourceObject
            Copy-Item -Path $fullSourceObject -Destination $fullTargetObject
        }


        if($f.SideIndicator -eq "=>" -and $syncMode -eq 2) {
            $fullSourceObject=$f.InputObject.FullName
            $fullTargetObject=$f.InputObject.FullName.Replace($target,$source)

            Write-Host "Attemp to copy the following: " $fullSourceObject
            Copy-Item -Path $fullSourceObject -Destination $fullTargetObject
        }

    }
    }      
    catch {
    Write-Error -Message "something bad happened!" -ErrorAction Stop
    }
    rm $source'initial.initial'
    rm $target'initial.initial'

Upvotes: 8

Paul Fijma
Paul Fijma

Reputation: 477

in addition to previous answers this article about the way you compare files might also help. to actually compare file by content needs an additional step. (like hashing). a detailed description of this method is written here: https://mcpmag.com/articles/2016/04/14/contents-of-two-folders-with-powershell.aspx

Upvotes: 0

clD
clD

Reputation: 2611

Have you looked at Robocopy (Robust File Copy)? It can be used with PS and provides what your looking for i.e. it is designed for reliable copying or mirroring of folders (changes/adds/removes) just select the options as required.

Robocopy sourceFolder destinationFolder /MIR /FFT /Z /XA:H /W:5

The /MIR option mirrors the source directory and the destination directory. It will delete files at the destination if they were deleted at the source.

Robocopy

Upvotes: 72

Related Questions