Skip to content

Instantly share code, notes, and snippets.

@jburckel
Last active May 26, 2021 06:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jburckel/bf4d566c74f49a46fc3039011e3fcff2 to your computer and use it in GitHub Desktop.
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.)
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
<#
# 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
}
}
}
<#
# 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
}
<#
# 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
}
}
<#
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