├── Add-BackupEntry.ps1 ├── Archive-Oldest.ps1 ├── BuildList.ps1 ├── DailyIncrementalBackup.ps1 ├── Exclude.txt ├── Get-myBackupFile.ps1 ├── Invoke-FullBackup.ps1 ├── LICENSE ├── LogBackupEntry.ps1 ├── MonitorDailyWatcher.ps1 ├── PSRar.psm1 ├── README.md ├── RarBackup.ps1 ├── UpdateBackupPending.ps1 ├── WeeklyFullBackup.ps1 ├── assets └── db.png ├── automation-functions ├── Create-IncrementalBackupJob.ps1 ├── Create-WatcherScheduledJob.ps1 └── Create-WeeklyFullBackupJobs.ps1 ├── changelog.md ├── formats ├── mybackupfile.format.ps1xml └── pending.format.ps1xml ├── loadformats.ps1 ├── myBackupPaths.txt ├── myBackupPending.ps1 ├── myBackupPendingSummary.ps1 ├── myBackupReport.ps1 └── myBackupTrim.ps1 /Add-BackupEntry.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | 3 | Function Add-BackupEntry { 4 | 5 | <# PSFunctionInfo 6 | 7 | Version 1.4.1 8 | Author Jeffery Hicks 9 | CompanyName JDH IT Solutions, Inc. 10 | Copyright (c) 2020-2021 JDH IT Solutions, Inc. 11 | Description Add an entry to one of my backup CSV files 12 | Guid a6c22223-4f16-4ad2-a3ff-b27e374ce52a 13 | Tags profile,backup 14 | LastUpdate 1/24/2021 4:29 PM 15 | Source C:\scripts\PSBackup\Add-BackupEntry.ps1 16 | 17 | #> 18 | [CmdletBinding(SupportsShouldProcess)] 19 | [Alias("abe")] 20 | [OutputType("none")] 21 | Param( 22 | [Parameter(Position = 1, Mandatory, ValueFromPipeline)] 23 | [ValidateScript({Test-Path $_})] 24 | [String]$Path, 25 | 26 | [Parameter(Position = 0,Mandatory)] 27 | #I'm using a dynamic argument completer instead of the old validate set 28 | #[ValidateSet("Scripts","Dropbox","Documents","GoogleDrive","jdhit")] 29 | [ArgumentCompleter({(Get-ChildItem D:\backup\*.csv).name.foreach({($_ -split "-")[0]})})] 30 | [alias("set")] 31 | [ValidateNotNullOrEmpty()] 32 | [String]$BackupSet 33 | ) 34 | 35 | Begin { 36 | Write-Verbose "[BEGIN ] Starting: $($MyInvocation.MyCommand)" 37 | $csvFile = "D:\Backup\$BackupSet-log.csv" 38 | $add = @() 39 | } #begin 40 | 41 | Process { 42 | $cPath = Convert-Path $path 43 | Write-Verbose "[PROCESS] Adding: $cPath" 44 | 45 | $file = Get-Item $cPath 46 | #the Google Drive path is different than the BackupSet name 47 | if ($BackupSet -eq 'GoogleDrive') { 48 | $BackupSet = "Google Drive" 49 | } 50 | $add += [PSCustomObject]@{ 51 | ID = 99999 52 | Date = $file.LastWriteTime 53 | Name = ($file.FullName -split "$BackupSet\\")[1] 54 | IsFolder = "False" 55 | Directory = $file.DirectoryName 56 | Size = $file.length 57 | Path = $file.FullName 58 | } 59 | } #process 60 | End { 61 | if ($add.count -gt 0) { 62 | Write-Verbose "[END ] Exporting $($add.count) entries to $CSVFile" 63 | $add | Out-String | Write-Verbose 64 | $add | Export-Csv -Path $CSVFile -Append -NoTypeInformation 65 | } 66 | Write-Verbose "[END ] Ending: $($MyInvocation.MyCommand)" 67 | } #end 68 | } 69 | 70 | <# 71 | ID : 13669) 72 | Date : 11/5/2019 5:02:32 PM 73 | Name : ps-regex\Module-4 74 | IsFolder : True 75 | Directory : 76 | Size : 1 77 | Path : C:\Scripts\ps-regex\Module-4 78 | #> 79 | -------------------------------------------------------------------------------- /Archive-Oldest.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | 3 | <# 4 | rename the oldest backup file for long term archiving 5 | run this every quarter to rename a file like 20201023_Training-FULL.rar 6 | to ARCHIVE_Training.rar 7 | #> 8 | 9 | [CmdletBinding(SupportsShouldProcess)] 10 | Param( 11 | [Parameter(Position = 0, HelpMessage = "Specify the backup path")] 12 | [ValidateScript({Test-Path $_})] 13 | [String]$Path = "\\DSTulipwood\backup", 14 | [Parameter(HelpMessage = "Specify the filename prefix")] 15 | [String]$Prefix = "ARCHIVE" 16 | ) 17 | 18 | #parse out backup set 19 | [regex]$rx = "(?<=_)\w+(?=\-)" 20 | 21 | Write-Verbose "Getting FULL backup rar files from $path" 22 | $files = Get-ChildItem -Path "$path\*Full.rar" 23 | 24 | if ($files) { 25 | Write-Verbose "Found $($files.count) file[s]" 26 | 27 | #get groups where there is more than 2 backup 28 | $Groups = $files | Group-Object { $rx.match($_.basename) } | Where-Object { $_.count -gt 2 } 29 | 30 | foreach ($item in $groups) { 31 | Write-Verbose "Processing backup set $($item.name)" 32 | $new = "{0}_{1}.rar" -f $prefix,$item.name 33 | Write-Verbose "Renaming oldest file to $new" 34 | 35 | #delete a previous file if found 36 | $target =(Join-Path -Path $path -ChildPath $new) 37 | if (Test-Path $target) { 38 | Write-Verbose "Removing previous file $target" 39 | Remove-Item $target 40 | } 41 | #rename the oldest file in the group 42 | $item.Group | Sort-Object -Property LastWriteTime | Select-Object -First 1 | 43 | Get-Item | Rename-Item -NewName $new -PassThru 44 | } 45 | } 46 | else { 47 | Write-Warning "No matching files found in $Path." 48 | } 49 | -------------------------------------------------------------------------------- /BuildList.ps1: -------------------------------------------------------------------------------- 1 | #build the full backup list 2 | #this script is intended to be used on my primary home desktop 3 | 4 | [CmdletBinding()] 5 | Param( 6 | [Parameter(Position = 0, HelpMessage = "Path to a text file with folders to backup.")] 7 | [ValidateNotNullOrEmpty()] 8 | [ValidateScript( { Test-Path $_ })] 9 | [String]$PathList = "c:\scripts\PSBackup\myBackupPaths.txt", 10 | 11 | [Parameter(Position = 1, HelpMessage = "The destination folder for the backup files")] 12 | [ValidateNotNullOrEmpty()] 13 | [String]$Destination = "\\DSTulipwood\backup" 14 | ) 15 | 16 | #regex to match on set name from RAR file 17 | [regex]$rx = "(?<=_)\w+(?=\-)" 18 | 19 | #8/3/2024 Explicitly set this to an array of strings otherwise names are appended to a single string 20 | [string[]]$sets = Get-ChildItem $Destination\*incremental.rar | 21 | Select-Object -Property @{Name = "Set"; Expression = { $rx.Match($_.name).Value } } | 22 | Select-Object -expand Set | Select-Object -Unique 23 | 24 | If ($Sets.Count -eq 0) { 25 | Write-Verbose "No incremental backups found" 26 | } 27 | else { 28 | Write-Verbose "Found incremental backups for $($sets -join ',') " 29 | } 30 | #get set name from pending log that may not have been backed up yet 31 | 32 | Write-Verbose "Checking pending backups" 33 | 34 | $csv = Get-ChildItem D:\Backup\*.csv | ForEach-Object { $_.BaseName.split("-")[0]} 35 | foreach ($item in $csv) { 36 | if ($sets -NotContains $item) { 37 | Write-Verbose "Adding $item to backup set" 38 | $sets+=$item 39 | } 40 | } 41 | 42 | #build the list 43 | foreach ($set in $sets) { 44 | Write-Verbose "Searching for $set path" 45 | #match on the first 4 characters of the set name 46 | $mtch = $set.substring(0,4) 47 | Write-Verbose "Searching for path match on $mtch" 48 | $Path = (Get-Content C:\scripts\PSBackup\myBackupPaths.txt | Select-String $mtch).line 49 | Write-Verbose "Backing up $Path" 50 | $Path 51 | } 52 | -------------------------------------------------------------------------------- /DailyIncrementalBackup.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | #requires -module BurntToast,PSScriptTools 3 | 4 | #backup files from CSV change logs 5 | #the CSV files should already follow path exclusions in Exclude.txt 6 | 7 | <# 8 | this is another way to get last version of each file instead of using Group-Object 9 | 10 | foreach ($name in (Import-CSV D:\Backup\Scripts-log.csv -OutVariable in | Select-Object Name -Unique).name) { 11 | $in | where {$_.name -EQ $name} | sort-object -Property ID | Select-Object -last 1 12 | } 13 | #> 14 | 15 | [CmdletBinding(SupportsShouldProcess)] 16 | Param( 17 | [Parameter(HelpMessage = 'Specify the location of the CSV files with incremental backup changes.')] 18 | [ValidateScript({ Test-Path $_ })] 19 | [String]$BackupPath = 'D:\Backup' 20 | ) 21 | #create a transcript log file 22 | $log = New-CustomFileName -Template 'DailyIncremental_%year%month%day%hour%minute.txt' 23 | #11/6/2023 Changed Backup log path so the files don't get removed on reboot 24 | $LogPath = Join-Path -Path D:\backupLogs -ChildPath $log 25 | $codeDir = 'C:\scripts\PSBackup' 26 | Start-Transcript -Path $LogPath 27 | 28 | #refresh NAS Credential 29 | cmdkey /add:DSTulipwood /user:Jeff /pass:(Get-Content C:\scripts\tulipwood.txt | Unprotect-CmsMessage) 30 | 31 | Write-Host "[$(Get-Date)] Starting Daily Incremental" -ForegroundColor Cyan 32 | 33 | #this is my internal archiving code. You can use whatever you want. 34 | Try { 35 | Import-Module $codeDir\PSRar.psm1 -Force -ErrorAction Stop 36 | } 37 | Catch { 38 | Write-Warning "Failed to import PSRar module at $codeDir\PSRar.psm1." 39 | #bail out if the module fails to load 40 | return 41 | } 42 | 43 | #get the CSV files 44 | $paths = (Get-ChildItem -Path "$BackupPath\*.csv").FullName 45 | 46 | foreach ($path in $paths) { 47 | $name = (Split-Path -Path $Path -Leaf).split('-')[0] 48 | $files = Import-Csv -Path $path | 49 | Where-Object { ($_.name -notmatch '~|\.tmp') -AND ($_.size -gt 0) -AND ($_.IsFolder -eq 'False') -AND (Test-Path $_.path) } | 50 | Select-Object -Property path, size, directory, IsFolder, ID | Group-Object -Property Path 51 | 52 | $tmpParent = Join-Path -Path D:\BackTemp -ChildPath $name 53 | 54 | foreach ($file in $files) { 55 | $ParentFolder = $file.group[0].directory 56 | #Create a temporary folder for backing up the day's files 57 | $RelPath = Join-Path -Path $tmpParent -ChildPath $ParentFolder.Substring(3) 58 | 59 | if (-Not (Test-Path -Path $RelPath)) { 60 | Write-Host "[$(Get-Date)] Creating $RelPath" -ForegroundColor cyan 61 | $new = New-Item -Path $RelPath -ItemType directory -Force 62 | Start-Sleep -Milliseconds 100 63 | 64 | #copy hidden attributes 65 | $attrib = (Get-Item $ParentFolder -Force).Attributes 66 | if ($attrib -match 'hidden') { 67 | Write-Host "[$(Get-Date)] Copying attributes from $ParentFolder to $($new.FullName)" -ForegroundColor yellow 68 | Write-Host $attrib -ForegroundColor yellow 69 | (Get-Item $new.FullName -Force).Attributes = $attrib 70 | } 71 | } 72 | Write-Host "[$(Get-Date)] Copying $($file.name) to $RelPath" -ForegroundColor green 73 | $f = Copy-Item -Path $file.Name -Destination $RelPath -Force -PassThru 74 | #copy attributes 75 | if ($PSCmdlet.ShouldProcess($f.name, 'Copy Attributes')) { 76 | $f.Attributes = (Get-Item $file.name -Force).Attributes 77 | } 78 | } #foreach file 79 | 80 | #create a RAR archive or substitute your archiving code 81 | $archive = Join-Path -Path D:\BackTemp -ChildPath "$(Get-Date -Format yyyyMMdd)_$name-INCREMENTAL.rar" 82 | #get some stats about the data to be archived 83 | $stats = Get-ChildItem -Path $tmpParent -File -Recurse | Measure-Object -Property length -Sum 84 | Write-Host "[$(Get-Date)] Creating $archive from $tmpParent" -fore green 85 | Write-Host "[$(Get-Date)] $($stats.count) files totaling $($stats.sum)" -fore green 86 | 87 | # for debugging 88 | #Pause 89 | 90 | $addParams = @{ 91 | Object = $tmpParent 92 | Archive = $archive 93 | Comment = "Incremental backup $(Get-Date)" 94 | excludeFile = 'C:\scripts\PSBackup\exclude.txt' 95 | verbose = $True 96 | } 97 | Add-RARContent @addParams 98 | 99 | Write-Host "[$(Get-Date)] Moving $archive to NAS" -fore green 100 | if ($PSCmdlet.ShouldProcess($archive, 'Move file')) { 101 | if (Test-Path \\DSTulipwood\backup) { 102 | Try { 103 | Move-Item -Path $archive -Destination \\DSTulipwood\backup -Force -ErrorAction Stop 104 | #only remove the file if it was successfully moved to the NAS 105 | Write-Host "[$(Get-Date)] Removing $path" -fore yellow 106 | if ($PSCmdlet.ShouldProcess($path, 'Remove file')) { 107 | Remove-Item $path 108 | } 109 | } 110 | Catch { 111 | Write-Warning "Failed to move $archive to \\DSTulipwood\Backup. $($_.Exception.Message)" 112 | } 113 | } 114 | else { 115 | #failed to connect to NAS 116 | Write-Host "[$(Get-Date)] Failed to verify \\DSTulipwood\Backup" -fore red 117 | Write-Verbose 'Failed to verify \\DSTulipwood\Backup' 118 | } 119 | } #whatIf 120 | 121 | } #foreach path 122 | 123 | Write-Host "[$(Get-Date)] Removing temporary Backup folders" -fore yellow 124 | Get-ChildItem -Path D:\BackTemp -Directory | Remove-Item -Force -Recurse 125 | 126 | $NewFiles = Get-ChildItem -Path \\DSTulipwood\backup\*incremental.rar | 127 | Where-Object LastWriteTime -GE (Get-Date).Date 128 | 129 | #send a toast notification 130 | $btText = @" 131 | Backup Task Complete 132 | 133 | Created $($NewFiles.count) files. 134 | View log at $LogPath 135 | "@ 136 | 137 | #$NewFiles | ForEach-Object { $btText+= "$($_.name)`n"} 138 | 139 | $params = @{ 140 | Text = $btText 141 | Header = $(New-BTHeader -Id 1 -Title 'Daily Incremental Backup') 142 | AppLogo = 'c:\scripts\db.png' 143 | } 144 | 145 | if ($PSCmdlet.ShouldProcess($LogPath, 'Send Toast Notification')) { 146 | New-BurntToastNotification @params 147 | } 148 | 149 | Write-Host "[$(Get-Date)] Ending Daily Incremental" -ForegroundColor cyan 150 | Stop-Transcript 151 | -------------------------------------------------------------------------------- /Exclude.txt: -------------------------------------------------------------------------------- 1 | #enter paths of files and directories to exclude 2 | C:\Users\Jeff\Documents\PowerShell\Help\* 3 | C:\Users\Jeff\Documents\Powershell\modules\* 4 | C:\users\jeff\documents\windowsPowershell\modules\* 5 | C:\scripts\testing\* 6 | c:\users\jeff\dropbox\dropbox.ini 7 | C:\users\jeff\dropbox\remoteop\* 8 | C:\users\jeff\dropbox\remoteop7\* 9 | c:\users\jeff\dropbox\mycomics\* 10 | C:\Users\Jeff\Documents\MuseScore3\SoundFonts\* 11 | C:\Users\Jeff\Documents\MuseScore4\SoundFonts\* 12 | C:\Users\Jeff\Google Drive\junk\* 13 | C:\users\jeff\documents\Outlook Files\* 14 | D:\OneDrive\Scores4\*.mscz~ 15 | -------------------------------------------------------------------------------- /Get-myBackupFile.ps1: -------------------------------------------------------------------------------- 1 | Function Get-MyBackupFile { 2 | 3 | <# PSFunctionInfo 4 | 5 | Version 1.1.0 6 | Author Jeffery Hicks 7 | CompanyName JDH IT Solutions, Inc. 8 | Copyright (c) 2021-2023 JDH IT Solutions, Inc. 9 | Description Get my backup files. 10 | Guid 74dc584d-a1e1-43e5-a5c9-b494d1971f8d 11 | Tags profile,backup 12 | LastUpdate 11/8/2023 8:38 AM 13 | Source C:\scripts\PSBackup\Get-MyBackupFile.ps1 14 | 15 | #> 16 | 17 | [CmdletBinding(DefaultParameterSetName = "default")] 18 | [alias("gbf")] 19 | [OutputType("myBackupFile")] 20 | 21 | Param( 22 | [Parameter(Position = 0, HelpMessage = "Enter the path where the backup files are stored.", ParameterSetName = "default")] 23 | [ValidateNotNullOrEmpty()] 24 | [ValidateScript( { Test-Path $_ })] 25 | #This is my NAS device 26 | [String]$Path = "\\DSTulipwood\backup", 27 | [Parameter(HelpMessage = "Get only Incremental backup files")] 28 | [Switch]$IncrementalOnly, 29 | [Parameter(HelpMessage = "Get the last X number of raw files")] 30 | [ValidateScript({$_ -ge 1})] 31 | [Int]$Last, 32 | [Parameter(HelpMessage = "Get files created yesterday")] 33 | [Switch]$Yesterday 34 | ) 35 | 36 | #convert path to a full filesystem path 37 | $Path = Convert-Path $path 38 | Write-Verbose "Starting $($MyInvocation.MyCommand)$ReportVer" 39 | Write-Verbose "Using parameter set $($PSCmdlet.ParameterSetName)" 40 | 41 | [regex]$rx = "^20\d{6}_(?\w+)-(?\w+)\.((rar)|(zip))$" 42 | 43 | <# 44 | I am doing some 'pre-filtering' on the file extension and then using the regular 45 | expression filter to fine tune the results 46 | #> 47 | Write-Verbose "Getting zip and rar files from $Path" 48 | $all = Get-ChildItem $Path\*.zip, $Path\*.rar 49 | if ($IncrementalOnly) { 50 | $files = $all.Where({ $_.name -match "Incremental" }) 51 | } 52 | else { 53 | $files = $all.where({ $rx.IsMatch($_.name)}) 54 | } 55 | 56 | if ($files.count -eq 0) { 57 | Write-Warning "No backup files found in $Path" 58 | return 59 | } 60 | 61 | Write-Verbose "Found $($files.count) matching files in $Path" 62 | foreach ($item in $files) { 63 | $BackupSet = $rx.matches($item.name).groups[4].value 64 | $SetType = $rx.matches($item.name).groups[5].value 65 | 66 | #add some custom properties to be used with formatted results based on named captures 67 | $item | Add-Member -MemberType NoteProperty -Name BackupSet -Value $BackupSet -force 68 | $item | Add-Member -MemberType NoteProperty -Name SetType -Value $SetType -force 69 | #insert a custom type name 70 | $item.PSObject.TypeNames.Insert(0,"myBackupFile") 71 | } 72 | 73 | if ($Yesterday) { 74 | $yd = (Get-Date).AddDays(-1).Date 75 | $files = $files | Where-Object {$_.LastWriteTime -ge $yd} 76 | } 77 | if ($Last -gt 0) { 78 | $files | Sort-Object Created | Select-Object -Last $last 79 | } 80 | else { 81 | $files | Sort-Object BackupSet,Created 82 | } 83 | } #end Get-MyBackupFile 84 | 85 | #define some alias properties for the custom object type 86 | Update-TypeData -TypeName "myBackupFile" -MemberType AliasProperty -memberName Size -Value Length -force 87 | Update-TypeData -TypeName "myBackupFile" -MemberType AliasProperty -memberName Created -Value CreationTime -force 88 | 89 | #load the custom format file 90 | Update-FormatData $PSScriptRoot\formats\myBackupFile.format.ps1xml 91 | -------------------------------------------------------------------------------- /Invoke-FullBackup.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | #requires -module BurntToast,PSScriptTools 3 | 4 | [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = "list")] 5 | Param( 6 | [Parameter(Position = 0, HelpMessage = "Path to a text file with folders to backup.", ParameterSetName = "List")] 7 | [ValidateNotNullOrEmpty()] 8 | [ValidateScript( { Test-Path $_ })] 9 | [String]$PathList = "c:\scripts\PSBackup\mybackupPaths.txt", 10 | 11 | [Parameter(Position = 0, HelpMessage = "Specify a single folder to backup", ParameterSetName = "Single")] 12 | [ValidateScript( { Test-Path $_ })] 13 | [String]$Path, 14 | 15 | [Parameter(Position = 0, HelpMessage = "The destination folder for the backup files")] 16 | [ValidateNotNullOrEmpty()] 17 | [String]$Destination = "\\DSTulipwood\backup" 18 | ) 19 | 20 | <# 21 | I am hardcoding the path to scripts because when I 22 | run this as a scheduled job, there is no $PSScriptRoot or $MyInvocation 23 | #> 24 | #create a transcript log file 25 | $log = New-CustomFileName -Template "WeeklyFull_%year%month%day%hour%minute%seconds-%###.txt" 26 | #11/6/2023 Changed Backup log path so the files don't get removed on reboot 27 | #8/17/2024 moved backup logs to OneDrive 28 | $LogPath = Join-Path -Path $env:OneDrive\backuplogs -ChildPath $log 29 | Start-Transcript -Path $LogPath 30 | 31 | #refresh NAS Credential 32 | if ($Destination -match "DSTulipwood") { 33 | Write-Host "[$(Get-Date)] Refreshing Tulipwood Credential" -ForegroundColor yellow 34 | cmdkey /add:DSTulipwood /user:Jeff /pass:(Get-Content C:\scripts\tulipwood.txt | Unprotect-CmsMessage) 35 | } 36 | $codeDir = "C:\scripts\PSBackup" 37 | 38 | Write-Host "[$(Get-Date)] Starting Weekly Full Backup" -ForegroundColor green 39 | Write-Host "[$(Get-Date)] Setting location to $codeDir" -ForegroundColor yellow 40 | Set-Location $CodeDir 41 | 42 | #import my custom module 43 | Try { 44 | Import-Module $codeDir\PSRar.psm1 -Force -ErrorAction Stop 45 | } 46 | Catch { 47 | Write-Warning "Failed to import PSRar module at $codeDir\PSRar.psm1." 48 | #bail out if the module fails to load 49 | return 50 | } 51 | 52 | If ($PSCmdlet.ParameterSetName -eq "list") { 53 | Write-Host "[$(Get-Date)] Getting backup paths" -ForegroundColor yellow 54 | #filter out blanks and commented lines 55 | $paths = Get-Content $PathList | Where-Object { $_ -match "(^[^#]\S*)" -and $_ -notmatch "^\s+$" } 56 | } #if mybackup paths file 57 | elseif ($PSCmdlet.ParameterSetName -eq 'single') { 58 | $paths = $Path 59 | } 60 | 61 | $paths | ForEach-Object { 62 | if ($PSCmdlet.ShouldProcess($_)) { 63 | Try { 64 | #invoke a control script using my custom module 65 | Write-Host "[$(Get-Date)] Backing up $_" -ForegroundColor yellow 66 | #this is my wrapper script using WinRar to create the archive. 67 | #you can use whatever tool you want in its place. 68 | &"$CodeDir\RarBackup.ps1" -Path $_ -ErrorAction Stop -Verbose 69 | $ok = $True 70 | } 71 | Catch { 72 | $ok = $False 73 | Write-Warning $_.exception.message 74 | } 75 | } #what if 76 | 77 | #clear corresponding incremental log files 78 | $name = ((Split-Path $_ -Leaf).replace(' ', '')) 79 | #specify the directory for the CSV log files 80 | $log = "D:\Backup\{0}-log.csv" -f $name 81 | 82 | if ($OK -AND (Test-Path $log) -AND ($PSCmdlet.ShouldProcess($log, "Clear Log"))) { 83 | Write-Host "[$(Get-Date)] Removing $log" -ForegroundColor yellow 84 | Remove-Item -Path $log 85 | } #WhatIf 86 | 87 | #clear incremental backups 88 | $target = Join-Path -Path $Destination -ChildPath "*_$name-incremental.rar" 89 | if ($ok -AND ($PSCmdlet.ShouldProcess($target, "Clear Incremental BackUps"))) { 90 | Write-Host "[$(Get-Date)] Removing $Target" -ForegroundColor yellow 91 | Remove-Item $target 92 | } #WhatIf 93 | 94 | #trim old backups 95 | Write-Host "[$(Get-Date)] Trimming backups from $Destination" -ForegroundColor yellow 96 | if ($OK -and ($PSCmdlet.ShouldProcess($Destination, "Trim backups"))) { 97 | &"$CodeDir\mybackuptrim.ps1" -path $Destination -count 2 98 | } #WhatIf 99 | 100 | #I am also backing up a smaller subset to OneDrive 101 | Write-Host "[$(Get-Date)] Trimming backups from $env:OneDriveConsumer\backup" -ForegroundColor yellow 102 | if ($OK -and ($PSCmdlet.ShouldProcess("OneDrive", "Trim backups"))) { 103 | &"$CodeDir\mybackuptrim.ps1" -path $env:OneDriveConsumer\backup -count 1 104 | } 105 | } #foreach path 106 | 107 | #send a toast notification 108 | $params = @{ 109 | Text = "Backup Task Complete. View log at $LogPath" 110 | Header = $(New-BTHeader -Id 1 -Title "Weekly Full Backup") 111 | AppLogo = "c:\scripts\db.png" 112 | } 113 | 114 | Write-Host "[$(Get-Date)] Ending Weekly Full Backup" -ForegroundColor green 115 | 116 | #don't run if using -WhatIf 117 | if (-Not $WhatIfPreference) { 118 | New-BurntToastNotification @params 119 | Stop-Transcript 120 | } 121 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020-2024 JDH Information Technology Solutions, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LogBackupEntry.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | 3 | [CmdletBinding()] 4 | [alias("lbe")] 5 | Param( 6 | [Parameter(Mandatory)] 7 | [ValidateNotNullOrEmpty()] 8 | [object]$Event, 9 | 10 | [Parameter(Mandatory)] 11 | [ValidateNotNullOrEmpty()] 12 | [String]$CSVPath, 13 | 14 | [Parameter(HelpMessage = "Specify the path to a text file with a list of paths to exclude from backup")] 15 | [ValidateScript( { Test-Path $_ })] 16 | [String]$ExclusionList = "c:\scripts\PSBackup\Exclude.txt" 17 | ) 18 | 19 | if (Test-Path $ExclusionList) { 20 | $excludes = Get-Content -Path $ExclusionList | Where-Object { $_ -match "\w+" -AND $_ -notmatch "^#" } 21 | } 22 | 23 | #log activity. Comment out the line to disable logging 24 | $LogFile = "D:\temp\watcherlog.txt" 25 | 26 | #if log is enabled and it is over 10MB in size, archive the file and start a new file. 27 | $ChkFile = Get-Item -Path $LogFile -ErrorAction SilentlyContinue 28 | if ($ChkFile.length -ge 10MB) { 29 | $ChkFile | Copy-Item -Destination d:\temp\archive-watcherlog.txt -Force 30 | Remove-Item -Path $LogFile 31 | } 32 | 33 | #uncomment for debugging and testing 34 | # this will create a serialized version of each fired event 35 | # $event | Export-Clixml ([System.IO.Path]::GetTempFileName()).replace("tmp","xml") 36 | 37 | if ($LogFile) { "$(Get-Date) LogBackupEntry fired" | Out-File -FilePath $LogFile -Append } 38 | if ($LogFile) { "$(Get-Date) Verifying path $($event.SourceEventArgs.FullPath)" | Out-File -FilePath $LogFile -Append } 39 | if (Test-Path $event.SourceEventArgs.FullPath) { 40 | $f = Get-Item -Path $event.SourceEventArgs.FullPath -Force 41 | if ($LogFile) { "$(Get-Date) Detected $($f.FullName)" | Out-File -FilePath $LogFile -Append } 42 | 43 | #test if the path is in the excluded list and skip the file 44 | $test = $excludes | where {$f -like "$($_)*"} 45 | <# $test = @() 46 | foreach ($x in $excludes) { 47 | $test += $f.directory -like "$($x)*" 48 | } #> 49 | #if ($test -NotContains $True) { 50 | if (-Not $test) { 51 | $OKBackup = $True 52 | } 53 | else { 54 | if ($LogFile) { "$(Get-Date) EXCLUDED ENTRY $($f.FullName) LIKE $Test" | Out-File -FilePath $LogFile -Append } 55 | $OKBackup = $false 56 | } 57 | if ($OKBackup) { 58 | #get current contents of CSV file and only add the file if it doesn't exist 59 | If (Test-Path $CSVPath) { 60 | $in = (Import-Csv -Path $CSVPath).path | Get-Unique -AsString 61 | if ($in -contains $f.FullName) { 62 | if ($LogFile) { "$(Get-Date) DUPLICATE ENTRY $($f.FullName)" | Out-File -FilePath $LogFile -Append } 63 | } 64 | } 65 | #only save files and not a temp file 66 | if (($in -NotContains $f.FullName) -AND (-Not $f.PSIsContainer) -AND ($f.basename -notmatch "(^(~|__rar).*)|(.*\.tmp$)")) { 67 | 68 | if ($LogFile) { "$(Get-Date) Saving to $CSVPath" | Out-File -FilePath $LogFile -Append } 69 | 70 | #write the object to the CSV file 71 | [PSCustomObject]@{ 72 | ID = $event.EventIdentifier 73 | Date = $event.timeGenerated 74 | Name = $event.sourceEventArgs.Name 75 | IsFolder = $f.PSIsContainer 76 | Directory = $f.DirectoryName 77 | Size = $f.length 78 | Path = $event.sourceEventArgs.FullPath 79 | } | Export-Csv -NoTypeInformation -Path $CSVPath -Append 80 | 81 | } #if not a container and not a temp file 82 | } #if OKBackup 83 | } #if test-path 84 | 85 | if ($LogFile) { "$(Get-Date) Ending LogBackupEntry.ps1" | Out-File -FilePath $LogFile -Append } 86 | 87 | #end of script 88 | -------------------------------------------------------------------------------- /MonitorDailyWatcher.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | #requires -module CimCmdlets,BurntToast 3 | 4 | [CmdletBinding(SupportsShouldProcess)] 5 | Param() 6 | 7 | Write-Verbose "Starting $($MyInvocation.MyCommand)" 8 | #verify the scheduled task exists and bail out if it doesn't. 9 | $name = 'DailyWatcher' 10 | 11 | Try { 12 | Write-Verbose "Getting scheduled task $Name" 13 | $task = Get-ScheduledTask -TaskName $Name -ErrorAction Stop 14 | } 15 | catch { 16 | Write-Verbose "Scheduled task $Name not found. Aborting." 17 | Throw $_ 18 | #make sure we bail out 19 | return 20 | } 21 | 22 | #if by chance the task is not running, go ahead and start it. 23 | if ($task.State -ne 'running') { 24 | Write-Verbose "Starting scheduled task $Name" 25 | if ($PSCmdlet.ShouldProcess($Name, 'Start-ScheduledTask')) { 26 | 27 | $task | Start-ScheduledTask 28 | 29 | #send a toast notification 30 | $params = @{ 31 | Text = "Starting scheduled task $($task.TaskName)" 32 | Header = $(New-BTHeader -Id 1 -Title 'Daily Watcher') 33 | AppLogo = 'c:\scripts\db.png' 34 | } 35 | Write-Verbose 'Sending Burnt Toast notification' 36 | New-BurntToastNotification @params 37 | 38 | } #if should process 39 | } 40 | 41 | #register an event subscriber if one doesn't already exist 42 | Try { 43 | Write-Verbose 'Testing for an existing event subscriber' 44 | Get-EventSubscriber -SourceIdentifier TaskChange -ErrorAction Stop 45 | Write-Verbose 'An event subscriber has been detected. No further action is required.' 46 | } 47 | Catch { 48 | 49 | <# 50 | the scheduled task object is of this CIM type 51 | Microsoft.Management.Infrastructure.CimInstance#Root/Microsoft/Windows/TaskScheduler/MSFT_ScheduledTask 52 | #> 53 | Write-Verbose 'Registering a new CimIndicationEvent' 54 | 55 | $query = "Select * from __InstanceModificationEvent WITHIN 10 WHERE TargetInstance ISA 'MSFT_ScheduledTask' AND TargetInstance.TaskName='$Name'" 56 | $NS = 'Root\Microsoft\Windows\TaskScheduler' 57 | 58 | #define a scriptblock to execute if the event fires 59 | $Action = { 60 | $previous = $Event.SourceEventArgs.NewEvent.PreviousInstance 61 | $current = $Event.SourceEventArgs.NewEvent.TargetInstance 62 | if ($previous.state -eq 'Running' -AND $current.state -ne 'Running') { 63 | Write-Host "[$(Get-Date)] Restarting the DailyWatcher task" -ForegroundColor green 64 | Get-ScheduledTask -TaskName DailyWatcher | Start-ScheduledTask 65 | } 66 | } 67 | 68 | $regParams = @{ 69 | SourceIdentifier = 'TaskChange' 70 | Namespace = $NS 71 | query = $query 72 | MessageData = "The task $Name has changed" 73 | MaxTriggerCount = 7 74 | Action = $action 75 | } 76 | 77 | $regParams | Out-String | Write-Verbose 78 | 79 | if ($PSCmdlet.ShouldProcess($regParams.SourceIdentifier, 'Register-CimIndicationEvent')) { 80 | Register-CimIndicationEvent @regParams 81 | } 82 | } 83 | 84 | Write-Verbose "Ending $($MyInvocation.MyCommand)" 85 | -------------------------------------------------------------------------------- /PSRar.psm1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | 3 | <# 4 | These are wrapper functions for RAR.EXE, which is the command line 5 | version of WinRAR. 6 | 7 | https://www.rarlab.com/ 8 | using v5.71 9 | 10 | This is an old module that hasn't been updated in years. 11 | #> 12 | 13 | Function Get-RARExe { 14 | [cmdletbinding()] 15 | Param() 16 | 17 | #get winrar folder 18 | $open = Get-ItemProperty -Path HKLM:\SOFTWARE\Classes\WinRar\Shell\Open\Command | Select-Object -ExpandProperty '(default)' 19 | #strip off the "%1" and quotes 20 | $opencmd = $open.Replace('"', '').Replace('%1', '') 21 | #$opencmd = $open.replace("""%1""","") 22 | #escape spaces 23 | $opencmd = $opencmd -replace ' ', '` ' 24 | #get the executable name from the 25 | $parent = Split-Path -Path $opencmd -Parent 26 | 27 | #now join path for command line RAR.EXE 28 | Join-Path -Path $parent -ChildPath 'rar.exe' 29 | 30 | } 31 | 32 | 33 | #don't run this in the ISE. Redirection doesn't work. 34 | 35 | Function Add-RARContent { 36 | 37 | <# 38 | .Synopsis 39 | Add files to an archive 40 | .Description 41 | Add a folder to a RAR archive. 42 | .Example 43 | PS C:\> Add-Rarcontent c:\work e:\MyArchive.zip 44 | .Notes 45 | Last Updated: 46 | Version : 0.9.1 47 | 48 | .Link 49 | http://jdhitsolutions.com/blog 50 | #> 51 | 52 | [cmdletbinding(SupportsShouldProcess)] 53 | 54 | Param( 55 | [Parameter(Position = 0, Mandatory, 56 | HelpMessage = 'Enter the path to the objects to be archived.')] 57 | [ValidateNotNullOrEmpty()] 58 | [SupportsWildCards()] 59 | [alias('path')] 60 | [string]$Object, 61 | [Parameter(Position = 1, Mandatory, HelpMessage = 'Enter the path to RAR archive.')] 62 | [ValidateNotNullOrEmpty()] 63 | [string]$Archive, 64 | [switch]$MoveFiles, 65 | [ValidateSet(1, 2, 3, 4, 5)] 66 | [int]$CompressionLevel = 5, 67 | [string]$Comment = ('Archive created {0} by {1}\{2}' -f (Get-Date), $env:userdomain, $env:username), 68 | [Parameter(HelpMessage = 'A text file with files and/or paths to exclude from the archive.')] 69 | [string]$ExcludeFile 70 | ) 71 | 72 | Begin { 73 | $ProgressPreference = 'Continue' 74 | Write-Verbose "Starting $($MyInvocation.MyCommand)" 75 | Write-Verbose "Using $rar" 76 | 77 | <# 78 | add|move files using update method 79 | dictionary size is 4k 80 | store paths 81 | store NTFS streams 82 | use a recovery record 83 | set compression level 84 | test archive 85 | recurse subfolders 86 | use quiet mode 87 | -id[c,d,p,q] 88 | Disable messages. 89 | Switch -idc disables the copyright string. 90 | Switch -idd disables “Done” string at the end of operation. 91 | Switch -idp disables the percentage indicator. 92 | Switch -idq turns on the quiet mode, so only error messages 93 | and questions are displayed. 94 | Use $env:temp as the temp folder 95 | #> 96 | 97 | #if delete files 98 | if ($MoveFiles) { 99 | Write-Verbose 'Using command to move files after archiving' 100 | $action = 'm' 101 | $verb = 'Move' #used with Write-Progress 102 | 103 | } 104 | else { 105 | Write-Verbose 'Using command to add files after archiving' 106 | $action = 'a' 107 | $verb = 'Add' #Used with Write-Progress 108 | } 109 | 110 | [string]$rarParam = "$action" 111 | if ($ExcludeFile -AND (Test-Path $ExcludeFile)) { 112 | Write-Verbose "Using exclusion file $ExcludeFile" 113 | $rarParam += " -x@$ExcludeFile" 114 | } 115 | <# 116 | $rarparam = @" 117 | {1} -u -ep1 -os -rr -m{0} -t -r -iddc -w{2} 118 | "@ -f $CompressionLevel, $action, $env:temp 119 | #> 120 | $rarParam += " -u -ep1 -os -rr -m$($CompressionLevel) -t -r -iddc -w$($env:temp)" 121 | } #begin 122 | 123 | Process { 124 | $rarparam += (' {0} {1}' -f """$archive""", """$object""") 125 | 126 | #Write-Verbose "$($rar) $($rarparam)" 127 | 128 | if ($PSCmdlet.ShouldProcess("$($Archive) from $($object)")) { 129 | Write-Progress -Activity 'RAR' -Status "$Verb content to $archive" -CurrentOperation $object 130 | 131 | $sb = [scriptblock]::Create("$rar $rarparam") 132 | $sb | Out-String | Write-Verbose 133 | Invoke-Command -ScriptBlock $sb 134 | 135 | } #should process 136 | #give the archive a moment to close 137 | Start-Sleep -Seconds 5 138 | } #process 139 | 140 | End { 141 | #create temp file if there is a comment and the archive was created 142 | if ($Comment -AND (Test-Path $Archive)) { 143 | Write-Verbose 'Creating temp file comment' 144 | $tmpComment = [System.IO.Path]::GetTempFileName() 145 | 146 | Write-Progress -Activity 'RAR' -Status "Adding comment to $archive" -CurrentOperation $tmpComment -PercentComplete 90 147 | 148 | #comment file must be ASCII 149 | $comment | Out-File -FilePath $tmpComment -Encoding ascii 150 | 151 | #use the some of the same params 152 | $rarparam = @' 153 | c -z{0} {1} -rr -idq 154 | '@ -f $tmpComment, """$Archive""" 155 | 156 | # Write-Verbose "$($rar) $($rarparam)" 157 | #add the comment to the archive 158 | if ($PSCmdlet.ShouldProcess("Archive Comment from $tmpComment")) { 159 | Write-Verbose "Adding Comment: $(Get-Content $tmpComment | Out-String)" 160 | 161 | $sb = [scriptblock]::Create("$rar $rarparam") 162 | Write-Verbose ($sb | Out-String) 163 | Invoke-Command -ScriptBlock $sb 164 | 165 | } #should process 166 | 167 | Write-Verbose 'Deleting comment temp file' 168 | Remove-Item -Path $tmpComment 169 | } #if comment 170 | 171 | #Write-Progress -Activity "RAR" -Status "Adding content to $archive" -CurrentOperation "Complete" -Completed 172 | 173 | Write-Verbose "Ending $($MyInvocation.MyCommand)" 174 | } #end 175 | } #close Add-RARContent 176 | Function Test-RARFile { 177 | [cmdletbinding()] 178 | 179 | Param ( 180 | [Parameter(Position = 0, Mandatory = $True, 181 | HelpMessage = 'Enter the path to a .RAR file.', 182 | ValueFromPipeline = $True)] 183 | [string]$Path 184 | ) 185 | 186 | Begin { 187 | Write-Verbose -Message "Starting $($MyInvocation.MyCommand)" 188 | } #begin 189 | 190 | Process { 191 | if ($path -is [System.IO.FileInfo]) { 192 | $Path = $path.FullName 193 | } 194 | 195 | Write-Verbose "Testing $path" 196 | 197 | $sb = [scriptblock]::Create("$rar t '$path' -idp") 198 | $a = Invoke-Command -ScriptBlock $sb 199 | 200 | Write-Verbose 'Parsing results into objects' 201 | #this is a matchinfo object 202 | $b = $a | Select-String testing | Select-Object -Skip 1 203 | 204 | foreach ($item in $b) { 205 | Write-Verbose $item 206 | #remove Testing 207 | $c = $item.ToString().Replace('Testing', '').Trim() 208 | 209 | #split it on at least 2 spaces 210 | [regex]$r = '\s{2}' 211 | $d = $r.Split($c) | Where-Object { $_ } 212 | 213 | #create a custom object for each archive entry 214 | [PSCustomObject][ordered]@{ 215 | Archive = $path 216 | File = $d[0].Trim() 217 | Status = $d[1].Trim() 218 | } 219 | } #foreach 220 | 221 | } #Process 222 | 223 | End { 224 | Write-Verbose -Message "Ending $($MyInvocation.MyCommand)" 225 | } #end 226 | } #end Test-RARFile 227 | 228 | Function Show-RARContent { 229 | [cmdletbinding()] 230 | 231 | Param ( 232 | [Parameter( 233 | Position = 0, 234 | Mandatory, 235 | HelpMessage = 'Enter the path to a .RAR file.', 236 | ValueFromPipeline 237 | )] 238 | [string]$Path, 239 | [switch]$Detailed 240 | ) 241 | 242 | Begin { 243 | Write-Verbose -Message "Starting $($MyInvocation.MyCommand)" 244 | } #begin 245 | 246 | Process { 247 | if (-Not $Detailed) { 248 | Write-Verbose -Message "Getting bare listing for $path" 249 | $sb = [scriptblock]::Create("$rar lb '$path'") 250 | [PSCustomObject][ordered]@{ 251 | Path = $path 252 | FileSize = (Get-Item -Path $path).Length 253 | Files = Invoke-Command -ScriptBlock $sb 254 | } 255 | } 256 | else { 257 | Write-Verbose -Message "Getting technical details for $path" 258 | 259 | $sb = [scriptblock]::Create("$rar vl '$path'") 260 | Invoke-Command -ScriptBlock $sb 261 | 262 | } #else technical 263 | } #process 264 | 265 | End { 266 | Write-Verbose -Message "Ending $($MyInvocation.MyCommand)" 267 | } #end 268 | } #end Show-RARContent 269 | 270 | 271 | Function Show-RARContent2 { 272 | [cmdletbinding()] 273 | 274 | Param ( 275 | [Parameter( 276 | Position = 0, 277 | Mandatory, 278 | HelpMessage = 'Enter the path to a .RAR file.', 279 | ValueFromPipeline 280 | )] 281 | [string]$Path, 282 | [switch]$Detailed 283 | ) 284 | 285 | Begin { 286 | Write-Verbose -Message "Starting $($MyInvocation.MyCommand)" 287 | } #begin 288 | 289 | Process { 290 | if (-Not $Detailed) { 291 | Write-Verbose -Message "Getting bare listing for $path" 292 | $sb = [scriptblock]::Create("$rar lb '$path'") 293 | [PSCustomObject][ordered]@{ 294 | Path = $path 295 | FileSize = (Get-Item -Path $path).Length 296 | Files = Invoke-Command -ScriptBlock $sb 297 | } 298 | } 299 | else { 300 | Write-Verbose -Message "Getting technical details for $path" 301 | 302 | $sb = [scriptblock]::Create("$rar vl '$path'") 303 | $out = Invoke-Command -ScriptBlock $sb 304 | $total = ($out | Where-Object { $_ -match '%' } | Select-Object -Last 1).Trim() 305 | # $details = $out | Where-Object { $_ -match "\d{4}-\d{2}-\d{2}" } 306 | 307 | Write-Verbose "Parsing $Total" 308 | #parse details out of totals 309 | $totalSplit = $total.split() | Where-Object { $_ } 310 | [int32]$TotalSize = $totalSplit[0].trim() 311 | [int32]$TotalPacked = $totalSplit[1].trim() 312 | [string]$TotalRatio = $totalSplit[2].trim() 313 | [int32]$TotalFiles = $totalSplit[3].trim() 314 | 315 | #parse out the comment line 316 | # $comment = (($out | Where-Object { $_ -match "^Comment:" }) -split "Comment: ")[1] 317 | 318 | #create master object for the archive 319 | [PSCustomObject][ordered]@{ 320 | Path = $Path 321 | # Comment = $comment 322 | # Files = $Files 323 | TotalFiles = $TotalFiles 324 | TotalSize = $TotalSize 325 | TotalPacked = $TotalPacked 326 | TotalRatio = $TotalRatio 327 | } 328 | } #else technical 329 | } #process 330 | 331 | End { 332 | Write-Verbose -Message "Ending $($MyInvocation.MyCommand)" 333 | } #end 334 | } #end Show-RARContent 335 | 336 | 337 | $rar = Get-RARExe 338 | 339 | Export-ModuleMember -Function Add-RARContent, Test-RARFile, Show-RARContent* -Variable rar 340 | 341 | <# 342 | 343 | RAR 5.71 x64 Copyright (c) 1993-2019 Alexander Roshal 28 Apr 2019 344 | Registered to Jeffery D. Hicks 345 | 346 | Usage: rar - - 347 | <@listfiles...> 348 | 349 | 350 | a Add files to archive 351 | c Add archive comment 352 | ch Change archive parameters 353 | cw Write archive comment to file 354 | d Delete files from archive 355 | e Extract files without archived paths 356 | f Freshen files in archive 357 | i[par]= Find string in archives 358 | k Lock archive 359 | l[t[a],b] List archive contents [technical[all], bare] 360 | m[f] Move to archive [files only] 361 | p Print file to stdout 362 | r Repair archive 363 | rc Reconstruct missing volumes 364 | rn Rename archived files 365 | rr[N] Add data recovery record 366 | rv[N] Create recovery volumes 367 | s[name|-] Convert archive to or from SFX 368 | t Test archive files 369 | u Update files in archive 370 | v[t[a],b] Verbosely list archive contents [technical[all],bare] 371 | x Extract files with full path 372 | 373 | 374 | - Stop switches scanning 375 | @[+] Disable [enable] file lists 376 | ac Clear Archive attribute after compression or extraction 377 | ad Append archive name to destination path 378 | ag[format] Generate archive name using the current date 379 | ai Ignore file attributes 380 | ao Add files with Archive attribute set 381 | ap Set path inside archive 382 | as Synchronize archive contents 383 | c- Disable comments show 384 | cfg- Disable read configuration 385 | cl Convert names to lower case 386 | cu Convert names to upper case 387 | df Delete files after archiving 388 | dh Open shared files 389 | dr Delete files to Recycle Bin 390 | ds Disable name sort for solid archive 391 | dw Wipe files after archiving 392 | e[+] Set file exclude and include attributes 393 | ed Do not add empty directories 394 | en Do not put 'end of archive' block 395 | ep Exclude paths from names 396 | ep1 Exclude base directory from names 397 | ep2 Expand paths to full 398 | ep3 Expand paths to full including the drive letter 399 | f Freshen files 400 | hp[password] Encrypt both file data and headers 401 | ht[b|c] Select hash type [BLAKE2,CRC32] for file checksum 402 | id[c,d,p,q] Disable messages 403 | ieml[addr] Send archive by email 404 | ierr Send all messages to stderr 405 | ilog[name] Log errors to file.\ra 406 | inul Disable all messages 407 | ioff[n] Turn PC off after completing an operation 408 | isnd[-] Control notification sounds 409 | iver Display the version number 410 | k Lock archive 411 | kb Keep broken extracted files 412 | log[f][=name] Write names to log file 413 | m<0..5> Set compression level (0-store...3-default...5-maximal) 414 | ma[4|5] Specify a version of archiving format 415 | mc Set advanced compression parameters 416 | md[k,m,g] Dictionary size in KB, MB or GB 417 | ms[ext;ext] Specify file types to store 418 | mt Set the number of threads 419 | n Additionally filter included files 420 | n@ Read additional filter masks from stdin 421 | n@ Read additional filter masks from list file 422 | o[+|-] Set the overwrite mode 423 | oc Set NTFS Compressed attribute 424 | oh Save hard links as the link instead of the file 425 | oi[0-4][:min] Save identical files as references 426 | ol[a] Process symbolic links as the link [absolute paths] 427 | oni Allow potentially incompatible names 428 | or Rename files automatically 429 | os Save NTFS streams 430 | ow Save or restore file owner and group 431 | p[password] Set password 432 | p- Do not query password 433 | qo[-|+] Add quick open information [none|force] 434 | r Recurse subdirectories 435 | r- Disable recursion 436 | r0 Recurse subdirectories for wildcard names only 437 | ri

[:] Set priority (0-default,1-min..15-max) and sleep time in ms 438 | rr[N] Add data recovery record 439 | rv[N] Create recovery volumes 440 | s[,v[-],e] Create solid archive 441 | s- Disable solid archiving 442 | sc[obj] Specify the character set 443 | sfx[name] Create SFX archive 444 | si[name] Read data from standard input (stdin) 445 | sl Process files with size less than specified 446 | sm Process files with size more than specified 447 | t Test files after archiving 448 | ta[mcao] Process files modified after YYYYMMDDHHMMSS date 449 | tb[mcao] Process files modified before YYYYMMDDHHMMSS date 450 | tk Keep original archive time 451 | tl Set archive time to latest file 452 | tn[mcao] Process files newer than time 453 | to[mcao] Process files older than time 454 | ts[m,c,a] Save or restore file time (modification, creation, access) 455 | u Update files 456 | v[k,b] Create volumes with size=*1000 [*1024, *1] 457 | vd Erase disk contents before creating volume 458 | ver[n] File version control 459 | vn Use the old style volume naming scheme 460 | vp Pause before each volume 461 | w Assign work directory 462 | x Exclude specified file 463 | x@ Read file names to exclude from stdin 464 | x@ Exclude files listed in specified list file 465 | y Assume Yes on all queries 466 | z[file] Read archive comment from file 467 | 468 | #> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSBackup 2 | 3 | ![data](assets/db.png) This repository is a collection of PowerShell scripts and functions that I use to back up critical files and folders on my primary desktop. The code in this repository is **not** a PowerShell module that you can download and install. The scripts and functions are written to my specific requirements, but I have had requests to make the code more readily available. You are welcome to use anything in the repository as reference material for your projects. However, everything is offered **as-is** with **no guarantees** that any of the material will work for you. 4 | 5 | > **There are many hard-coded paths unique to my environment. This is normally not a best practice.** 6 | 7 | You can read more about the code in these blog posts. 8 | 9 | - [Creating a PowerShell Backup System](https://jdhitsolutions.com/blog/powershell/6905/creating-a-powershell-backup-system/) 10 | - [Creating a PowerShell Backup System - Part 2](https://jdhitsolutions.com/blog/powershell/6910/creating-a-powershell-backup-system-part-2/) 11 | - [Creating a PowerShell Backup System – Part 3](https://jdhitsolutions.com/blog/powershell/6955/creating-a-powershell-backup-system-part-3/) 12 | - [Creating a PowerShell Backup System – Part 4](https://jdhitsolutions.com/blog/powershell/6962/creating-a-powershell-backup-system-part-4/) 13 | - [Managing My PowerShell Backup Files](https://jdhitsolutions.com/blog/powershell/7081/managing-my-powershell-backup-files/) 14 | 15 | This is a __read-only__ repository, although I am updating it. See the [`Changelog`](changelog.md) file. 16 | 17 | If you have questions or comments, please reach out to me on X using [@JeffHicks](https://twitter.com/jeffhicks). 18 | -------------------------------------------------------------------------------- /RarBackup.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | 3 | [CmdletBinding(SupportsShouldProcess)] 4 | Param( 5 | [Parameter(Mandatory)] 6 | [ValidateScript( { Test-Path $_ })] 7 | [String]$Path, 8 | 9 | [Parameter(HelpMessage = "The final location for the backup files.")] 10 | [ValidateScript( { Test-Path $_ })] 11 | [String]$Destination = "\\DSTulipwood\backup", 12 | 13 | [ValidateScript( { Test-Path $_ })] 14 | #my temporary work area with plenty of free disk space 15 | [String]$TempPath = "D:\Temp", 16 | 17 | [ValidateSet("FULL", "INCREMENTAL")] 18 | [String]$Type = "FULL", 19 | 20 | [Parameter(HelpMessage = "Specify the path to a text file with a list of paths to exclude from backup")] 21 | [ValidateScript( { Test-Path $_ })] 22 | [String]$ExclusionList = "c:\scripts\PSBackup\Exclude.txt" 23 | ) 24 | 25 | Write-Verbose "[$(Get-Date)] Starting $($MyInvocation.MyCommand)" 26 | Write-Host "[$(Get-Date)] Starting $($MyInvocation.MyCommand) $Type for $Path" -foreground green 27 | 28 | if (-Not (Get-Module PSRar)) { 29 | Import-Module C:\scripts\PSBackup\PSRar.psm1 -Force 30 | } 31 | 32 | #replace spaces in path names 33 | $name = "{0}_{1}-{2}.rar" -f (Get-Date -Format "yyyyMMdd"), (Split-Path -Path $Path -Leaf).replace(' ', ''), $Type 34 | $target = Join-Path -Path $TempPath -ChildPath $name 35 | 36 | $nasPath = Join-Path -Path $Destination -ChildPath $name 37 | Write-Host "[$(Get-Date)] Archiving $path to $target" -foreground green 38 | 39 | $rarParams = @{ 40 | path = $Path 41 | Archive = $target 42 | CompressionLevel = 5 43 | Comment = "$Type backup of $(($Path).ToUpper()) from $env:Computername" 44 | } 45 | 46 | if (Test-Path $ExclusionList) { 47 | Write-Verbose "[$(Get-Date)] Using exclusion list from $ExclusionList" 48 | $rarParams.Add("ExcludeFile", $ExclusionList) 49 | } 50 | 51 | $rarParams | Out-String | Write-Verbose 52 | 53 | if ($PSCmdlet.ShouldProcess($Path)) { 54 | #Create the RAR archive -you can use any archiving technique you want 55 | [void](Add-RARContent @rarParams) 56 | 57 | Try { 58 | #copy the RAR file to the NAS for offline storage 59 | Write-Verbose "[$(Get-Date)] Copying $target to $nasPath" 60 | Copy-Item -Path $target -Destination $NASPath -ErrorAction Stop 61 | 62 | #copy to OneDrive 63 | Write-Verbose "[$(Get-Date)] Copying $target to OneDrive\Backup" 64 | Copy-Item -Path $Target -Destination "$ENV:OneDriveConsumer\Backup" -ErrorAction SilentlyContinue 65 | } 66 | Catch { 67 | Write-Warning "Failed to copy $target. $($_.exception.message)" 68 | Throw $_ 69 | } 70 | #verify the file was copied successfully 71 | Write-Verbose "[$(Get-Date)] Validating file hash" 72 | 73 | $here = Get-FileHash $Target 74 | $there = Get-FileHash $nasPath 75 | if ($here.hash -eq $there.hash) { 76 | #delete the file if the hashes match 77 | Write-Verbose "[$(Get-Date)] Deleting $target" 78 | Remove-Item $target 79 | } 80 | else { 81 | Write-Warning "File hash difference detected." 82 | Throw "File hash difference detected" 83 | } 84 | } 85 | 86 | Write-Verbose "[$(Get-Date)] Ending $($MyInvocation.MyCommand)" 87 | Write-Host "[$(Get-Date)] Ending $($MyInvocation.MyCommand)" -foreground green 88 | 89 | #end of script file 90 | -------------------------------------------------------------------------------- /UpdateBackupPending.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | 3 | <# 4 | update backup pending CSV file with updates sizes and dates 5 | as well as deleting files that have since been deleted 6 | #> 7 | 8 | [CmdletBinding(SupportsShouldProcess)] 9 | 10 | Param( 11 | [parameter(position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 12 | [ArgumentCompleter({ $(Get-ChildItem d:\backup\*.csv).FullName })] 13 | [ValidateScript({ Test-Path $_ })] 14 | [string[]]$Path 15 | ) 16 | 17 | Begin { 18 | Write-Verbose "Starting $($MyInvocation.MyCommand)" 19 | } 20 | Process { 21 | Foreach ($item in $Path) { 22 | Write-Verbose "Importing data from $item" 23 | $csv = [System.Collections.generic.list[object]]::new() 24 | [object[]]$imported = Import-Csv -Path $item -OutVariable in 25 | if ($imported.count -eq 0) { 26 | Return "No items found." 27 | } 28 | elseif ($imported.count -eq 1) { 29 | $csv.Add($imported) 30 | } 31 | else { 32 | $csv.AddRange($imported) 33 | } 34 | 35 | Write-Verbose "Processing $($csv.count) items" 36 | $updated = $csv.where({ Test-Path $_.Path }).foreach({ 37 | #get the current version of the file 38 | $now = Get-Item $_.path -Force 39 | 40 | #update size 41 | if ($now.length -ne $_.Size) { 42 | Write-Verbose "Updating file size for $($_.path) from $($_.size) to $($now.length)" 43 | if ($in.count -eq 1) { 44 | $_[0].size = $now.length 45 | } 46 | else { 47 | $_.size = $now.length 48 | } 49 | } 50 | #Update date 51 | if ($now.LastWriteTime -gt [DateTime]$_.Date) { 52 | Write-Verbose "Updating $($now.name) date from $($_.Date) to $($now.LastWriteTime)" 53 | # $_ | Out-String | Write-Verbose 54 | $_[0].Date = ("{0:g}" -f $now.LastWriteTime) 55 | } 56 | $_ 57 | }) 58 | 59 | $remove = $in.where( { $updated.path -notcontains $_.path }).path 60 | if ($remove.count -gt 0) { 61 | Write-Verbose "Removing these files" 62 | $remove | Write-Verbose 63 | } 64 | Write-Verbose "Updated list to $($updated.count) items" 65 | 66 | #update the CSV 67 | Write-Verbose "Updating CSV $item" 68 | $updated | ConvertTo-Csv | Write-Verbose 69 | $updated | Export-Csv -Path $item -NoTypeInformation 70 | } #foreach item 71 | } #process 72 | end { 73 | Write-Verbose "Ending $($MyInvocation.MyCommand)" 74 | } #end 75 | 76 | # New-Item -path 'C:\Program Files\WindowsPowerShell\Scripts\' -name UpdateBackupPending.ps1 -itemtype symbolicLink -value (Convert-Path .\UpdateBackupPending.ps1) -force 77 | 78 | -------------------------------------------------------------------------------- /WeeklyFullBackup.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | #requires -module BurntToast,PSScriptTools 3 | 4 | #only do a full backup if there was an incremental 5 | 6 | [CmdletBinding(SupportsShouldProcess)] 7 | Param( 8 | [Parameter(Position = 0, HelpMessage = "Path to a text file with folders to backup.")] 9 | [ValidateNotNullOrEmpty()] 10 | [ValidateScript( { Test-Path $_ })] 11 | [String]$PathList = "c:\scripts\PSBackup\mybackupPaths.txt", 12 | 13 | [Parameter(Position = 1, HelpMessage = "The destination folder for the backup files")] 14 | [ValidateNotNullOrEmpty()] 15 | [String]$Destination = "\\DSTulipwood\backup" 16 | ) 17 | 18 | #verify credential 19 | C:\scripts\Test-DSTulipwood.ps1 20 | $VerbosePreference = "Continue" 21 | 22 | Write-Verbose "[$(Get-Date)] Starting $($MyInvocation.MyCommand)" 23 | Write-Verbose "[$(Get-Date)] Creating Sets list" 24 | $sets = C:\scripts\PSBackup\BuildList.ps1 -PathList $PathList -Destination $Destination 25 | 26 | #it is possible there will be no backup set 27 | if ($sets.count -gt 0) { 28 | foreach ($set in $sets) { 29 | Write-Verbose "[$(Get-Date)] Invoking backup for $set" 30 | #8/17/2024 Pass the destination to the backup script 31 | c:\scripts\PSBackup\Invoke-FullBackup.ps1 -path $set -Destination $Destination 32 | } 33 | } 34 | #1/26/2024 Remove Box Sync Folder 35 | 36 | <# # 8/4/2022 Copy Quickbook Backups to Box 37 | if (Test-Path 'C:\users\jeff\Box\Default Sync Folder\') { 38 | Get-ChildItem D:\OneDrive\Backup\*.qbb | Copy-Item -Destination 'C:\users\jeff\Box\Default Sync Folder\' 39 | } 40 | else { 41 | Write-Warning "[$(Get-Date)] Can't verify Box folder." 42 | } #> 43 | 44 | Write-Verbose "[$(Get-Date)] Ending $($MyInvocation.MyCommand)" 45 | 46 | $VerbosePreference = "SilentlyContinue" -------------------------------------------------------------------------------- /assets/db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdhitsolutions/PSBackup/13b40a9dfaa9e6a3d8e1d31aed96d8ad199e9372/assets/db.png -------------------------------------------------------------------------------- /automation-functions/Create-IncrementalBackupJob.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | #requires -module PSScheduledJob 3 | 4 | #create incremental scheduled job 5 | 6 | Import-Module PSScheduledJob 7 | 8 | $filepath = "C:\scripts\PSBackup\DailyIncrementalBackup.ps1" 9 | 10 | if (Test-Path $filepath) { 11 | $trigger = New-JobTrigger -At 10:00PM -DaysOfWeek Saturday, Sunday, Monday, Tuesday, Wednesday, Thursday -Weekly 12 | $jobOpt = New-ScheduledJobOption -RunElevated -RequireNetwork -WakeToRun 13 | 14 | #parameters for Register-ScheduledJob 15 | $job = @{ 16 | FilePath = $filepath 17 | Name = "DailyIncremental" 18 | Trigger = $trigger 19 | ScheduledJobOption = $jobOpt 20 | MaxResultCount = 7 21 | Credential = $env:username 22 | } 23 | 24 | Register-ScheduledJob @job 25 | } 26 | else { 27 | Write-Warning "Can't find $filepath" 28 | } 29 | 30 | # Unregister-ScheduledJob DailyIncremental 31 | -------------------------------------------------------------------------------- /automation-functions/Create-WatcherScheduledJob.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | #requires -module PSScheduledJob 3 | 4 | #create FileSystemWatcher job for my incremental backups. 5 | 6 | #scheduled job scriptblock 7 | $action = { 8 | 9 | if (Test-Path c:\scripts\PSBackup\myBackupPaths.txt) { 10 | #filter out commented lines and lines with just white space 11 | $paths = Get-Content c:\scripts\PSBackup\myBackupPaths.txt | Where-Object {$_ -match "(^[^#]\S*)" -and $_ -notmatch "^\s+$"} 12 | } 13 | else { 14 | Throw "Failed to find c:\scripts\PSBackup\myBackupPaths.txt" 15 | #bail out 16 | Return 17 | } 18 | 19 | #trim leading and trailing white spaces in each path 20 | Foreach ($Path in $Paths.Trim()) { 21 | 22 | #get the directory name from the list of paths 23 | $name = ((Split-Path $path -Leaf).replace(' ', '')) 24 | 25 | #specify the directory for the CSV log files 26 | $log = "D:\Backup\{0}-log.csv" -f $name 27 | 28 | #define the watcher object 29 | Write-Host "Creating a FileSystemWatcher for $Path" -ForegroundColor green 30 | $watcher = [System.IO.FileSystemWatcher]($path) 31 | $watcher.IncludeSubdirectories = $True 32 | #enable the watcher 33 | $watcher.EnableRaisingEvents = $True 34 | 35 | #the Action scriptblock to be run when an event fires 36 | $sbText = "c:\scripts\PSBackup\LogBackupEntry.ps1 -event `$event -CSVPath $log" 37 | 38 | $sb = [scriptblock]::Create($sbText) 39 | 40 | #register the event subscriber 41 | 42 | #possible events are Changed,Deleted,Created 43 | $params = @{ 44 | InputObject = $watcher 45 | EventName = "changed" 46 | SourceIdentifier = "FileChange-$Name" 47 | MessageData = "A file was created or changed in $Path" 48 | Action = $sb 49 | } 50 | 51 | $params.MessageData | Out-String | Write-Host -ForegroundColor cyan 52 | $params.Action | Out-String | Write-Host -ForegroundColor Cyan 53 | Register-ObjectEvent @params 54 | 55 | } #foreach path 56 | 57 | Get-EventSubscriber | Out-String | Write-Host -ForegroundColor yellow 58 | 59 | #keep the job alive 60 | Do { 61 | Start-Sleep -Seconds 1 62 | } while ($True) 63 | 64 | } #close job action 65 | 66 | $trigger = New-JobTrigger -AtStartup 67 | 68 | Register-ScheduledJob -Name "DailyWatcher" -ScriptBlock $action -Trigger $trigger 69 | 70 | # manually start the task in Task Scheduler 71 | 72 | # Unregister-ScheduledJob "DailyWatcher" 73 | -------------------------------------------------------------------------------- /automation-functions/Create-WeeklyFullBackupJobs.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | #requires -module PSScheduledJob 3 | 4 | #create weekly full backups 5 | 6 | $trigger = New-JobTrigger -At 10:00PM -DaysOfWeek Friday -WeeksInterval 1 -Weekly 7 | $jobOpt = New-ScheduledJobOption -RunElevated -RequireNetwork -WakeToRun 8 | 9 | $params = @{ 10 | FilePath = "C:\scripts\PSBackup\WeeklyFullBackup.ps1" 11 | Name = "WeeklyFullBackup" 12 | Trigger = $trigger 13 | ScheduledJobOption = $jobOpt 14 | MaxResultCount = 5 15 | Credential = "$env:computername\jeff" 16 | } 17 | 18 | Register-ScheduledJob @params 19 | 20 | # Unregister-ScheduledJob WeeklyFullBackup. 21 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # PSBackup ChangeLog 2 | 3 | ## October 2, 2024 4 | 5 | - Modified files to use `$ENV:OneDrive` in place of the hard-coded path. 6 | - Modified weekly backup to pass the destination path as a parameter to `Invoke-FullBackup.ps1`. 7 | - Moved primary branch from `master` to `main`. 8 | 9 | ## August 3, 2024 10 | 11 | - Updated `BuildList.ps1` to fix a bug in building the list when there are no incremental backups. 12 | 13 | ## April 15, 2024 14 | 15 | - Revised `LogBackupEntry.ps1` to better exclude files. 16 | - Updated `WeeklyFullBackup.ps1` to validate NAS credential. 17 | - Restructured folder layout for this project. 18 | 19 | ## February 2, 2024 20 | 21 | - Changed `Substack` references to `Behind`. 22 | 23 | ## December 16, 2023 24 | 25 | - Added more Verbose output to `WeeklyFullBackup.ps1` 26 | 27 | ## December 9, 2023 28 | 29 | - Modified `WeeklyFullBackup` to test if there is a backup set. 30 | - Code cleanup 31 | - 32 | ## November 7, 2023 33 | 34 | - Code cleanup 35 | - Modified NAS references to point to new location 36 | 37 | ## August 4, 2022 38 | 39 | - Updated Exclude file 40 | - Added a Quickbooks backup to Box as part of the weekly backup. 41 | - Added `loadformats.ps1`. 42 | 43 | ## May 18, 2021 44 | 45 | - Modified `Add-BackupEntry` to use normalized file system path. 46 | - Modified `Add-BackupEntry` to handle paths with spaces like "Google Drive" 47 | 48 | ## April 13, 2021 49 | 50 | - Added a table view called KB to `mybackupfile.format.ps1xml` which is a duplicate of the default except showing file sizes in KB. 51 | - Created `myBackupPendingSummary.ps1` with custom format file `pending.format.ps1xml`. 52 | - Modified `myBackupPending.ps1` to write a custom object to the pipeline using a table view defined in `pending.format.ps1xml`. The summary is now separate from the list of pending files. 53 | - Removed `-Raw` from `myBackupPending.ps1`. Use `Select-Object` to see all properties. 54 | - Added script file `loadformat.ps1` to load format.ps1xml files. 55 | - Updated `UpdateBackupPending.ps1` to update the file date in the CSV file. 56 | 57 | ## April 3, 2021 58 | 59 | - Renamed `WeeklyFullBackup.ps1xml` to `Invoke-FullBackup.ps1`. Created a new `WeeklyFullBackup.ps1` file to only back up folders that have pending incremental backup files. There's no reason to backup a folder if nothing has changed. 60 | - Added `BuildList.ps1` to create the list of paths for the weekly full backup based on existing incremental backups and logged changes that haven't been backed up yet. 61 | - Updated `PSRar.psm1` and removed references to the the Dev version of the module. 62 | 63 | ## January 27, 2021 64 | 65 | - Added `Yesterday` parameter to `Get-MyBackupFile`. 66 | 67 | ## January 24, 2021 68 | 69 | - Modified `Add-BackupEntry` to define an alias `abe`. 70 | - Modified `BackupSet` parameter in `Add-BackupEntry` to be position 0 since I am mostly piping files to the command. 71 | - Updated `mybackupfile.format.ps1xml` to display set grouping with an ANSI color sequence. 72 | 73 | ## January 19, 2021 74 | 75 | - Fixed incorrect property name in `MyBackupReport.ps1`. 76 | - Added a `Last` parameter to the `Raw` parameter set in `MyBackupReport.ps1`. 77 | - Created `Get-MyBackupFile` function and `mybackupfile.format.ps1xml` format file. The function has an alias of `gbf`. 78 | - Updated `LICENSE`. 79 | 80 | ## January 12, 2021 81 | 82 | - Fixed another bug in `UpdateBackupPending.ps1` to handle situations where CSV only has 1 item. 83 | 84 | ## December 29, 2020 85 | 86 | - Added more explicit WhatIf code to `DailyIncrementalBackup.ps1`. 87 | - Fixed bug in `DailyIncrementalBackup.ps1` that wasn't archiving the proper path. 88 | 89 | ## December 22, 2020 90 | 91 | - Modified `UpdateBackupPending.ps1` to handle situations where CSV only has 1 item. 92 | 93 | ## November 19, 2020 94 | 95 | - Added `Archive-Oldest.ps1` to rename the oldest file per backup set in a location as an Archive. The goal is to keep at least one archive for a longer period. You might run this quarterly or every 6 months. 96 | - Modified `LogBackupEntry.ps1` to archive the log file when it is 10MB in size and reset the file. 97 | - Adding an exclusion file to skip certain paths. 98 | - Modified `RarBackup.ps1` to set the location via a parameter and not be hardcoded. 99 | - Modified `myBackupReport.ps1` to include the path in the report header. 100 | - Modified `myBackupReport.ps1` to include a `-SummaryOnly` parameter. This added parameter sets to the script. 101 | 102 | ## November 5, 2020 103 | 104 | - Modified `myBackupPending.ps1` to include files with zero size as these might be new files that haven't been updated yet. 105 | - Updated `myBackupPaths.txt` to use full filesystem paths. 106 | - Added `UpdateBackupPending.ps1` to update pending CSV files. This will remove deleted files and update file sizes. 107 | 108 | ## October 17, 2020 109 | 110 | - Fixed bad OneDrive reference in `WeeklyFullBackup.ps1`. 111 | - Added ValidatePath() to Path parameter in `DailyIncrementalBackup.ps1`. 112 | - Added error handling to `DailyIncrementalBackup.ps1` and `WeeklyFullBackup.ps1` to abort if the PSRar module can't be loaded. 113 | - Minor code reformatting. 114 | 115 | ## October 10, 2020 116 | 117 | - Modified references to OneDrive to use `$ENV:OneDriveConsumer`. 118 | - Modified `MonitorDailyWatcher.ps1` to use splatting and support `-WhatIf`. 119 | 120 | ## September 18, 2020 121 | 122 | - Added new paths to `myBackupPaths.txt` 123 | - Minor changes in backup scripts with `Write-Host` commands to reflect progress and aid in troubleshooting errors. 124 | 125 | ## September 9, 2020 126 | 127 | - Added transcript log files to incremental and weekly backup scripts. The transcript name uses the `New-CustomFileName` command from the [PSScriptTools](https://github.com/jdhitsolutions/PSScriptTools) module. 128 | - Modified `MyBackupReport` to show decimal points if the backup file is smaller than 1MB in size. 129 | - Code reformatting to make some scripts and functions easier to read. 130 | 131 | ## September 1, 2020 132 | 133 | - Modified `LogBackupEntry.ps1` to only add the file if it doesn't exist in the CSV file and to make logging optional. 134 | - Modified `myBackupReport.ps1` to format values as MB. Added a header to the report. 135 | - Modified `myBackupPending.ps1` to pass the backup folder as a parameter. 136 | - Code cleanup in `PSRar.psm1`. 137 | 138 | ## August 23, 2020 139 | 140 | - Added BurntToast notifications to some of my commands. This requires the [BurntToast](https://github.com/Windos/BurntToast) module. 141 | - Updated `README.md`. 142 | 143 | ## July 14, 2020 144 | 145 | - Reduced OneDrive backup copies to 2. 146 | - Renamed `Dev-PSRar.psm1` to `PSRar.psm1` so it can be included in the GitHub repo. 147 | - Changed relative paths in `WeeklyFullBackup.ps1` to use `$PSScriptRoot`. 148 | 149 | ## April 10, 2020 150 | 151 | - Added the `Dev-PSRar.psm1` file to the module for reference purposes. 152 | - Added time stamps to trace messages in `DailyIncrementalBackup.ps1` 153 | - Modified `DailyIncrementalBackup.ps1` to better cleanup temporary backup folder. 154 | 155 | ## March 31, 2020 156 | 157 | - Modified `WeeklyFullBackup.ps1` to allow backing up a single directory. 158 | - Added `Add-BackupEntry.ps1` to the project. 159 | 160 | ## February 8, 2020 161 | 162 | - Bug fixes in `WeeklyBackup.ps1` from changes I made to variable names and paths 163 | - Moved `RarBackup.ps1` to this repository 164 | 165 | ## February 7, 2020 166 | 167 | - Initial file upload 168 | - Revised files to reflect the new location 169 | - New `README.md` 170 | -------------------------------------------------------------------------------- /formats/mybackupfile.format.ps1xml: -------------------------------------------------------------------------------- 1 |  2 | 9 | 10 | 11 | 12 | 13 | default 14 | 15 | myBackupFile 16 | 17 | 18 | 19 | $ln = "{0}{1}" -f ($($_.BackupSet)[0].tostring().toupper()),$($_.Backupset).Substring(1) 20 | if ($host.name -match 'console|code') { 21 | "$([char]27)[38;5;217m$ln$([char]27)[0m" 22 | } 23 | else{ 24 | $ln 25 | } 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 25 36 | left 37 | 38 | 39 | 40 | 10 41 | right 42 | 43 | 44 | 45 | 40 46 | left 47 | 48 | 49 | 50 | 51 | 52 | 53 | Created 54 | 55 | 56 | 57 | if ($_.Size -lt 1MB) { 58 | [math]::round($_.Length/1MB,4) 59 | } 60 | else { 61 | $_.Size/1MB -as [int] 62 | } 63 | 64 | 65 | 66 | 67 | 68 | if ($host.name -match 'console|code' -AND $_.settype -eq 'Incremental') { 69 | "$([char]27)[38;5;191m$($_.name)$([char]27)[0m" 70 | } 71 | else { 72 | $_.Name 73 | } 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 86 | kb 87 | 88 | myBackupFile 89 | 90 | 91 | 92 | $ln = "{0}{1}" -f ($($_.BackupSet)[0].tostring().toupper()),$($_.Backupset).Substring(1) 93 | if ($host.name -match 'console|code') { 94 | "$([char]27)[38;5;217m$ln$([char]27)[0m" 95 | } 96 | else{ 97 | $ln 98 | } 99 | 100 | 101 | 102 | 103 | 105 | 106 | 107 | 108 | 25 109 | left 110 | 111 | 112 | 113 | 10 114 | right 115 | 116 | 117 | 118 | 40 119 | left 120 | 121 | 122 | 123 | 124 | 125 | 126 | Created 127 | 128 | 129 | 130 | if ($_.Size -lt 1KB) { 131 | [math]::round($_.Length/1KB,4) 132 | } 133 | else { 134 | $_.Size/1KB -as [int] 135 | } 136 | 137 | 138 | 139 | 140 | 141 | if ($host.name -match 'console|code' -AND $_.settype -eq 'Incremental') { 142 | "$([char]27)[38;5;191m$($_.name)$([char]27)[0m" 143 | } 144 | else { 145 | $_.Name 146 | } 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | raw 157 | 158 | myBackupFile 159 | 160 | 161 | Directory 162 | 163 | 164 | 165 | 167 | 168 | 169 | 170 | 22 171 | left 172 | 173 | 174 | 175 | 15 176 | right 177 | 178 | 179 | 180 | 38 181 | left 182 | 183 | 184 | 185 | 18 186 | left 187 | 188 | 189 | 190 | 191 | 192 | 193 | Created 194 | 195 | 196 | Size 197 | 198 | 199 | Name 200 | 201 | 202 | BackupSet 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /formats/pending.format.ps1xml: -------------------------------------------------------------------------------- 1 |  2 | 9 | 10 | 11 | 12 | 13 | default 14 | 15 | pendingBackupFiles 16 | 17 | 18 | 20 | 21 | 22 | 23 | 15 24 | left 25 | 26 | 27 | 28 | 8 29 | right 30 | 31 | 32 | 33 | 10 34 | right 35 | 36 | 37 | 38 | 39 | 40 | 41 | Backup 42 | 43 | 44 | Files 45 | 46 | 47 | Size 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | default 56 | 57 | pendingBackupSummary 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | $head = "Pending Backup Files [$(Get-Date)]" 66 | if ($host.name -match 'console') { 67 | "$([char]27)[1;38;5;216m$head$([char]27)[0m" 68 | } 69 | else { 70 | $head 71 | } 72 | 73 | 74 | 75 | 76 | 77 | $_.pendingFiles | Format-Table | Out-String 78 | 79 | 80 | 81 | 82 | $total = @" 83 | Total Files : $($_.TotalFiles) 84 | Total SizeKB: $($_.TotalSizeKB) 85 | "@ 86 | $total 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | default 97 | 98 | pendingFile 99 | 100 | 101 | Log 102 | 103 | 104 | 105 | 107 | 108 | 109 | 110 | 23 111 | left 112 | 113 | 114 | 115 | 50 116 | left 117 | 118 | 119 | 120 | 9 121 | right 122 | 123 | 124 | 125 | 43 126 | left 127 | 128 | 129 | 130 | 131 | 132 | 133 | Date 134 | 135 | 136 | Name 137 | 138 | 139 | Size 140 | 141 | 142 | Directory 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | filelist 152 | 153 | pendingFile 154 | 155 | 156 | 164 | Set 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 57 174 | left 175 | 176 | 177 | 178 | 179 | 180 | 184 | 185 | Path 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /loadformats.ps1: -------------------------------------------------------------------------------- 1 | # load the format files 2 | 3 | <# Get-ChildItem $PSScriptRoot\*format.ps1xml | 4 | ForEach-Object { 5 | Update-FormatData -AppendPath $_.FullName 6 | } #> 7 | 8 | Update-FormatData $PSScriptRoot\formats\mybackupfile.format.ps1xml 9 | Update-FormatData $PSScriptRoot\formats\pending.format.ps1xml 10 | -------------------------------------------------------------------------------- /myBackupPaths.txt: -------------------------------------------------------------------------------- 1 | #comment out paths with a # symbol at the beginning of the line 2 | #you can't use $ENV variables or shell shortcuts like ~ 3 | 4 | #the first four characters of the directory need to be unique 5 | 6 | C:\users\jeff\documents 7 | C:\Users\Jeff\Google Drive 8 | C:\users\jeff\dropbox 9 | C:\Scripts 10 | C:\Users\Jeff\Documents\MuseScore4\Scores 11 | D:\OneDrive\PSProfiles 12 | D:\OneDrive\Behind 13 | D:\jdhit 14 | D:\OneDrive\PSBehind 15 | D:\OneDrive\bsky 16 | C:\Presentations 17 | 18 | #C:\Training 19 | #D:\ToolmakingBook 20 | #D:\PowerShellPracticePrimer 21 | -------------------------------------------------------------------------------- /myBackupPending.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | 3 | #get a report of pending files to be backed up 4 | 5 | [CmdletBinding()] 6 | 7 | Param( 8 | [parameter(position = 0)] 9 | [ValidateNotNullOrEmpty()] 10 | [String]$Backup = "*", 11 | 12 | [Parameter(HelpMessage = "Specify the location for the backup-log.csv files.")] 13 | [ValidateNotNullOrEmpty()] 14 | [ValidateScript({Test-Path $_})] 15 | [String]$BackupFolder = "D:\Backup" 16 | ) 17 | 18 | $csv = Join-Path -Path $BackupFolder -ChildPath "$backup-log.csv" 19 | Write-Verbose "Parsing CSV files $csv" 20 | 21 | $f = Get-ChildItem -path $csv | ForEach-Object { 22 | $p = $_.FullName 23 | Write-Verbose "Processing $p" 24 | Import-Csv -Path $p -OutVariable in | Add-Member -MemberType NoteProperty -Name Log -value $p -PassThru 25 | } | Where-Object { $_.IsFolder -eq 'False' } 26 | 27 | Write-Verbose "Found $($in.count) files to import" 28 | Write-Verbose "Getting unique file names from $($f.count) files" 29 | $files = ($f.name | Select-Object -Unique).Foreach({ 30 | $n = $_; 31 | $f.where({ $_.name -eq $n }) | 32 | Sort-Object -Property {$_.date -as [DateTime] } | Select-Object -last 1 }) 33 | 34 | Write-Verbose "Found $($files.count) unique files" 35 | 36 | $files | ForEach-Object { 37 | #insert a new TypeName 38 | $_.PSObject.TypeNames.insert(0,'pendingFile') 39 | $_ | Add-Member -MemberType ScriptProperty -Name Set -value {(split-path $this.log -Leaf).split("-")[0] } 40 | $_ | Add-Member -MemberType ScriptProperty -Name FileName -Value {Split-Path $this.path -leaf} -PassThru 41 | } | Sort-Object -Property Log, Directory, Name 42 | 43 | Write-Verbose "Pending report finished" 44 | -------------------------------------------------------------------------------- /myBackupPendingSummary.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | 3 | #get a report of pending files to be backed up 4 | 5 | [CmdletBinding()] 6 | [OutputType("pendingBackupSummary")] 7 | 8 | Param( 9 | [parameter(position = 0)] 10 | [ValidateNotNullOrEmpty()] 11 | [String]$Backup = "*", 12 | 13 | [Parameter(HelpMessage = "Specify the location for the backup-log.csv files.")] 14 | [ValidateNotNullOrEmpty()] 15 | [ValidateScript( { Test-Path $_ })] 16 | [String]$BackupFolder = "D:\Backup" 17 | ) 18 | 19 | $csv = Join-Path -Path $BackupFolder -ChildPath "$backup-log.csv" 20 | Write-Verbose "Parsing CSV files $csv" 21 | 22 | $f = Get-ChildItem -Path $csv | ForEach-Object { 23 | $p = $_.FullName 24 | Write-Verbose "Processing $p" 25 | Import-Csv -Path $p -OutVariable in | Add-Member -MemberType NoteProperty -Name Log -Value $p -PassThru 26 | } | Where-Object { $_.IsFolder -eq 'False' } 27 | 28 | Write-Verbose "Found $($in.count) files to import" 29 | Write-Verbose "Getting unique file names from $($f.count) files" 30 | $files = ($f.name | Select-Object -Unique).Foreach( { $n = $_; $f.where( { $_.name -eq $n }) | 31 | Sort-Object -Property { $_.date -as [DateTime] } | Select-Object -Last 1 }) 32 | 33 | Write-Verbose "Found $($files.count) unique files" 34 | 35 | Write-Verbose "Grouping files" 36 | $grouped = $files | Group-Object Log 37 | 38 | $pendingFiles = foreach ($item in $grouped) { 39 | [PSCustomObject]@{ 40 | PSTypeName = "pendingBackupFiles" 41 | Backup = (Get-Item $item.Name).basename.split("-")[0] 42 | Files = $item.Count 43 | Size = ($item.group | Measure-Object -Property size -Sum).sum 44 | } 45 | } #foreach item 46 | 47 | $totSize = ($pendingFiles.Size | Measure-Object -sum ).sum 48 | [PSCustomObject]@{ 49 | PSTypeName = "pendingBackupSummary" 50 | TotalFiles = ($grouped | Measure-Object -Property count -Sum).sum 51 | TotalSizeKB = [math]::round($totSize/ 1KB, 4) 52 | PendingFiles = $pendingFiles 53 | } 54 | 55 | 56 | Write-Verbose "Pending report finished" 57 | -------------------------------------------------------------------------------- /myBackupReport.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | #requires -module PSScriptTools 3 | 4 | #myBackupReport.ps1 5 | # this script uses Format-Value from the PSScriptTools module. 6 | [CmdletBinding(DefaultParameterSetName = 'default')] 7 | 8 | Param( 9 | [Parameter(Position = 0, HelpMessage = 'Enter the path where the backup files are stored.', ParameterSetName = 'default')] 10 | [Parameter(ParameterSetName = 'raw')] 11 | [Parameter(ParameterSetName = 'sumOnly')] 12 | [ValidateNotNullOrEmpty()] 13 | [ValidateScript({ Test-Path $_ })] 14 | #This is my NAS device 15 | [String]$Path = '\\DSTulipwood\backup', 16 | 17 | [Parameter(HelpMessage = 'Only display the summary', ParameterSetName = 'sumOnly')] 18 | [Switch]$SummaryOnly, 19 | 20 | [Parameter(HelpMessage = 'Get backup files only with no formatted summary.', ParameterSetName = 'raw')] 21 | [Switch]$Raw, 22 | [Parameter(HelpMessage = 'Get the last X number of raw files', ParameterSetName = 'raw')] 23 | [Int]$Last 24 | ) 25 | 26 | $reportVer = '1.3.1' 27 | 28 | #convert path to a full filesystem path 29 | $Path = Convert-Path $path 30 | Write-Verbose "Starting $($MyInvocation.MyCommand) v.$ReportVer" 31 | Write-Verbose "Using parameter set $($PSCmdlet.ParameterSetName)" 32 | 33 | <# 34 | A regular expression pattern to match on backup file name with named captures 35 | to be used in adding some custom properties. My backup names are like: 36 | 37 | 20191101_Scripts-FULL.rar 38 | 20191107_Scripts-INCREMENTAL.rar 39 | #> 40 | 41 | [regex]$rx = '^20\d{6}_(?\w+)-(?\w+)\.((rar)|(zip))$' 42 | 43 | <# 44 | I am doing so 'pre-filtering' on the file extension and then using the regular 45 | expression filter to fine tune the results 46 | #> 47 | $files = Get-ChildItem $Path\*.zip, $Path\*.rar | Where-Object { $rx.IsMatch($_.name) } 48 | 49 | #Bail out if no file 50 | if ($files.count -eq 0) { 51 | Write-Warning "No backup files found in $Path" 52 | return 53 | } 54 | 55 | Write-Verbose "Found $($files.count) files in $Path" 56 | 57 | foreach ($item in $files) { 58 | $SetPath = $rx.matches($item.name).groups[4].value 59 | $SetType = $rx.matches($item.name).groups[5].value 60 | 61 | #add some custom properties to be used with formatted results based on named captures 62 | $item | Add-Member -MemberType NoteProperty -Name SetPath -Value $SetPath 63 | $item | Add-Member -MemberType NoteProperty -Name SetType -Value $SetType 64 | } 65 | 66 | if ($raw -AND $last) { 67 | Write-Verbose "Getting last $last raw files" 68 | $Files | Sort-Object -Property LastWriteTime | Select-Object -Last $last 69 | } 70 | elseif ($raw) { 71 | Write-Verbose 'Getting all raw files' 72 | $Files | Sort-Object -Property LastWriteTime 73 | } 74 | else { 75 | Write-Verbose 'Preparing report data' 76 | Write-Host "$([char]0x1b)[1;4;38;5;216m`nMy Backup Report - $Path`n$([char]0x1b)[0m" 77 | if ($PSCmdlet.ParameterSetName -eq 'default') { 78 | $files | Sort-Object SetPath, SetType, LastWriteTime | 79 | Format-Table -GroupBy SetPath -Property @{Name = 'Created'; Expression = { $_.LastWriteTime } }, 80 | @{Name = 'SizeMB'; Expression = { 81 | $size = $_.length 82 | if ($size -lt 1MB) { 83 | $d = 4 84 | } 85 | else { 86 | $d = 0 87 | } 88 | Format-Value -input $size -Unit MB -Decimal $d 89 | } 90 | }, 91 | Name 92 | } 93 | $grouped = $files | Group-Object -Property SetPath 94 | $summary = foreach ($item in $grouped) { 95 | [PSCustomObject]@{ 96 | BackupSet = $item.name 97 | Files = $item.Count 98 | SizeMB = ($item.group | Measure-Object -Property Length -Sum -OutVariable m).sum | Format-Value -Unit MB -Decimal 2 99 | } 100 | } 101 | 102 | $total = [PSCustomObject]@{ 103 | TotalFiles = ($summary.files | Measure-Object -Sum).sum 104 | TotalSizeMB = ($summary.sizeMB | Measure-Object -Sum).sum 105 | } 106 | 107 | Write-Host "Backup Summary $((Get-Date).ToShortDateString())" -ForegroundColor yellow 108 | Write-Host "Path: $Path" -ForegroundColor Yellow 109 | 110 | ($summary | Sort-Object Size -Descending | Format-Table | Out-String).TrimEnd() | Write-Host -ForegroundColor yellow 111 | 112 | ($total | Format-Table | Out-String).TrimEnd() | Write-Host -ForegroundColor yellow 113 | } 114 | 115 | if ($PSCmdlet.ParameterSetName -ne 'raw') { 116 | Write-Host "$([char]0x1b)[38;5;216m`nReport version $reportver$([char]0x1b)[0m" 117 | } 118 | -------------------------------------------------------------------------------- /myBackupTrim.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5.1 2 | 3 | #MyBackupTrim.ps1 4 | 5 | #trim full backups to the last X number of files 6 | 7 | [CmdletBinding(SupportsShouldProcess)] 8 | Param( 9 | [Parameter(Position = 0, HelpMessage = "Specify the backup folder location")] 10 | [ValidateNotNullOrEmpty()] 11 | [ValidateScript( { Test-Path $_ })] 12 | [String]$Path = "\\DSTulipwood\backup", 13 | 14 | [Parameter(HelpMessage = "Specify a file pattern")] 15 | [ValidateNotNullOrEmpty()] 16 | [String]$Pattern = "*-FULL.rar", 17 | 18 | [Parameter(HelpMessage = "Specify the number of the most recent files to keep")] 19 | [ValidateScript({ $_ -ge 1 })] 20 | [Int]$Count = 4 21 | ) 22 | 23 | $find = Join-Path -Path $path -ChildPath $pattern 24 | Write-Verbose "Finding backup files from $Find" 25 | Try { 26 | $files = Get-ChildItem -Path $find -File -ErrorAction Stop 27 | } 28 | Catch { 29 | Throw $_ 30 | } 31 | 32 | if ($files.count -gt 0) { 33 | Write-Verbose "Found $($files.count) backup files" 34 | <# 35 | group the files based on the naming convention 36 | like 20191108_documents-FULL.rar and 20191108_Scripts-FULL.rar 37 | but make sure there are at least $Count number of files 38 | #> 39 | 40 | $grouped = $files | 41 | Group-Object -Property { ([regex]"(?<=_)\w+(?=-)").match($_.BaseName).value } | 42 | Where-Object { $_.count -gt $count } 43 | 44 | if ($grouped) { 45 | foreach ($item in $grouped) { 46 | Write-Verbose "Trimming $($item.name)" 47 | $item.group | 48 | Sort-Object -Property LastWriteTime -Descending | 49 | Select-Object -Skip $count | Remove-Item 50 | } 51 | } 52 | else { 53 | Write-Host "Not enough files to justify cleanup." -ForegroundColor magenta 54 | } 55 | } 56 | 57 | #End of script 58 | --------------------------------------------------------------------------------