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 = "yyyy.MM.dd_HH.mm.ss" , [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... at $SourceDirectory" 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 Write-Log "Found $fileCount files" $fileCountIndex = 0 $parallelQueues = 5 $splitArrays = @() if($fileCount -eq 1){ $splitArrays = $FilesToCopy } else{ $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) $files = $array Write-Log "Looping over the files" 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 } Write-Log "BackupType: $backupType" if ($backupType -eq "Full") { Write-Log "Copy $($file.FullName) to $destinationFilePath" 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 { throw } } 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" -ErrorAction SilentlyContinue) | 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 |