Skip to content

Instantly share code, notes, and snippets.

@kotauskas
Created December 10, 2021 13:14
Show Gist options
  • Save kotauskas/cbe645f56ac685b8991e68aa4c28a7c6 to your computer and use it in GitHub Desktop.
Save kotauskas/cbe645f56ac685b8991e68aa4c28a7c6 to your computer and use it in GitHub Desktop.
PowerShell backup tool

A cute little PowerShell backup script that uses 7-Zip to back up a directory or file, complete with atomic overwrite (new backup will overwrite the previous one, but will not delete it before the new one is produced) and Event Viewer logging. I only tested this on Windows, so it will spit errors about failing to register the Event Viewer log at best and won't perform its intended purpose at worst.

Usage

powershell.exe Backup-Location -Source <source> -Destination <destination> [<other parameters>]

Will grab the directory or file at <source> and create an archive at <destination> that overwrites it. The process is as follows:

  1. The file at <destination> with a .tmp suffix is checked. If one exists, it's renamed to just <destination> and a warning with event ID 4 is emitted to the event log that notes down the presence of an unsuccessful previous backup.
    • If the move fails, an error with event ID 2 is logged and the backup is aborted.
  2. The 7-Zip command line tool is invoked to create a new backup at <destination>.tmp.
  3. If that succeeds, the file at <destination>, if any, is deleted (which logs an information event with ID 1), and <destination.tmp> is renamed to <destination>.
    • If deletion fails, an error with event ID 3 is logged and the backup is aborted at this stage. The new backup is then available at <destination>.tmp, and <destination> has the old backup.
    • If the move fails, an error with event ID 2 is logged similarly to step 1.
  4. If everything succeeds, an information event with ID 0 is logged.

Before all of this, the script checks whether the default (or the specified custom) event log and source exist, and if not, attempts to register them. That requires administrative privileges. Run the script in a shell with administrative privileges with the -OnlyRegisterLog switch to register those, although the script will still attempt registration without that switch and will display the error and continue if that fails.

Arguments

  • -Source — specifies the source file or directory to be backed up. Mandatory unless -OnlyRegisterLog is specified.
  • -Destination — specifies the path and filename of the final backup archive. Mandatory unless -OnlyRegisterLog is specified.
  • -7ZipPath — overrides the path to the 7z command. Optional, defaults to 7z, which looks in PATH.
  • -CompressionMethod — overrides the compression method. The default is to create a .7z archive (regardless of the extension) and to use multithreading. Optional, defaults to -t7z -mmt=on.
  • -AdditionalFlags — empty by default. Use to specify additional flags on top of -CompressionMethod, which overrides the defaults. Optional, defaults to nothing.
  • -EventLogName — overrides the name of the event log. Changing requires re-registration. Optional, defaults to Backup.
  • -EventLogSource — overrides the name of the event source. You could use this to distinguish different backups. Changing requires re-registration. Optional, defaults to Backup-Location.
  • -OnlyRegisterLog — if passed, registers the event log and source and exits without performing a backup. Optional, disabled by default.

License

Snatch this without credit if you want, I couldn't care less. Just don't tell people you made this, you'd look really stupid, and writing 122 whole lines of PowerShell code is not exactly a huge achievement, especially if you're not the one who did it. For legal purposes, consider this in the public domain or under the Unlicense or something.

param(
[string] $Source = $null,
[string] $Destination = $null,
[string] $7ZipPath = "7z",
[string] $CompressionMethod = "-t7z -mmt=on",
[string] $AdditionalFlags = "",
[string] $EventLogName = "Backup",
[string] $EventLogSource = "Backup-Location",
[switch] $OnlyRegisterLog
)
$LoggingEnabled = $true
function Check-IfEventSourceExists {
return $EventLogSource -in (Get-ChildItem HKLM:\SYSTEM\CurrentControlSet\Services\EventLog\$EventLogName).pschildname
}
function Setup-Logging {
if (-not $script:LoggingEnabled) { return }
# Searches for the event log name and checks for null
$EventLogExists = $null -ne (Get-EventLog -List | where { $_.Log -eq $EventLogName })
# We need to create the log-source pair if the event log doesn't exist, or if it exists but the source doesn't
$CreationNeeded = `
-not $EventLogExists `
-or ($EventLogExists -and -not (script:Check-IfEventSourceExists))
if ($CreationNeeded) {
# Creates both the log and the source, capturing the error
New-EventLog -LogName $EventLogName -Source $EventLogSource -ErrorVariable e -ErrorAction SilentlyContinue
if ($e) {
$script:LoggingEnabled = $false
Write-Error "Could not create event log or source: $e"
}
}
}
function Log {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0)] [System.Diagnostics.EventLogEntryType] $EntryType,
[Parameter(Mandatory = $true, Position = 1)] [Int32] $EventId,
[Parameter(Mandatory = $true, Position = 2)] [string] $Message
)
if (-not $LoggingEnabled) { return }
Write-EventLog `
-LogName $EventLogName `
-Source $EventLogSource `
-EntryType $EntryType `
-EventId $EventId `
-Message $Message `
-ErrorAction SilentlyContinue
}
function Check-IfFileExists([string] $Path) {
Get-Item $Path -ErrorVariable e -ErrorAction SilentlyContinue > $null
return -not $e
}
function SwapIn-Temporary([switch] $Older) {
Move-Item -Path "$Destination.tmp" -Destination $Destination -ErrorVariable e -ErrorAction SilentlyContinue
if ($e) {
if ($Older) {
$O = "older"
$N = "old"
} else {
$O = "old"
$N = "new"
}
script:Log Error 2 "Failed to replace the $O backup at $Destination with the $N one: $e. The $N backup is available at $Destination.tmp."
throw "Failed to replace backup: $e"
}
}
script:Setup-Logging
if ($OnlyRegisterLog) { return }
if (-not $Source) {
throw "Source must be specified if -OnlyRegisterLog is not passed"
}
if (-not $Destination) {
throw "Destination must be specified if -OnlyRegisterLog is not passed"
}
if (Check-IfFileExists -Path "$Destination.tmp") {
script:Log Warning 4 "Found $Destination.tmp from an old failed backup, renaming to $Destination."
Write-Output "Found an old failed backup."
SwapIn-Temporary -Older
}
Start-Process `
-WindowStyle Hidden `
-Wait `
-FilePath $7ZipPath `
-ArgumentList "a $Destination.tmp $CompressionMethod $AdditionalFlags -- $Source" `
-ErrorAction SilentlyContinue `
-ErrorVariable e
if ($e) {
script:Log Error 1 "Failed to back up $Source into $Destination`: $e"
Write-Error "Backup failed: $e"
Remove-Item $Destination.tmp -ErrorAction Ignore
return
}
$PreviousBackupExists = Check-IfFileExists -Path $Destination
if ($PreviousBackupExists) {
Remove-Item $Destination -ErrorVariable e -ErrorAction SilentlyContinue
if (-not $e) {
script:Log Information 1 "Deleted previous backup at $Destination."
Write-Output "Deleted previous backup."
} else {
script:Log Error 3 "Failed to delete previous backup at $Destination`: $e. The new backup is available at $Destination.tmp."
Write-Output "Failed to delete previous backup: $e"
return
}
}
SwapIn-Temporary
script:Log Information 0 "Successfully backed up $Source into $Destination."
Write-Output "Successfully backed up."
Copy link

ghost commented Dec 10, 2021

:ferrisOwO:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment