public/Invoke-IncrementalFileBackup.ps1
function Invoke-IncrementalFileBackup { <# #> param( [ValidateSet("Debug","Verbose","Info","Warning","Error","Disable")] [string]$logLevel = "Debug", [Parameter(position = 0,ValueFromPipelineByPropertyName)] [string]$SourceDirectory = $null, [Parameter(position = 1,ValueFromPipelineByPropertyName)] [string]$BackupToRootPath = $null ,[Parameter(position = 2,ValueFromPipelineByPropertyName)] [string]$BackupName = $null ,[Parameter(position = 3,ValueFromPipelineByPropertyName)] [string]$BackupInstanceFormat = $null ,[Parameter(position = 4,ValueFromPipelineByPropertyName)] [string[]]$ExcludeDirectories = $null ,[Parameter(position = 5,ValueFromPipelineByPropertyName)] [int]$NumberOfIncrementalBeforeFull = 10 ,[switch]$ForceFull ,[int]$NumberOfBackupsToKeep = 20 ,[switch] $compressFiles = $true ) function Split-Array ([object[]]$InputObject,[int]$SplitSize=100) { #https://powershell.org/forums/topic/splitting-an-array-in-smaller-arrays/ $length=$InputObject.Length for ($Index = 0; $Index -lt $length; $Index += $SplitSize) { #, encapsulates result in array #-1 because we index the array from 0 ,($InputObject[$index..($index+$splitSize-1)]) } } $OrigLogLevel = Get-LogLevel $originalLocation = Get-Location import-module FC_Data -force -DisableNameChecking Write-Log "Original log level: $OrigLogLevel" Debug try { if ([string]::IsNullOrEmpty($logLevel)) { $logLevel = "Info" } Set-LogLevel $logLevel if ([string]::IsNullOrEmpty($SourceDirectory)) { Write-Log "Please pass a valid path to the directory you want to backup using the -SourceDirectory parameter" Error -ErrorAction Stop } if ([string]::IsNullOrEmpty($BackupToRootPath)) { Write-Log "Please pass a valid path to the root directory the backup will be written to using the -BackupToRootPath parameter" Error -ErrorAction Stop } if ([string]::IsNullOrEmpty($BackupName)) { Write-Log "Please pass a name for the backup, which will be used to create the sub folder structure of the timestamped backup files using the -BackupName parameter" Error -ErrorAction Stop } $anchorBackupDirectory = "$BackupToRootPath\$BackupName" $prevBackupDir = Get-ChildItem $anchorBackupDirectory | Where-Object { $_.PSIsContainer } | sort name -Descending | Select-Object -First 1 -ExpandProperty FullName if([string]::IsNullOrEmpty($prevBackupDir)){ } else{ Write-Log "Loading previous backup data" Verbose $PreviousBackupPath = "$prevBackupDir\FC_BackupData.json" $PreviousBackup = Get-Content $PreviousBackupPath -Raw | ConvertFrom-Json -ErrorAction Stop $IndexSinceLastFull = $PreviousBackup.IndexSinceLastFull Write-Log "Done loading previous backup data" Verbose } $backupInstant = $(Get-Date -Format $BackupInstanceFormat) if ($ForceFull -or $PreviousBackup -eq $null -or ($PreviousBackup.backupType -eq "Incremental" -and $PreviousBackup.IndexSinceLastFull -gt $NumberOfIncrementalBeforeFull)) { $backupType = "Full" $IndexSinceLastFull = 0 } else { $backupType = "Incremental" } Write-Log "Backup type: $backupType" $BackupInstanceName = "$($backupInstant)_$backupType" $destination = "$anchorBackupDirectory\$BackupInstanceName" if (Test-Path $SourceDirectory) { if (Test-Path $BackupToRootPath) { } else { Write-Error "Could not find the path to the backup directory: $BackupToRootPath" } } else { Write-Error "Can not find path: $SourceDirectory" } $outBackupDataPath = "$destination\FC_BackupData.json" [string]$backupDataConnectionString = $outBackupDataPath if (!(Test-Path $destination)) { New-Item $destination -ItemType Directory -Force -ErrorAction Stop | Out-Null } Write-Log "File backup started." Write-Log "root destination directory: $destination" -tabLevel 1 Write-Log "Scanning files ..." Set-Location $SourceDirectory $hashAlgorithm = "SHA256" $FileIOToCopy = Get-ChildItem $SourceDirectory -Force -Recurse #| where { ![string]::IsNullOrEmpty($ExcludeDirectories) -and $_.FullName -notmatch $ExcludeDirectories} $FilesToCopy = $FileIOToCopy | Where-Object { $_.PSIsContainer -ne $true } | Select-Object Name,FullName,Length $FilesToCopy | Add-Member -MemberType NoteProperty -Name "FileHash" -Value "" $FilesToCopy | Add-Member -MemberType NoteProperty -Name "FileHashAlgo" -Value $hashAlgorithm $FilesToCopy | Add-Member -MemberType NoteProperty -Name "WasUpdated" -Value 0 $FilesToCopy | Add-Member -MemberType NoteProperty -Name "LastBackup" -Value "$backupInstant" $fileCount = $FilesToCopy | Measure-Object | Select-Object -ExpandProperty Count $fileCountIndex = 0 $parallelQueues = 5 $splitArrays = @() $splitArrays = Split-Array -InputObject $FilesToCopy -SplitSize ($fileCount/5) $jobs = @() $i = 0; foreach ($array in $splitArrays){ $i++ $jobName = "$(Get-JobPrefix)FileBackup$i" $jobs += Start-Job { param($files,$hashAlgorithm,$destination,$PreviousBackup) foreach ($file in $files) { $file.FileHash = Get-FileHash -Path $file.FullName -Algorithm $hashAlgorithm | Select-Object -ExpandProperty Hash $relativePath = "$(Resolve-Path -Relative (Split-Path $file.FullName -Parent))\$($file.Name)" $relativePath = $relativePath.Substring(2,$relativePath.Length - 2) $destinationFilePath = "$destination\$relativePath" $DestinationFIleParentDirPath = Split-Path $destinationFilePath -Parent Write-Log (($file | Select-Object FullName,LastBackup,WasUpdated) -join ",") Verbose if (!(Test-Path (Split-Path $destinationFilePath -Parent))) { New-Item (Split-Path $destinationFilePath -Parent) -ItemType Directory -Force -ErrorAction Continue | Out-Null } if ($backupType -eq "Full") { Copy-Item -Path $file.FullName -Destination $destinationFilePath -Force $file.WasUpdated = 1 } elseif ($backupType -eq "Incremental") { if ($file.FileHash -eq ($PreviousBackup.Files | Where-Object { $_.FullName -eq $file.FullName } | Select-Object -ExpandProperty FileHash)) { $prevFilePath = "$prevBackupDir\$relativePath" if(!(Test-Path $prevFilePath)){ continue; } try{ New-Item -Path $destinationFilePath -ItemType SymbolicLink -Value $prevFilePath -ErrorAction Stop | Out-Null } catch{ $x = 0; } } else { Copy-Item -Path $file.FullName -Destination $destinationFilePath -Force $file.WasUpdated = 1 } } } }-ArgumentList ($array,$hashAlgorithm,$destination,$PreviousBackup) -Name $jobName } $jobPollTime = 15 $exit = $false while (!$exit){ $jobs = Request-JobStatus if ($jobs -eq (Get-JobsCompleteFlag) ){ $exit = $true } sleep $jobPollTime } $outputObj = New-Object -TypeName psobject $outputObj | Add-Member -MemberType NoteProperty -Name "BackupInstance" -Value $backupInstant $outputObj | Add-Member -MemberType NoteProperty -Name "backupType" -Value $backupType $outputObj | Add-Member -MemberType NoteProperty -Name "PreviousBackup" -Value $PreviousBackupPath $outputObj | Add-Member -MemberType NoteProperty -Name "IndexSinceLastFull" -Value ($IndexSinceLastFull+1) $outputObj | Add-Member -MemberType NoteProperty -Name "Files" -Value $FilesToCopy $outputObj | ConvertTo-Json -Depth 5 | Set-Content $outBackupDataPath Write-Log "Performing metadata file cleaning" $TotalbackupCount = (Get-Content "$anchorBackupDirectory\FC_RootBackup.log" | Measure-Object).Count "$BackupInstanceName,$TotalbackupCount" | Add-Content "$anchorBackupDirectory\FC_RootBackup.log" $clearLastNBackups = 10 $numberOfBackupsToDelete = $TotalbackupCount - $clearLastNBackups if ($numberOfBackupsToDelete -gt 0) { $folders = Get-ChildItem $anchorBackupDirectory | Where-Object { $_.PSIsContainer } $numToDelete = ($folders | Measure-Object | select -ExpandProperty Count) - $NumberOfBackupsToKeep if($numToDelete -gt 0){ Write-Log "Deleting the last $numberOfBackupsToDelete backups" $delete = $folders | sort name -Descending | Select-Object -Last $numToDelete -ExpandProperty FullName $delete| Remove-item -Force -Recurse } Write-Log "Removing the last $numberOfBackupsToDelete from $anchorBackupDirectory\FC_RootBackup.log" Debug (Get-Content "$anchorBackupDirectory\FC_RootBackup.log" | Select-Object -Skip $numberOfBackupsToDelete) | Set-Content "$anchorBackupDirectory\FC_RootBackup.log" } Write-Log "Done cleaning up the metadata file" Write-Log "File backup completed." } catch { throw } finally { Set-Location $originalLocation } } Export-ModuleMember -Function Invoke-IncrementalFileBackup |