====== Powershell Backup ====== #requires -version 3.0 #### PowerShell Backup #### Version 1.18 #### Last updated 2014-02-25 #### The canonical version of this script is at http://tallguyracing.com/wiki/doku.php?id=powershell_backup Param($backupType) $rootBackupDirectory = 'D:\Backups' $remoteBackupDirectory = 'P:\StephenH\Backups' $minHoursBetweenFullBackup = 20 $tfExe = 'C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\TF.exe' $7zExe = 'C:\Program Files\7-Zip\7z.exe' $tfsServer = 'cybertron.provoke.co.nz' $incrBackupFilenameMask = "Incr_Backup_{0:00}.7z" $full0BackupFilenameMask = "Full_L0_Backup_{0:00}.7z" $full1BackupFilenameMask = "Full_L1_Backup_{0:00}.7z" $firstIncrBackupFilename = "Incr_Backup_00.7z" $firstFull0BackupFilename = "Full_L0_Backup_00.7z" $firstFull1BackupFilename = "Full_L1_Backup_00.7z" $incrListingFilename = "Incr_Listing.txt" $full0ListingFilename = "Full_L0_Listing.txt" #### Logging code start $LogHeader = 0; $LogError = 1; $LogWarning = 2; $LogMessage = 3; $LogDebug = 4; $DisplayLevel = 3; $LogRootDirectory = "D:\Backups\Logs" $LogFileSizeLimit = 50 * 1024 * 1024 $LogFileResizeLines = 100000 # When a log file size exceeds $LogFileSizeLimit bytes and SizeLimitLogFiles is called, the log file will be # truncated to the last $LogFileResizeLines lines. 10000 lines of text = approx 1mb file size. $LogFiles = @( , @($LogDebug, "$LogRootDirectory\Debug.txt") , @($LogMessage, "$LogRootDirectory\Message.txt") ); # Note that an extra comma has deliberately been inserted before the first element. This prevents PowerShell # expanding out the first element. Function Write-Log ($logType, $message) { $formattedMessage = Get-FormatedLog $logType $message foreach ($logFile in $LogFiles) { if ($logType -le $logFile[0]) { Add-Content -Path $logFile[1] -Value $formattedMessage } } Write-LogHost $logType $formattedMessage } Function Get-FormatedLog ($logType, $message) { If (($logType -eq $LogHeader) -and ($message -ne "")) { $message = "**** " + $message + " ****" } ElseIf ($logType -eq $LogError) { $message = "ERROR: " + $message } ElseIf ($logType -eq $LogWarning) { $message = "Warning: " + $message } $now = Get-Date $prefix = $now.ToString("yyyyMMdd HHmmssff: ") $message = $prefix + $message.Replace("`n", "`n" + $prefix) return $message } Function Write-LogHost ($logType, $message) { If ($logType -gt $DisplayLevel) { return } If ($logType -eq $LogHeader) { Write-Host ($message) -f green } ElseIf ($logType -eq $LogError) { Write-Host ($message) -f red } ElseIf ($logType -eq $LogWarning) { Write-Host ($message) -f yellow } ElseIf ($logType -eq $LogDebug) { Write-Host ($message) -f white } Else { Write-Host ($message) } } Function SizeLimitLogFiles() { Write-Log $LogMessage "Checking the size of the log files." foreach ($logFile in $LogFiles) { $logFilename = $logFile[1] $length = (New-Object io.FileInfo $logFilename).Length Write-Log $LogDebug "logFilename = '$logFilename'" Write-Log $LogDebug "length = $length" if ($length -gt $LogFileSizeLimit) { Write-Log $LogMessage "Resizing the log file '$logFilename'." (Get-Content $logFilename)[-$LogFileResizeLines .. -1] | Set-Content $logFilename } } } #### Logging code end Function Roll ($fileNameMask, $limit) { Write-Log $LogMessage ("Rolling " + ($fileNameMask -f "xx")) Write-Log $LogDebug "Roll: limit = $limit" $lastFileName = ($fileNameMask -f ($limit + 0)) Write-Log $LogDebug "Roll: lastFileName = '$lastFileName'" if (Test-Path $lastFileName) { Write-Log $LogDebug "Removing '$lastFileName'" Remove-Item $lastFileName } for ($i = $limit - 1; $i -ge 0; $i--) { $fromFileName = ($fileNameMask -f $i) $toFileName = ($fileNameMask -f ($i + 1)) if (Test-Path $fromFileName) { Write-Log $LogDebug "Renaming '$fromFileName' to '$toFileName'" Rename-Item $fromFileName $toFileName } } } Function Coalesce($a, $b) { if ($a -ne $null) { $a } else { $b } } Function GetFileDescriptions($directory) { (Get-ChildItem -Recurse -Path $directory *.* | ForEach-Object { "{0};{1};{2:yyyyMMddhhmmss}" -f $_.FullName, (Coalesce $_.Length 0), $_.LastWriteTimeUtc } | Sort | Out-String -Width 999999).TrimEnd() } function GetFileDescriptionsFromList($changedFilesList) { ($changedFilesList -split "`n" | ForEach-Object { New-Object IO.FileInfo ($_.Trim()) } | ForEach-Object { "{0};{1};{2:yyyyMMddhhmmss}" -f $_.FullName, (Coalesce $_.Length 0), $_.LastWriteTimeUtc } | Sort | Out-String -Width 999999).TrimEnd() } Function GetFlags ($contentType) { switch ($contentType) { 'all' { return '' } 'tfs' { return '' } 'cs' { return '-x!*.dll', '-x!*.zip', '-x!*.pdf', '-x!*.pdb', '-x!*.exe', '-x!*.mp3', '-x!*.gif', '-x!*.png', '-x!*.jpg', '-x!*.jpeg', '-x!*.SnsDelta', '-x!*.SnsIndex', '-x!*.dbmdl', '-x!*.svn-base', '-x!*.nupkg', '-x!*.scc', '-x!*.rdl.data', '-x!*.cache', '-x!$tf', '-x!PAF2_V200?Q?V0?_DELIVERY_ADDRESSES*' } default { Write-Log $LogError "Unknown content type: $contentType" Exit(-1) } } } function Is7zFileEmpty($archiveFilename) { $archiveContents = (& "C:\Program Files\7-Zip\7z.exe" l $archiveFilename | Out-String -Width 999999).TrimEnd() $7zExitCode = $LastExitCode Write-Log $LogDebug $archiveContents if ($7zExitCode -gt 0) { Write-Log $LogError "An error occurred calling 7z. Exit code was $7zExitCode." Exit(-1) } return $archiveContents.EndsWith("0 files, 0 folders") } function GetTfsChangedFiles($backupSourceDirectory) { Set-Location $backupSourceDirectory $tfsStatus = (& $tfExe status) $tfsExitCode = $LastExitCode if ($tfsExitCode -gt 1) { Write-Log $LogError "An error occurred calling TF. Exit code was $tfsExitCode." Exit(-1) } if (! ($tfsStatus[0] -match "Change\s+Local path")) { Write-Log $LogError "Could not parse the TF status - could not find the headings." Exit(-1) } #Write-Log $LogDebug "GetTfsChangedFiles: tfsStatus = $tfsStatus" $changeIndex = $tfsStatus[0].IndexOf($Matches[0]) #Write-Log $LogDebug "GetTfsChangedFiles: changeIndex = $changeIndex" if ($changeIndex -le 0) { Write-Log $LogError "Could not parse the TF status - could not determine the index of the headings." Exit(-1) } $tfsStatus = $tfsStatus -replace "^.{$changeIndex}", '' | Select-String -Pattern '^(add |edit )(.+)$' | Out-String -Width 999999 #Write-Log $LogDebug "GetTfsChangedFiles: tfsStatus = $tfsStatus" $tfsStatus.Trim().Replace('add ', '').Replace('edit ', '') } function Backup ($backupName, $backupSourceDirectory, $backupType, $contentType, $maxBackupCount, $maxLevels, $levelReduceFactor) { Write-Log $LogHeader "$backupName $backupType" Write-Log $LogDebug "Backup: backupName = $backupName" Write-Log $LogDebug "Backup: backupSourceDirectory = $backupSourceDirectory" Write-Log $LogDebug "Backup: backupType = $backupType" Write-Log $LogDebug "Backup: contentType = $contentType" Write-Log $LogDebug "Backup: maxBackupCount = $maxBackupCount" Write-Log $LogDebug "Backup: maxLevels = $maxLevels" Write-Log $LogDebug "Backup: levelReduceFactor = $levelReduceFactor" $backupDestinationDirectory = "$rootBackupDirectory\$backupName" $flags = GetFlags($contentType) Write-Log $LogDebug "Backup: backupDestinationDirectory = $backupDestinationDirectory" Write-Log $LogDebug "Backup: flags = $flags" switch ($backupType) { "quick" { $listingFilename = $incrListingFilename $backupFilenameMask = $incrBackupFilenameMask } "full" { $listingFilename = $full0ListingFilename $backupFilenameMask = $full0BackupFilenameMask } default { Write-Log $LogError "Unknown backup type: $backupType" Exit(-1) } } $listingFullFilename = "$backupDestinationDirectory\$listingFilename" $currentBackupFilename = $backupFilenameMask -f 0 $currentBackupFullFilename = "$backupDestinationDirectory\$currentBackupFilename" $tempBackupFilename = $backupFilenameMask -f "xx" $tempBackupFullFilename = "$backupDestinationDirectory\$tempBackupFilename" $currentFull0BackupFullFilename = "$backupDestinationDirectory\$firstFull0BackupFilename" $currentFull1BackupFullFilename = "$backupDestinationDirectory\$firstFull1BackupFilename" Write-Log $LogDebug "Backup: listingFullFilename = $listingFullFilename" Write-Log $LogDebug "Backup: currentBackupFilename = $currentBackupFilename" Write-Log $LogDebug "Backup: currentBackupFullFilename = $currentBackupFullFilename" Write-Log $LogDebug "Backup: tempBackupFilename = $tempBackupFilename" Write-Log $LogDebug "Backup: tempBackupFullFilename = $tempBackupFullFilename" Write-Log $LogDebug "Backup: currentFull0BackupFullFilename = $currentFull0BackupFullFilename" Write-Log $LogDebug "Backup: currentFull1BackupFullFilename = $currentFull1BackupFullFilename" if (! (Test-Path -path $backupSourceDirectory)) { Write-Log $LogWarning "The backup source directory '$backupSourceDirectory' could not be accessed. Backup not done." return } if (! (Test-Path -path $backupDestinationDirectory)) { mkdir $backupDestinationDirectory } # Limit full backups to $minHoursBetweenFullBackup if (($backupType -eq 'full') -and (Test-Path -path $listingFullFilename)) { $timeSinceLastFullBackup = $(Get-Date) - (Get-Item $currentBackupFullFilename).LastWriteTime if ($timeSinceLastFullBackup.TotalHours -lt $minHoursBetweenFullBackup) { Write-Log $LogMessage 'Skipping backup as not enough time has passed since the last full backup.' return } } if ($contentType -eq "tfs") { # Ping the TFS Server to make sure it is alive. $ping = New-Object System.Net.NetworkInformation.Ping $pingResult = $ping.Send($tfsServer, 2000) if ($pingResult.Status -ne 'Success') { Write-Log $LogWarning "The TFS server '$tfsServer' is not currently available. A ping returned a status of '$($pingResult.Status)'. Backup skipped." return } $tfsChangedFiles = GetTfsChangedFiles $backupSourceDirectory if ($tfsChangedFiles.Length -eq 0) { Write-Log $LogMessage "There are no changed files." return } #Write-Log $LogDebug "Backup: tfsChangedFiles = $tfsChangedFiles" $currentListing = GetFileDescriptionsFromList $tfsChangedFiles #Write-Log $LogDebug "Backup: currentListing = $currentListing" } else { $currentListing = GetFileDescriptions $backupSourceDirectory } Write-Log $LogDebug "Backup: currentListing = $currentListing" # Ensure that the temp file does not exist. if (Test-Path -path $tempBackupFullFilename) { Write-Log $LogWarning "The temporary backup file '$tempBackupFullFilename' was not cleaned up." Remove-Item $tempBackupFullFilename if (Test-Path -path $tempBackupFullFilename) { Write-Log $LogError "Could not remove the temporary backup file '$tempBackupFullFilename'." Exit(-1) } } if (Test-Path -path $listingFullFilename) { $lastListing = ([Io.File]::ReadAllText($listingFullFilename)).TrimEnd() if ($lastListing -eq $currentListing) { # Update only the listing file to indicate that a 'backup check' was done, but leave the actual backup file to indicate # when something was actually last changed. (Get-Item $listingFullFilename).LastWriteTime = Get-Date Write-Log $LogMessage "Nothing has changed since the last backup. A new backup is not required." return } } $currentListing > $listingFullFilename Write-Log $LogMessage "Changes have occurred since the last backup. A new backup is required." if (($backupType -eq "quick") -and ! (Test-Path -path $currentFull0BackupFullFilename)) { Backup $backupName $backupSourceDirectory "full" $contentType $maxBackupCount $maxLevels $levelReduceFactor return } if ($contentType -eq "tfs") { $tfsChangedFiles $listFileName = "$backupDestinationDirectory\listFile.txt" Write-Log $LogDebug "Backup: listFileName = $listFileName" Write-Log $LogDebug "Backup: backupSourceDirectory = $backupSourceDirectory" $tfsChangedFiles.Trim().Replace("$backupSourceDirectory\", '') | Out-File $listFileName -Encoding utf8 $7zBackupSource = "@$listFileName" } else { $7zBackupSource = $backupSourceDirectory } Write-Log $LogDebug "Backup: 7zBackupSource = $7zBackupSource" switch ($backupType) { "quick" { $7zOutput = (& $7zExe u -r $flags $currentFull0BackupFullFilename -u- "-up0q0x2y2z0w2!$tempBackupFullFilename" $7zBackupSource | Out-String -Width 999999) } "full" { $7zOutput = (& $7zExe a -r $flags $tempBackupFullFilename $7zBackupSource | Out-String -Width 999999) } default { Write-Log $LogError "Unknown backup type: $backupType" Exit(-1) } } $7zExitCode = $LastExitCode Write-Log $LogDebug $7zOutput if ($listFileName) { Write-Log $LogDebug "Removing '$listFileName'" Remove-Item $listFileName } if ($7zExitCode -gt 1) { Write-Log $LogError "An error occurred calling 7z. Exit code was $7zExitCode." Exit(-1) } if ($7zExitCode -eq 1) { Write-Log $LogWarning "7z returned 1 (warning - non fatal error)." } if (Is7zFileEmpty($tempBackupFullFilename)) { Write-Log $LogMessage "New backup file is empty - throwing it away." Remove-Item $tempBackupFullFilename return } Write-Log $LogMessage "Backup created successfully." Roll "$backupDestinationDirectory\$backupFilenameMask" $maxBackupCount Move-Item $tempBackupFullFilename $currentBackupFullFilename if (($backupType -eq "full") -and ($remoteBackupDirectory) -and (Test-Path -path $remoteBackupDirectory)) { $remoteBackupFullFilename = "$remoteBackupDirectory\$backupName.7z" Copy-Item $currentBackupFullFilename $remoteBackupFullFilename -Force } if ($backupType -eq "full") { $backupFullMask = "$backupDestinationDirectory\Full_L{0}_Backup_{1:00}.7z" $sourceLevel = 0 BackupHigherFull $backupName $backupFullMask $sourceLevel $maxBackupCount $maxLevels $levelReduceFactor } } function BackupHigherFull ($backupName, $backupFullMask, $sourceLevel, $maxBackupCount, $maxLevel, $levelReduceFactor) { $targetLevel = $sourceLevel + 1 Write-Log $LogHeader "$backupName Full Level $targetLevel" Write-Log $LogDebug "BackupHigherFull: backupName = $backupName" Write-Log $LogDebug "BackupHigherFull: backupFullMask = $backupFullMask" Write-Log $LogDebug "BackupHigherFull: sourceLevel = $sourceLevel" Write-Log $LogDebug "BackupHigherFull: maxBackupCount = $maxBackupCount" Write-Log $LogDebug "BackupHigherFull: maxLevel = $maxLevel" Write-Log $LogDebug "BackupHigherFull: levelReduceFactor = $levelReduceFactor" $currentBackupFullFilename = $backupFullMask -f (([Int]$sourceLevel), 0) $comparatorBackupFullFilename = $backupFullMask -f (([Int]$sourceLevel), ([Int]$levelReduceFactor - 1)) $targetBackupFullFilename = $backupFullMask -f (([Int]$targetLevel), 0) $targetBackupMask = $backupFullMask -f (([Int]$targetLevel), "{0:00}") Write-Log $LogDebug "BackupHigherFull: currentBackupFullFilename = $currentBackupFullFilename" Write-Log $LogDebug "BackupHigherFull: comparatorBackupFullFilename = $comparatorBackupFullFilename" Write-Log $LogDebug "BackupHigherFull: targetBackupFullFilename = $targetBackupFullFilename" Write-Log $LogDebug "BackupHigherFull: targetBackupMask = $targetBackupMask" if (! (Test-Path -path $currentBackupFullFilename)) { Write-Log $LogError "A current level $sourceLevel backup should exist." Exit(-1) } if (! (Test-Path -path $targetBackupFullFilename)) { Write-Log $LogMessage "Saving as there is no existing level $targetLevel backup." Copy-Item $currentBackupFullFilename $targetBackupFullFilename return } if (! (Test-Path -path $comparatorBackupFullFilename)) { Write-Log $LogMessage "Skipping as there is no comparator level $sourceLevel backup yet." return } if ((Get-Item $comparatorBackupFullFilename).LastWriteTime -le (Get-Item $targetBackupFullFilename).LastWriteTime) { Write-Log $LogMessage "Skipping as the current level $targetLevel backup does not require replacement yet." return } Write-Log $LogMessage "Creating a level $targetLevel backup." Roll $targetBackupMask $maxBackupCount Copy-Item $currentBackupFullFilename $targetBackupFullFilename if ($targetLevel -lt $maxLevel) { BackupHigherFull $backupName $backupFullMask $targetLevel $maxBackupCount $maxLevel $levelReduceFactor } } function BackupAll ($backupType) { # NOTE: When type = tfs, do not put a trailing slash on backupSourceDirectory. Write-Log $LogHeader "---- Backup All $backupType ---- Start ----" Backup 'Dev' 'D:\Dev' $backupType 'tfs' 20 2 6 Backup 'Bin' 'C:\Bin\*' $backupType 'all' 20 2 6 Backup 'VS2010Settings' 'C:\Users\stephenh\Documents\Visual Studio 2010\*' $backupType 'all' 20 2 6 Backup 'VS2012Settings' 'C:\Users\stephenh\Documents\Visual Studio 2012\*' $backupType 'all' 20 2 6 Backup 'BigPrimes' 'C:\Not so temp\BigPrimes' $backupType 'all' 20 2 6 Backup 'Desktop' 'C:\Users\stephenh\Desktop\*' $backupType 'all' 20 2 6 CopyBinToFlicky if ($backupType -eq 'full') { SizeLimitLogFiles } Write-Log $LogHeader "---- Backup All $backupType ---- Done ----" } #### Oddball Backups ##### function CopyBinToFlicky () { Write-Log $LogHeader '---- Copy Bin To Flicky ---- Start ----' $fSyncExe = 'C:\Bin\Fsync\Fsync.exe' $localBinDir = 'C:\Bin' $flickyBinDir = 'C:\External Drives\Flicky\Dropbox\Bins\Provoke\Bin' if (! (Test-Path -path $flickyBinDir )) { Write-Log $LogMessage "Flicky is not plugged in. Copy skipped." return } $fSyncOutput = (& $fSyncExe $localBinDir $flickyBinDir | Out-String -Width 999999) Write-Log $LogDebug $fSyncOutput Write-Log $LogHeader '---- Copy Bin To Flicky ---- Done ----' } ########################### #CopyBinToFlicky #exit(0) #$DisplayLevel = 4; #$backupType = 'test' switch ($backupType) { 'quick' { BackupAll 'quick' } 'full' { BackupAll 'full' } 'test' { Backup 'Desktop' 'C:\Users\stephenh\Desktop\*' 'full' 'all' 20 2 6 } 'devtest' { Backup 'Dev' 'D:\Dev' 'quick' 'tfs' 20 2 6 } 'tfstest' { GetTfsChangedFiles 'D:\Dev' } default { Write-Host 'You are an idiot.' } } #Write-Host "Press any key to exit." #$dontcare = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")