Last active
May 26, 2021 06:07
-
-
Save jburckel/bf4d566c74f49a46fc3039011e3fcff2 to your computer and use it in GitHub Desktop.
Un script Powershell permettant de synchroniser deux dossiers et capable de gérer la suppression d'un dossier/fichier. (A Powershell script that synchronizes two folder and that handles deletion of a folder/file.)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
param ( | |
$sourcePath = "C:\Users\$([Environment]::UserName)\Documents\Test\Source", | |
$destinationPath = "C:\Users\$([Environment]::UserName)\Documents\Test\Destination", | |
$trackingDestination = "Source", | |
$trackingFolder = ".tracking", | |
$trackingFile = "origine.csv", | |
$trackingTrash = "Trash", | |
$logFile = "C:\Users\$([Environment]::UserName)\Documents\LogFile.log", | |
$logLevel = 2, | |
$outputLevel = 2 | |
) | |
################################################################### | |
# Paramètres internes | |
# Format de la date utilisé lors de l'export et l'import du fichier CSV | |
$dateFormat = "yyyy-MM-dd HH:mm:ss" | |
################################################################### | |
# Importation des fonctions | |
If (-Not (Test-Path "$PSScriptRoot\Modules")) { | |
Write-Error "Le dossier contenant les modules nécessaires à l'execution du script n'existe pas. Exécution impossible." | |
return | |
} | |
Import-Module "$PSScriptRoot\Modules\LsCustom.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop | |
Import-Module "$PSScriptRoot\Modules\CompareCustom.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop | |
Import-Module "$PSScriptRoot\Modules\ToLogFile.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop | |
Import-Module "$PSScriptRoot\Modules\CompareActions.psm1" -Forc -WarningAction SilentlyContinue -ErrorAction Stop | |
function Export-Log { | |
param( | |
[string]$Texte, | |
[ValidateSet("Info", "Error")] | |
[string]$Type | |
) | |
To-LogFile -Texte $Texte -Fichier $logFile -LogLevel $logLevel -Type $Type | |
} | |
$timer = [System.Diagnostics.Stopwatch]::StartNew() | |
################################################################### | |
# Vérification des dossiers nécessaires à la synchronisation | |
# Récupération du contenu des dossiers à synchroniser | |
try { | |
$source = Ls-Custom -Folder $sourcePath -Exclude $trackingFolder | |
} catch { | |
Write-Error $_ | |
Export-Log -Texte "Le dossier source n'existe pas : ($sourcePath)" -Type Error | |
return | |
} | |
If ($outputLevel -gt 1) { Write-Host "Liste des fichiers/dossiers dans la source" $timer.Elapsed } | |
try { | |
$destination = Ls-Custom -Folder $destinationPath -Exclude "prive" | |
} catch { | |
Export-Log -Texte "Le dossier destination n'existe pas : $($destinationPath) - $($_)" -Fichier $logFile -LogLevel $logLevel -Type Error | |
return | |
} | |
If ($outputLevel -gt 1) { Write-Host "Liste des fichiers/dossiers dans la destination" $timer.Elapsed } | |
$trackingContainer = $sourcePath | |
If ($trackingDestination -eq "Destination") { | |
$trackingContainer = $destinationPath | |
} | |
$trackingFolderFullName = Join-Path $trackingContainer -ChildPath $trackingFolder | |
$trackingFileFullName = Join-Path $trackingFolderFullName -ChildPath $trackingFile | |
$trackingTrashFullName = Join-Path $trackingFolderFullName -ChildPath $trackingTrash | |
If (-Not (Test-Path $trackingFolderFullName)) { | |
New-Item $trackingFolderFullName -ItemType Directory | Out-Null | |
} | |
# Cacher le dossier | |
try { | |
(Get-Item $trackingFolderFullName -Force).attributes = "Hidden" | |
} catch { | |
Export-Log -Texte $_ -Type Error | |
} | |
If (-Not (Test-Path $trackingTrashFullName)) { | |
New-Item $trackingTrashFullName -ItemType Directory | Out-Null | |
} | |
If ($outputLevel -gt 1) { Write-Host $timer.Elapsed } | |
# | |
If (-Not (Test-Path $trackingFileFullName)) { | |
# Le fichier de la dernière synchronisation n'est pas présent, on fait une synchronisation simple, sans suppression | |
# On compare le dossier source et le dossier de destination | |
try { | |
$diffs = Compare-Custom -Reference @($source | Select-Object) -Difference @($destination | Select-Object) | |
$diffs | %{ | |
Add-Member -InputObject $_ -NotePropertyName "CompareReference" -NotePropertyValue "Source" | |
Add-Member -InputObject $_ -NotePropertyName "CompareDifference" -NotePropertyValue "Destination" | |
Add-Member -InputObject $_ -NotePropertyName "TrackingType" -NotePropertyValue "Simple" | |
} | |
} catch { | |
Export-Log -Texte "Un problème est survenu lors de la comparaison entre la source et la destination." -Type Error | |
return | |
} | |
} Else { | |
# Synchronisation et suppression | |
try { | |
# On récupére les informations de la dernière synchronisation | |
$origine = Import-Csv -Path $trackingFileFullName -Encoding UTF8 -ErrorAction Stop | |
$origine | % { | |
$_.CreationTimeUTC = [DateTime]::ParseExact($_.CreationTimeUTC, $dateFormat, $null) | |
$_.LastWriteTimeUTC = [DateTime]::ParseExact($_.LastWriteTimeUTC, $dateFormat, $null) | |
If ($_.Length -eq '') { $_.Length = $null } | |
} | |
} catch { | |
Export-Log -Texte "Import du csv d'origine impossible." -Type Error | |
return | |
} | |
# Nécessaire de copier l'objet pour éviter des problèmes | |
$origineSource = $origine | ConvertTo-Json | ConvertFrom-Json | |
# Comparaison | |
try { | |
$diffSource = Compare-Custom -Reference @($origineSource | Select-Object) -Difference @($source | Select-Object) | |
$diffSource | %{ | |
Add-Member -InputObject $_ -NotePropertyName "CompareReference" -NotePropertyValue "Origine" | |
Add-Member -InputObject $_ -NotePropertyName "CompareDifference" -NotePropertyValue "Source" | |
Add-Member -InputObject $_ -NotePropertyName "TrackingType" -NotePropertyValue "Advanced" | |
} | |
} catch { | |
Export-Log -Texte "Un problème est survenu lors de la comparaison entre le fichier d'origine et la source" -Type Error | |
return | |
} | |
$origineDestination = $origine | ConvertTo-Json | ConvertFrom-Json | |
try { | |
$diffDestination = Compare-Custom -Reference @($origineDestination | Select-Object) -Difference @($destination | Select-Object) | |
$diffDestination | %{ | |
Add-Member -InputObject $_ -NotePropertyName "CompareReference" -NotePropertyValue "Origine" | |
Add-Member -InputObject $_ -NotePropertyName "CompareDifference" -NotePropertyValue "Destination" | |
Add-Member -InputObject $_ -NotePropertyName "TrackingType" -NotePropertyValue "Advanced" | |
} | |
} catch { | |
Export-Log -Texte "Un problème est survenu lors de la comparaison entre le fichier d'origine et la destination" -Type Error | |
return | |
} | |
# Concatenation des différences | |
$diffs = @() | |
If ($diffSource -ne $null) { $diffs += $diffSource } | |
If ($diffDestination -ne $null) { $diffs += $diffDestination } | |
} | |
If ($outputLevel -gt 1) { Write-Host $timer.Elapsed } | |
# Objet qui contiendra l'ensemble des modifications à apporter | |
$toDos = @{} | |
$toRemoveFolder = New-Object System.Collections.Generic.List[System.Object] | |
foreach($diff in $diffs) { | |
If (-Not $toDos.ContainsKey($diff.RelativeName)) { | |
If ($diff.SideIndicator -eq "<=" -and $diff.CompareReference -eq "Origine") { | |
# L'élément n'apparait plus dans l'un des dossiers | |
# Potentiellement supprimé | |
$toDos[$diff.RelativeName] = Compare-Actions -Difference $diff -Type Delete | |
} Else { | |
# Element n'existe pas dans le fichier de suivi ou dans l'un des dossiers | |
# Nouveau fichier (ou fichier mis à jour) | |
$toDos[$diff.RelativeName] = Compare-Actions -Difference $diff -Type Create | |
} | |
} Else { | |
# Si l'on a déjà rencontré cet élément auparavant | |
If ($diff.SideIndicator -eq "=>") { | |
$toDos[$diff.RelativeName] = Compare-Actions -Difference $diff -Initial $toDos[$diff.RelativeName] | |
} Else { | |
# Only in Reference Object | |
If ($diff.CompareReference -eq "Origine") { | |
If ($toDos[$diff.RelativeName].TrackingAction -eq "Delete") { | |
# Fichier supprimé dans les deux dossiers | |
# Ne rien faire | |
$toDos.Remove($diff.RelativeName) | |
} Else { | |
# Fichier modifié dans un dossier et supprimé dans l'autre | |
# Ne rien faire | |
} | |
} Else { | |
# Fichier Mis à jour | |
$toDos[$diff.RelativeName] = Compare-Actions -Difference $diff -Initial $toDos[$diff.RelativeName] | |
} | |
} | |
} | |
} | |
If ($outputLevel -gt 1) { Write-Host $timer.Elapsed } | |
$toRemoveFolder = New-Object System.Collections.Generic.List[System.Object] | |
# Recherche de dossier à supprimer | |
# A reflechir si nécessaire de le faire dans une autre boucle au cas où le dossier est à mettre à jour | |
foreach ($toDo in $toDos.Values) { | |
If ($toDo.Type -eq "Folder" -and $toDo.TrackingAction -eq "Delete") { | |
$toRemoveFolder.Add($toDo) | |
} | |
} | |
# Conserver uniquement le dossier de premier niveau dans l'arborescence | |
foreach ($folder in $toRemoveFolder) { | |
$toRemoveKeys = @{} | |
foreach ($key in $toDos.Keys) { | |
$toDo = $toDos[$key] | |
If ( | |
($folder.RelativeName -ne $toDo.RelativeName) -and ($toDo.RelativePath -match "^" + [regex]::Escape($folder.RelativeName)) | |
) { | |
$toRemoveKeys.Add($key, $true) | |
} | |
} | |
foreach ($key in $toRemoveKeys.Keys) { | |
$toDos.Remove($key) | |
} | |
} | |
If ($outputLevel -gt 1) { Write-Host $timer.Elapsed } | |
$modification = 0 | |
$syncFailed = $false | |
foreach ($toDo in $toDos.Values) { | |
$fromPath = $sourcePath | |
$toPath = $destinationPath | |
If ($toDo.TrackingTakeFrom -eq "Destination") { | |
$fromPath = $destinationPath | |
$toPath = $sourcePath | |
} | |
If ($toDo.TrackingAction -eq "Delete") { | |
$toPath = Join-Path $trackingTrashFullName -ChildPath (Get-Date -Format "yyyy-MM-dd") | |
If (-Not (Test-Path $toPath)) { | |
New-Item $toPath -ItemType Directory | Out-Null | |
} | |
} | |
$from = Join-Path $fromPath -ChildPath $toDo.RelativeName | |
$to = Join-Path $toPath -ChildPath $toDo.RelativeName | |
If ($toDo.TrackingAction -ne "Delete" -and $toDo.Type -eq "File") { | |
# Créer tout d'abord, si nécessaire, l'arborescence | |
$container = Join-Path $toPath -ChildPath $toDo.RelativePath | |
If (-Not (Test-Path $container)) { | |
New-Item $container -ItemType Directory | Out-Null | |
To-LogFile -Texte "Le dossier a été $container créé." -Fichier $logFile -Type Info -LogLevel $logLevel | |
} | |
try { | |
$element = Copy-Item -Path $from -Destination $to -PassThru -ErrorAction Stop | |
Export-Log -Texte "L'élément $from a été copié dans $to." -Type Info | |
$modification += 1 | |
} catch { | |
$syncFailed = $true | |
Export-Log -Texte "L'élément $from n'a pas pu être copié dans $to." -Type Error | |
} | |
} Else { | |
If ($toDo.TrackingAction -eq "Delete") { | |
try { | |
Move-Item $from -Destination $toPath -Force -ErrorAction Stop | Out-Null | |
Export-Log -Texte "L'élément $from a été déplacé dans $toPath." -Type Info | |
$modification += 1 | |
} catch { | |
$syncFailed = $true | |
Export-Log -Texte "L'élément $from n'a pas pu être déplacé dans $toPath." -Type Error | |
} | |
} | |
If (Test-Path $to) { | |
$element = Get-Item $to -ErrorAction Stop | |
} ElseIf ($toDo.Type -eq "Folder") { | |
$element = New-Item $to -ItemType Directory | |
Export-Log -Texte "Le dossier $to a été créé." -Type Info | |
} Else { | |
Export-Log -Texte "Il y a eu un problème lors du déplacement de l'élément $to" -Type Error | |
} | |
} | |
# | |
# On fait correspondre les dates de création | |
try { | |
$element.CreationTimeUtc = $toDo.CreationTimeUtc | |
$element.LastWriteTimeUTC = $toDo.LastWriteTimeUtc | |
} catch { | |
Export-Log -Texte "La date de l'élément $($element.FullName) n'a pas pu être mise à jour" -Type Error | |
} | |
} | |
If ($outputLevel -gt 1) { Write-Host $timer.Elapsed } | |
# Sauvegarder l'état du dossier | |
try { | |
If (-Not $syncFailed) { | |
$n_origine = Ls-Custom -Folder $sourcePath | |
$n_origine | %{ | |
$_.CreationTimeUTC = $_.CreationTimeUTC.ToString($dateFormat) | |
$_.LastWriteTimeUTC = $_.LastWriteTimeUTC.ToString($dateFormat) | |
} | |
$n_origine | Export-Csv -Path $trackingFileFullName -Encoding UTF8 | Out-Null | |
Export-Log -Texte "Le fichier 'origine' $trackingFileFullName a bien été créé." -Type Info | |
} else { | |
If (Test-Path $trackingFileFullName) { | |
Remove-Item -Path $trackingFileFullName -Force | |
Export-Log -Texte "La syncronisation n'a pas été complète. Suppression du fichier 'origine'" -Type Info | |
} | |
} | |
} catch { | |
Export-Log -Texte "Il y a eu un problème lors de la création du fichier 'origine'." -Type Error | |
} | |
If ($outputLevel -gt 1) { Write-Host $timer.Elapsed } | |
$timer.Stop() | |
Export-Log -Texte "Fin de la mise à jour des dossiers $sourcePath et $destinationPath. $modification modification(s) apportée(s)." -Type Info |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
# Les actions à entreprendre suivant les changements détectés. | |
#> | |
function Compare-Actions { | |
param( | |
[object]$Difference, | |
[ValidateSet("Create", "Update", "Delete")] | |
[string]$Type, | |
[object]$Initial = $null | |
) | |
switch ($Type) { | |
Create { | |
$copy = $Difference.PSObject.Copy() | |
$takeFrom = $Difference.CompareReference | |
If ($Difference.SideIndicator -eq "=>") { $takeFrom = $Difference.CompareDifference } | |
Add-Member -InputObject $copy -NotePropertyName "TrackingAction" -NotePropertyValue "Create" -Force | |
Add-Member -InputObject $copy -NotePropertyName "TrackingTakeFrom" -NotePropertyValue $takeFrom -Force | |
return $copy | |
} | |
Update { | |
$copy = $Initial | |
If ((Get-Date $Difference.LastWriteTimeUtc) -gt (Get-Date $Initial.LastWriteTimeUtc)) { | |
$copy = $Difference.PSObject.Copy() | |
$takeFrom = $Difference.CompareReference | |
If ($Difference.SideIndicator -eq "=>") { $takeFrom = $Difference.CompareDifference } | |
Add-Member -InputObject $copy -NotePropertyName "TrackingTakeFrom" -NotePropertyValue $takeFrom -Force | |
} | |
Add-Member -InputObject $copy -NotePropertyName "TrackingAction" -NotePropertyValue "Update" -Force | |
return $copy | |
} | |
Delete { | |
$copy = $Difference.PSObject.Copy() | |
$takeFrom = "Source" | |
If ($Difference.CompareDifference -eq "Source") { $takeFrom = "Destination" } | |
Add-Member -InputObject $copy -NotePropertyName "TrackingTakeFrom" -NotePropertyValue $takeFrom -Force | |
Add-Member -InputObject $copy -NotePropertyName "TrackingAction" -NotePropertyValue "Delete" -Force | |
return $copy | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
# Comparaisons de deux arborescences suivant les propriétés définies | |
# L'option PassThru permet de récupérer l'ensemble des propriétés, pas uniquement celles qui sont comparées. | |
#> | |
function Compare-Custom { | |
param( $Reference, $Difference) | |
return Compare-Object -ReferenceObject $Reference -DifferenceObject $Difference -Property RelativeName, Length -PassThru -ErrorAction Stop | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
# Générer les logs de façon uniforme | |
#> | |
function To-LogFile { | |
param ( | |
[string]$Texte, | |
[string]$Fichier, | |
[int]$LogLevel, | |
[ValidateSet("Info", "Error")] | |
[string]$Type | |
) | |
If ( | |
$logLevel -gt 1 -or ($LogLevel -gt 0 -and $Type -eq "Error") | |
) { | |
"$($Type): $(Get-Date -Format 'yyyy-MM-dd hh:mm:ss') $($Texte)" | Out-File -FilePath $Fichier -Append | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
Une fonction de type ls récupérant les informations nécessaires à la synchronisation | |
#> | |
function Ls-Custom { | |
param( | |
[string]$Folder, | |
[string]$Exclude="" | |
) | |
try { | |
If (-Not (Test-Path $Folder)) { throw "Erreur" } | |
$content = ls $Folder -Recurse -Exclude $Exclude -ErrorAction Stop | |
} catch { | |
throw "Dossier Introuvable" | |
} | |
$content | % { | |
# Mettre les millisecondes à 0 pour éviter les problèmes lors de la comparaison | |
$_.CreationTimeUTC = Get-Date ($_.CreationTimeUtc) -Millisecond 0 | |
$_.LastWriteTime = Get-Date ($_.LastWriteTime) -Millisecond 0 | |
# On récupére le chemin relatif et le nom du dossier | |
$relativeName = $_.FullName -replace [regex]::Escape($Folder), "" | |
$relativeName = $relativeName -replace "^\\", "" | |
$relativePath = $relativeName -replace [regex]::Escape($_.Name), "" | |
# On récupère le type fichier/dossier | |
$type = "Folder" | |
If($_.GetType() -eq [System.IO.FileInfo]) { $type = "File" } | |
Add-Member -InputObject $_ -NotePropertyName "Type" -NotePropertyValue $type | |
Add-Member -InputObject $_ -NotePropertyName "RelativeName" -NotePropertyValue $relativeName | |
Add-Member -InputObject $_ -NotePropertyName "RelativePath" -NotePropertyValue $relativePath | |
} | |
return $content | Select Fullname, Name, RelativeName, RelativePath, Length, Type, CreationTimeUTC, LastWriteTimeUTC | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment