Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9241c0744 | ||
|
|
649d8dc119 | ||
|
|
7ba3db056a | ||
|
|
6244dbc910 | ||
|
|
033bdb3afd | ||
|
|
d7bc684e33 | ||
|
|
ce170f0685 | ||
|
|
efd7c78f59 | ||
|
|
463d1c9d42 | ||
|
|
1af63b87e2 | ||
|
|
71b7bd1407 | ||
|
|
916c11d20f | ||
|
|
51a35ef109 | ||
|
|
a0de49b804 | ||
|
|
f68ff68ae2 | ||
|
|
eaf97cd2d7 | ||
|
|
7c0111308a | ||
|
|
ec212218c8 | ||
|
|
bc2969514c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@ local.exclude
|
||||
logs
|
||||
restic.exe
|
||||
secrets.ps1
|
||||
config.ps1
|
||||
state.xml
|
||||
testing
|
||||
restic.exe.bak
|
||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,5 +1,33 @@
|
||||
# Changelog
|
||||
|
||||
## [1.8](https://github.com/kmwoley/restic-windows-backup/tree/1.8) (2025-02-20)
|
||||
[Full Changelog](https://github.com/kmwoley/restic-windows-backup/compare/1.7.1...1.8)
|
||||
|
||||
## Summary
|
||||
* New features
|
||||
* Added `update.ps1` which makes updating `restic-windows-backup` installations easier.
|
||||
* Added the ability to run custom actions at the start and end of the script execution. Can be used to invoke healthchecks or run custom scripts. Look at `config_sample.ps1` for examples.
|
||||
|
||||
* Bug fixes
|
||||
* Explicitly test the backup source media for VSS support instead of assuming it is or is not supported
|
||||
* Install script sets Task Scheduler user LogonType correctly, fixing #40
|
||||
* Error checking of restic.exe results fixed (was broken by release 1.7.1)
|
||||
|
||||
## [1.7.1](https://github.com/kmwoley/restic-windows-backup/tree/1.7.1) (2025-02-03)
|
||||
[Full Changelog](https://github.com/kmwoley/restic-windows-backup/compare/1.7...1.7.1)
|
||||
|
||||
## Summary
|
||||
* (Optionally) prevent backup & maintenance while on a metered network connection. By default, backups will occur while on a metered network connection. To disable backups over metered network connections, set `$BackupOnMeteredNetwork = $false` in `config.ps1`
|
||||
* Added `$GlobalParameters = @()` configuration variable, which will apply additional configuration parameters every time `restic.exe` is run. This is useful to add options for different types of backend targets.
|
||||
* Added `$SelfUpdateEnabled = $true` configuration variable, which can be used to disable `restic.exe` from automatically updating to the latest version when maintenance is run. To disable self update, set `$SelfUpdateEnabled = $false` in `config.ps1`
|
||||
|
||||
## What's Changed
|
||||
* Add optional configuration options for additional parameters to resti… by @woelfisch in https://github.com/kmwoley/restic-windows-backup/pull/96
|
||||
* Add feature to control backups on metered connections by @innovara in https://github.com/kmwoley/restic-windows-backup/pull/108
|
||||
|
||||
## New Contributors
|
||||
* @woelfisch made their first contribution in https://github.com/kmwoley/restic-windows-backup/pull/96
|
||||
|
||||
## [1.7](https://github.com/kmwoley/restic-windows-backup/tree/1.7) (2025-01-25)
|
||||
[Full Changelog](https://github.com/kmwoley/restic-windows-backup/compare/1.6...1.7)
|
||||
|
||||
|
||||
67
README.md
67
README.md
@@ -6,6 +6,7 @@ Simplifies the process of installation and running daily backups.
|
||||
* **VSS (Volume Snapshot Service) support** - backup everything, don't worry about what files are open/in-use
|
||||
* **Removable, External Drives** - drives can be identified by their volume labels or serial numbers, making it easy to backup drives that occasionally aren't there or change drive letter.
|
||||
* **Easy Installation** - `install.ps1` script downloads Restic, initializes the restic repository, and setups up a Windows Task Scheduler task to run the backup daily
|
||||
* **Easy to update** - `update.ps1` script can be used to keep your scripts up to date with the latest release on GitHub
|
||||
* **Backup, Maintenance and Monitoring are Automated** - `backup.ps1` script handles
|
||||
* Emailing the results of each execution, including log files when there are problems
|
||||
* Runs routine maintenence (pruning and checking the repo for errors on a regular basis)
|
||||
@@ -13,26 +14,43 @@ Simplifies the process of installation and running daily backups.
|
||||
|
||||
# Installation Instructions
|
||||
|
||||
1. Create your restic repository
|
||||
1. This is up to you to sort out where you want the data to go to. *Minio, B2, S3, oh my.*
|
||||
1. Install Scripts
|
||||
1. **Create your restic repository**
|
||||
1. This is up to you to sort out where you want the data to go to. *Minio, B2, S3, etc.*. Refer to the restic documents about how to create your repository.
|
||||
1. **Install the scripts**
|
||||
1. Create script directory: `C:\restic`
|
||||
1. Download scripts from https://github.com/kmwoley/restic-windows-backup, and unzip them into `C:\restic`
|
||||
1. Download scripts using the `update.ps1` script.
|
||||
1. Open PowerShell
|
||||
1. Change your working directory to the installation directory
|
||||
```
|
||||
cd c:\restic
|
||||
```
|
||||
1. Run the `update.ps1` script:
|
||||
```
|
||||
Invoke-Expression (Invoke-WebRequest -Uri "https://raw.githubusercontent.com/kmwoley/restic-windows-backup/main/update.ps1" -UseBasicParsing).Content
|
||||
```
|
||||
*Alternatively, you can download the scripts from this repository and and unzip them into `C:\restic`*
|
||||
1. Launch PowerShell as Administrator
|
||||
1. Change your working directory to `C:\restic`
|
||||
1. If you haven't done so in the past, set your Powershell script [execution policy](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.1) to allow for scripts to run. For example, `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned` is a good default.
|
||||
1. If you haven't done so in the past, set your Powershell script [execution policy](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.1) to allow for scripts to run. For example, this is a good default:
|
||||
```
|
||||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
|
||||
```
|
||||
1. Depending on the policy you choose, may need to 'unblock' the execution of the scripts you download by running `Unblock-File *.ps1`
|
||||
1. Create `secrets.ps1` file
|
||||
1. The secrets file contains location and passwords for your restic repository.
|
||||
1. `secrets_template.ps1` is a template for the `secrets.ps1` file - copy or rename this file to `secrets.ps1` and edit.
|
||||
1. restic will pick up the repo destination from the environment variables you set in this file - see this doc for more information about configuring restic repos https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html
|
||||
1. Email sending configuration is also contained with this file. The scripts assume you want to get emails about the success/failure of each backup attempt.
|
||||
1. Create `secrets.ps1` file. The secrets file contains location and passwords for your restic repository.
|
||||
1. `secrets_sample.ps1` is an example of the `secrets.ps1` file. Copy or rename this file to `secrets.ps1` and edit.
|
||||
1. Restic will pick up the repo destination from the environment variables you set in this file - see this doc for more information about configuring restic repos https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html
|
||||
1. Email sending configuration is also contained with this file. The scripts are able to send email about the success/failure of each backup attempt.
|
||||
1. Create `config.ps1` file. The config file contains the settings that control how the script runs backups, forgets snapshots, and prunes the restic repository. It's important that you configure this file to meet your needs since it will be backing up and maintaining your repository.
|
||||
1. `config_sample.ps1` contins an example configuration file. Copy or rename this file to `config.ps1` and edit to suit your needs.
|
||||
1. Add your `$BackupSources` to `config.ps1`
|
||||
1. By default, all of `C:\` will be backed up. You can add multiple root drives to be backed up. And you can define only specific folders you would like backed up.
|
||||
1. External, removable disk drives (i.e. USB hard drives) can be identified by their Volume Label, Serial Number, or Device Name. For example, if you have an external device with the Volume Label "MY BOOK", you can define a backup source as `$BackupSources["MY BOOK"]=@()`. It is recommended to use the device serial number to identify external drives to backup, which you can find using the Powershell `get-disk` command. You may also want to set `$IgnoreMissingBackupSources=$true` to avoid seeing errors when the removable drive is not present.
|
||||
1. Review all of the default settings in `config.ps1`.
|
||||
1. Most of the defaults are safe, but you should be sure restic is configured to meet your specifics needs.
|
||||
1. **Warning** - if you're using a shared restic repository across multiple machines, pay close attention to the `$SnapshotRetentionPolicy` settings to be sure this script does not intentionally destroy backup data in your repository.
|
||||
1. Run `install.ps1` file
|
||||
1. From the elevated (Run as Administrator) Powershell window, run `.\install.ps1`
|
||||
1. This will initialize the repo, create your logfile directory, create a scheduled task in Windows Task Scheduler to run the task daily, and install Send-MailKitMessage module.
|
||||
1. Add your `$BackupSources` to `config.ps1`
|
||||
1. By default, all of `C:\` will be backed up. You can add multiple root drives to be backed up. And you can define only specific folders you would like backed up.
|
||||
1. External, removable disk drives (i.e. USB hard drives) can be identified by their Volume Label, Serial Number, or Device Name. For example, if you have an external device with the Volume Label "MY BOOK", you can define a backup source as `$BackupSources["MY BOOK"]=@()`. I would recommend using the device serial number to identify external drives to backup, which you can find using the Powershell `get-disk` command. You may also want to set `$IgnoreMissingBackupSources=$true` to avoid seeing errors when the removable drive is not present.
|
||||
1. Add files/paths not to backup to `local.exclude`
|
||||
1. If you don't want to modify the included exclude file, you can add any files/paths you want to exclude from the backup to `local.exclude`
|
||||
1. Add `C:\restic\restic.exe` to the Windows Defender / Virus & Threat Detection Exclude list
|
||||
@@ -48,6 +66,29 @@ Simplifies the process of installation and running daily backups.
|
||||
1. `& $ResticExe find -i "*filename*"`
|
||||
1. `& $ResticExe restore ...`
|
||||
|
||||
## Updating restic-windows-backup
|
||||
|
||||
Use `update.ps1` to update the installed `restic-windows-backup` scripts to the latest release.
|
||||
|
||||
1. Open PowerShell (no need to be Administrator)
|
||||
1. Change directory to your installation directory (e.g. `c:\restic`)
|
||||
1. Run `update.ps1`
|
||||
|
||||
### `update.ps1` Details
|
||||
|
||||
Running `update.ps1` without any parameters will check for a new release from `kmwoley/restic-windows-backup`. If there is a newer release, the script will overwrite the local files in the script directory with the updated scripts.
|
||||
|
||||
* The script will not overwrite your local configuration files (i.e. `config.ps1` or `secrets.ps1`).
|
||||
* Any custom files created in the installation directory will not be deleted or modified (e.g. any custom action scripts, log files, etc.)
|
||||
* The script will warn before overwriting any files that have been changed since the last installation.
|
||||
* When `update.ps1` is run the first time, it will prompt before overwriting (since it may not know the current version of the fiels installed).
|
||||
|
||||
### `update.ps1` Options
|
||||
|
||||
* `-Mode <release | branch> (Default: release)` - change if the script updates from the latest release or a branch of `kmwoley/restic-windows-backup`
|
||||
* `-Branch <branch> (Default: 'main')` - When in branch mode, this parameter controls which branch to install from. Defaults to the `main` branch.
|
||||
* `-InstallPath <directory>` - choose which directory to install the files into. Defaults to the directory that `update.ps1` is in.
|
||||
|
||||
## Backup over SFTP
|
||||
|
||||
You can use any restic repository type you like pretty easily. SFTP on Windows, however, can be particularly tricky given that these scripts execute as the SYSTEM user and need to have access to the .ssh keys. Here are some steps and tips to getting it working.
|
||||
|
||||
67
backup.ps1
67
backup.ps1
@@ -12,6 +12,9 @@ $ConfigScript = Join-Path $PSScriptRoot "config.ps1"
|
||||
|
||||
# =========== end configuration =========== #
|
||||
|
||||
# make LASTEXITCODE global to enable error checking for Invoke-Expression commands
|
||||
$global:LASTEXITCODE=0
|
||||
|
||||
# globals for state storage
|
||||
$Script:ResticStateRepositoryInitialized = $null
|
||||
$Script:ResticStateLastMaintenance = $null
|
||||
@@ -65,6 +68,20 @@ function Get-Drives {
|
||||
}
|
||||
}
|
||||
|
||||
# test the path's storage media for VSS support
|
||||
# returns $true if VSS is supported at the provided path
|
||||
function Test-VSSSupport {
|
||||
Param($test_path)
|
||||
|
||||
$drive_letter = Split-Path $test_path -Qualifier
|
||||
$volume = Get-WmiObject -Query "SELECT * FROM Win32_Volume WHERE DriveLetter = '$drive_letter'"
|
||||
$deviceID = ($volume.DeviceID -replace '.*(\{.*\}).*', '$1')
|
||||
### https://learn.microsoft.com/en-us/previous-versions/windows/desktop/vsswmi/win32-shadowvolumesupport
|
||||
$supportedVolumes = Get-WmiObject -Query "SELECT * FROM Win32_ShadowVolumeSupport WHERE __PATH LIKE '%$deviceID%'"
|
||||
|
||||
return ($null -ne $supportedVolumes)
|
||||
}
|
||||
|
||||
# restore backup state from disk
|
||||
function Get-BackupState {
|
||||
if(Test-Path $Script:StateFile) {
|
||||
@@ -80,9 +97,15 @@ function Invoke-Unlock {
|
||||
Param($SuccessLog, $ErrorLog)
|
||||
|
||||
$locks = Invoke-Expression "$Script:ResticExe list locks --no-lock -q 3>&1 2>> $ErrorLog"
|
||||
if($LASTEXITCODE) {
|
||||
"[[Unlock]] Warning: unable to list locks." | Tee-Object -Append $ErrorLog
|
||||
}
|
||||
if($locks.Length -gt 0) {
|
||||
# unlock the repository (assumes this machine is the only one that will ever use it)
|
||||
Invoke-Expression "$Script:ResticExe unlock 3>&1 2>> $ErrorLog | Out-File -Append $SuccessLog"
|
||||
if($LASTEXITCODE) {
|
||||
"[[Unlock]] Error - unable to unlock repository." | Tee-Object -Append $ErrorLog
|
||||
}
|
||||
"[[Unlock]] Repository was locked. Unlocking." | Tee-Object -Append $ErrorLog | Out-File -Append $SuccessLog
|
||||
Start-Sleep 120
|
||||
}
|
||||
@@ -128,7 +151,7 @@ function Invoke-Maintenance {
|
||||
# forget snapshots based upon the retention policy
|
||||
"[[Maintenance]] Start forgetting..." | Out-File -Append $SuccessLog
|
||||
Invoke-Expression "$Script:ResticExe forget $SnapshotRetentionPolicy 3>&1 2>> $ErrorLog | Out-File -Append $SuccessLog"
|
||||
if(-not $?) {
|
||||
if($LASTEXITCODE) {
|
||||
"[[Maintenance]] Forget operation completed with errors" | Tee-Object -Append $ErrorLog | Out-File -Append $SuccessLog
|
||||
$maintenance_success = $false
|
||||
}
|
||||
@@ -137,7 +160,7 @@ function Invoke-Maintenance {
|
||||
# `forget` only prunes when it detects removed snapshots upon invocation, not previously removed
|
||||
"[[Maintenance]] Start pruning..." | Out-File -Append $SuccessLog
|
||||
Invoke-Expression "$Script:ResticExe prune $SnapshotPrunePolicy 3>&1 2>> $ErrorLog | Out-File -Append $SuccessLog"
|
||||
if(-not $?) {
|
||||
if($LASTEXITCODE) {
|
||||
"[[Maintenance]] Prune operation completed with errors" | Tee-Object -Append $ErrorLog | Out-File -Append $SuccessLog
|
||||
$maintenance_success = $false
|
||||
}
|
||||
@@ -164,8 +187,8 @@ function Invoke-Maintenance {
|
||||
}
|
||||
|
||||
Invoke-Expression "$Script:ResticExe check $data_check 3>&1 2>> $ErrorLog | Out-File -Append $SuccessLog"
|
||||
if(-not $?) {
|
||||
"[[Maintenance]] Check completed with errors" | Tee-Object -Append $ErrorLog | Tee-Object -Append $SuccessLog | Write-Host
|
||||
if($LASTEXITCODE) {
|
||||
"[[Maintenance]] Data check completed with errors" | Tee-Object -Append $ErrorLog | Tee-Object -Append $SuccessLog | Write-Host
|
||||
$maintenance_success = $false
|
||||
}
|
||||
|
||||
@@ -175,7 +198,7 @@ function Invoke-Maintenance {
|
||||
# check for updated restic version
|
||||
"[[Maintenance]] Checking for new version of restic..." | Out-File -Append $SuccessLog
|
||||
Invoke-Expression "$Script:ResticExe self-update 3>&1 2>> $ErrorLog | Out-File -Append $SuccessLog"
|
||||
if(-not $?) {
|
||||
if($LASTEXITCODE) {
|
||||
"[[Maintenance]] Self-update of restic.exe completed with errors" | Tee-Object -Append $ErrorLog | Out-File -Append $SuccessLog
|
||||
$maintenance_success = $false
|
||||
}
|
||||
@@ -204,8 +227,6 @@ function Invoke-Backup {
|
||||
$root_path = $item.Key
|
||||
$tag = $item.Key
|
||||
|
||||
$vss_option = "--use-fs-snapshot"
|
||||
|
||||
# Test if root path is a valid path, if not assume it is an external drive identifier
|
||||
if(-not (Test-Path $root_path)) {
|
||||
# attempt to find a drive letter associated with the identifier provided
|
||||
@@ -228,13 +249,16 @@ function Invoke-Backup {
|
||||
continue
|
||||
}
|
||||
|
||||
# there is exactly one drive
|
||||
$root_path = Join-Path $drives[0].DriveLetter ""
|
||||
|
||||
# disable VSS / file system snapshot for external drives
|
||||
# TODO: would be best to just test for VSS compatibility on the drive, rather than assume it won't work
|
||||
$vss_option = $null
|
||||
}
|
||||
|
||||
# determine if VSS is supported by the drive
|
||||
$vss_option = $null
|
||||
if(Test-VSSSupport $root_path) {
|
||||
$vss_option = "--use-fs-snapshot"
|
||||
}
|
||||
|
||||
"[[Backup]] Start $(Get-Date) [$tag]" | Out-File -Append $SuccessLog
|
||||
|
||||
# build the list of folders to backup
|
||||
@@ -283,7 +307,7 @@ function Invoke-Backup {
|
||||
else {
|
||||
# Launch Restic
|
||||
Invoke-Expression "$Script:ResticExe backup $folder_list $vss_option --tag $tag --exclude-file=$WindowsExcludeFile --exclude-file=$LocalExcludeFile $AdditionalBackupParameters 3>&1 2>> $ErrorLog | Out-File -Append $SuccessLog"
|
||||
if(-not $?) {
|
||||
if($LASTEXITCODE) {
|
||||
"[[Backup]] Completed with errors" | Tee-Object -Append $ErrorLog | Tee-Object -Append $SuccessLog | Write-Host
|
||||
$return_value = $false
|
||||
}
|
||||
@@ -531,6 +555,11 @@ function Invoke-Main {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# custom start action
|
||||
if($null -ne $CustomActionStart) {
|
||||
Invoke-Expression $CustomActionStart
|
||||
}
|
||||
|
||||
$error_count = 0
|
||||
$backup_success = $false
|
||||
$maintenance_success = $false
|
||||
@@ -656,6 +685,20 @@ function Invoke-Main {
|
||||
}
|
||||
}
|
||||
|
||||
# custom end actions
|
||||
if((-not $backup_success) -or ($maintenance_needed -and -not $maintenance_success)) {
|
||||
# call the custom error action if backup failed and/or maintenance was needed and failed
|
||||
if($null -ne $CustomActionEndError) {
|
||||
Invoke-Expression $CustomActionEndError
|
||||
}
|
||||
}
|
||||
else {
|
||||
# call custom success action if backup & maintenance were successful
|
||||
if($null -ne $CustomActionEndSuccess) {
|
||||
Invoke-Expression $CustomActionEndSuccess
|
||||
}
|
||||
}
|
||||
|
||||
# Save state to file
|
||||
Set-BackupState
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# Sample configuration file
|
||||
# Update this file to control how the restic backup, forget, and purge operations are run
|
||||
# Rename to `config.ps1`
|
||||
|
||||
# general configuration
|
||||
$InstallPath = "C:\restic"
|
||||
$ExeName = "restic.exe"
|
||||
@@ -40,4 +44,23 @@ $SnapshotMaintenanceDays = 30
|
||||
$SnapshotDeepMaintenanceDays = 90
|
||||
|
||||
# restic.exe self update configuration
|
||||
$SelfUpdateEnabled = $true
|
||||
$SelfUpdateEnabled = $true
|
||||
|
||||
# (optional) custom actions
|
||||
# Define commands to pass to Invoke-Expression at script start and script end
|
||||
# note: Errors will only be reported if the script does not eventually succeed. Errors
|
||||
# from unsuccessful attempts to backup or maintain the repository will not result
|
||||
# in the custom error action being called unless all attempts to backup or maintain failed.
|
||||
$CustomActionStart = $null
|
||||
$CustomActionEndError = $null
|
||||
$CustomActionEndSuccess = $null
|
||||
|
||||
# Example: Calling a healthcheck remote service
|
||||
# $healthCheckURL = "https://healthcheckservice.com/etc/etc"
|
||||
# $CustomActionStart = "Invoke-RestMethod $healthCheckURL/start"
|
||||
# $CustomActionEndError = "Invoke-RestMethod $healthCheckURL/fail"
|
||||
# $CustomActionEndSuccess = "Invoke-RestMethod $healthCheckURL"
|
||||
|
||||
# Example: Invoking a script
|
||||
# $successScript = Join-Path $InstallPath "mySuccessScript.ps1"
|
||||
# $CustomActionEndSuccess = "& $successScript"
|
||||
32
install.ps1
32
install.ps1
@@ -20,6 +20,9 @@ $ConfigScript = Join-Path $PSScriptRoot "config.ps1"
|
||||
$ResticExe = Join-Path $InstallPath $ExeName
|
||||
$LogPath = Join-Path $InstallPath "logs"
|
||||
|
||||
# make LASTEXITCODE global to enable error checking for Invoke-Expression commands
|
||||
$global:LASTEXITCODE=0
|
||||
|
||||
# =========== end configuration =========== #
|
||||
|
||||
# download restic
|
||||
@@ -31,11 +34,17 @@ if(-not (Test-Path $ResticExe)) {
|
||||
else {
|
||||
$url = "https://github.com/restic/restic/releases/download/v0.17.3/restic_0.17.3_windows_386.zip"
|
||||
}
|
||||
$output = Join-Path $InstallPath "restic.zip"
|
||||
Invoke-WebRequest -Uri $url -OutFile $output
|
||||
Expand-Archive -LiteralPath $output $InstallPath
|
||||
Remove-Item $output
|
||||
Get-ChildItem *.exe | Rename-Item -NewName $ExeName
|
||||
try {
|
||||
$output = Join-Path $InstallPath "restic.zip"
|
||||
Invoke-WebRequest -Uri $url -OutFile $output
|
||||
Expand-Archive -LiteralPath $output $InstallPath
|
||||
Remove-Item $output
|
||||
Get-ChildItem *.exe | Rename-Item -NewName $ExeName
|
||||
}
|
||||
catch {
|
||||
Write-Error "[[Install]] restic.exe download failed. Check errors and resolve: $_"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Apply global paramters to $ResticExe, after the $ResticExe has been downloaded/confirmed to exist
|
||||
@@ -47,6 +56,9 @@ if(-not [String]::IsNullOrEmpty($GlobalParameters)) {
|
||||
# This is enabled by default unless configuration disables self-update
|
||||
if ([String]::IsNullOrEmpty($SelfUpdateEnabled) -or ($SelfUpdateEnabled -eq $true)) {
|
||||
Invoke-Expression "$ResticExe self-update"
|
||||
if($LASTEXITCODE) {
|
||||
Write-Warning "[[Update]] Restic self-update failed. Check errors and resolve."
|
||||
}
|
||||
}
|
||||
|
||||
# Create log directory if it doesn't exit
|
||||
@@ -62,11 +74,11 @@ if(-not (Test-Path $LocalExcludeFile)) {
|
||||
|
||||
# Initialize the restic repository
|
||||
Invoke-Expression "$ResticExe --verbose init"
|
||||
if($?) {
|
||||
Write-Output "[[Init]] Repository successfully initialized."
|
||||
if($LASTEXITCODE) {
|
||||
Write-Warning "[[Init]] Repository initialization failed. Check errors and resolve."
|
||||
}
|
||||
else {
|
||||
Write-Warning "[[Init]] Repository initialization failed. Check errors and resolve."
|
||||
Write-Output "[[Init]] Repository successfully initialized."
|
||||
}
|
||||
|
||||
# Scheduled Windows Task Scheduler to run the backup
|
||||
@@ -75,14 +87,14 @@ $backup_task = Get-ScheduledTask $backup_task_name -ErrorAction SilentlyContinue
|
||||
if($null -eq $backup_task) {
|
||||
try {
|
||||
$task_action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-ExecutionPolicy Bypass -NonInteractive -NoLogo -NoProfile -Command ".\backup.ps1; exit $LASTEXITCODE"' -WorkingDirectory $InstallPath
|
||||
$task_user = New-ScheduledTaskPrincipal -UserId "NT AUTHORITY\SYSTEM" -RunLevel Highest
|
||||
$task_user = New-ScheduledTaskPrincipal -UserId "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
||||
$task_settings = New-ScheduledTaskSettingsSet -RestartCount 4 -RestartInterval (New-TimeSpan -Minutes 15) -ExecutionTimeLimit (New-TimeSpan -Days 3) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -DontStopOnIdleEnd -MultipleInstances IgnoreNew -IdleDuration 0 -IdleWaitTimeout 0 -StartWhenAvailable -RestartOnIdle
|
||||
$task_trigger = New-ScheduledTaskTrigger -Daily -At 4:00am
|
||||
Register-ScheduledTask $backup_task_name -Action $task_action -Principal $task_user -Settings $task_settings -Trigger $task_trigger | Out-Null
|
||||
Write-Output "[[Scheduler]] Backup task scheduled."
|
||||
}
|
||||
catch {
|
||||
Write-Warning "[[Scheduler]] Scheduling failed."
|
||||
Write-Error "[[Scheduler]] Setting up backup task schedule failed: $_"
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
337
update.ps1
Normal file
337
update.ps1
Normal file
@@ -0,0 +1,337 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Updates the local installed restic backup scripts from GitHub,
|
||||
either using the latest tagged release or by targeting a specific branch.
|
||||
|
||||
.DESCRIPTION
|
||||
This script supports two modes:
|
||||
|
||||
1. **Release mode (default):**
|
||||
- Fetches the latest release info via GitHub’s API.
|
||||
- Compares the release tag (after normalization) against a locally stored version (in state.xml).
|
||||
- If the GitHub release is newer, downloads the release zip, extracts it, copies the files
|
||||
over the local installation.
|
||||
|
||||
2. **Branch mode:**
|
||||
- Targets a specific branch (default "main") by retrieving branch information from GitHub.
|
||||
- Compares the latest commit SHA on that branch against a locally stored SHA (in state.xml).
|
||||
- If the remote commit SHA differs, downloads the branch zip archive, extracts it,
|
||||
copies the files over the local installation.
|
||||
|
||||
.NOTES
|
||||
Example 1 - update scripts to the latest tagged release
|
||||
.\update.ps1
|
||||
|
||||
Example 2 - update scripts from a branch
|
||||
.\update.ps1 -Mode branch -BranchName 'release_1.8'
|
||||
|
||||
Example 3 - download a new copy of the update scripts and run it
|
||||
1. Change your directory to your installation directory (e.g. `cd c:\restic`)
|
||||
2. Invoke-WebRequest "https://raw.githubusercontent.com/kmwoley/restic-windows-backup/main/update.ps1" -OutFile update.ps1
|
||||
3. .\update.ps1
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateSet("release", "branch")]
|
||||
[string]$Mode = "release",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$BranchName = "main",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$InstallPath = $null
|
||||
)
|
||||
|
||||
# ====================================
|
||||
# Configuration and Setup
|
||||
# ====================================
|
||||
|
||||
# GitHub repository details
|
||||
$repoOwner = "kmwoley"
|
||||
$repoName = "restic-windows-backup"
|
||||
|
||||
# User-Agent header (GitHub requires this)
|
||||
$headers = @{ "User-Agent" = "PowerShell" }
|
||||
|
||||
# default the installation directory to the location of the running script
|
||||
if([string]::IsNullOrEmpty($InstallPath)) {
|
||||
# default to the script's location, if running as a script
|
||||
$InstallPath = $PSScriptRoot
|
||||
if([string]::IsNullOrEmpty($InstallPath)) {
|
||||
# default to the current working directory, if not running as a script
|
||||
$InstallPath = Get-Location
|
||||
}
|
||||
}
|
||||
|
||||
# ====================================
|
||||
# Functions for state management
|
||||
# ====================================
|
||||
function Get-State {
|
||||
if(Test-Path $Script:StateFile) {
|
||||
Import-Clixml $Script:StateFile | ForEach-Object{ Set-Variable -Scope Script $_.Name $_.Value }
|
||||
}
|
||||
}
|
||||
function Set-State {
|
||||
Get-Variable ResticState* | Export-Clixml $Script:StateFile
|
||||
}
|
||||
|
||||
# ===========================================
|
||||
# Functions for file management and download
|
||||
# ===========================================
|
||||
function Get-ModifiedFiles {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Source,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Destination,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$DateTime
|
||||
)
|
||||
|
||||
$modifiedFiles = New-Object System.Collections.Generic.List[System.Object]
|
||||
|
||||
if(-not (Test-Path $Source)) {
|
||||
Write-Error "Source does not exist ($Source)"
|
||||
exit 1
|
||||
}
|
||||
if(-not (Test-Path $Destination)) {
|
||||
Write-Error "Destination does not exist ($Destination)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$sourceFiles = Get-ChildItem $Source
|
||||
|
||||
ForEach ($sourceFile in $sourceFiles) {
|
||||
# find if there's a corrosponding file in the destination
|
||||
$destFileName = Join-Path $Destination $sourceFile.Name
|
||||
if(Test-Path $destFileName) {
|
||||
$destFile = Get-ChildItem $destFileName
|
||||
if($destFile.LastWriteTime -gt $DateTime) {
|
||||
# destination file has been modified after $DateTime
|
||||
$modifiedFiles.Add($destFile.FullName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return $modifiedFiles
|
||||
}
|
||||
|
||||
function Update-InstalledScripts {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$ZipUrl,
|
||||
[Parameter(Mandatory=$true)][string]$DestinationFolder
|
||||
)
|
||||
|
||||
$timestamp = Get-Date -Format FileDateTime
|
||||
$tempExtractDir = Join-Path $env:TEMP ("restic-windows-backup." + $timestamp)
|
||||
$tempZipPath = Join-Path $env:TEMP ("restic-windows-backup." + $timestamp + ".zip")
|
||||
|
||||
# test temp location, fail if in use
|
||||
if (Test-Path $tempExtractDir) {
|
||||
Write-Error "Temporary directory already exists: $tempExtractDir"
|
||||
exit 1
|
||||
}
|
||||
if (Test-Path $tempZipPath) {
|
||||
Write-Error "Temporary directory already exists: $tempZipPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Create a temporary folder for extraction
|
||||
New-Item -ItemType Directory -Path $tempExtractDir | Out-Null
|
||||
|
||||
Write-Host "Downloading from: $ZipUrl"
|
||||
try {
|
||||
Invoke-WebRequest -Uri $ZipUrl -OutFile $tempZipPath -Headers $headers
|
||||
} catch {
|
||||
Write-Error "Failed to download the file: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
try {
|
||||
Expand-Archive -LiteralPath $tempZipPath $tempExtractDir
|
||||
} catch {
|
||||
Write-Error "Error extracting zip file: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Determine the actual folder containing the repository files.
|
||||
$extractedContent = Get-ChildItem -Path $tempExtractDir | Where-Object { $_.PSIsContainer }
|
||||
if ($extractedContent.Count -eq 1) {
|
||||
$extractedFolder = $extractedContent[0].FullName
|
||||
} else {
|
||||
$extractedFolder = $tempExtractDir
|
||||
}
|
||||
|
||||
# Check to make sure not to overwrite modified files
|
||||
$installedDate = $Script:ResticStateInstalledDate
|
||||
if([string]::IsNullOrEmpty($installedDate)) {
|
||||
# unkown install date; setting the date
|
||||
$installedDate = [datetime]::MinValue
|
||||
}
|
||||
|
||||
$modifiedFiles = Get-ModifiedFiles -Source $extractedFolder -Destination $DestinationFolder -DateTime $installedDate
|
||||
if($modifiedFiles) {
|
||||
if([string]::IsNullOrEmpty($Script:ResticStateInstalledDate)) {
|
||||
Write-Host "WARNING: The following files already exist in the target directory"
|
||||
}
|
||||
else {
|
||||
Write-Host "WARNING: The following files have been modified since they were installed on $installedDate"
|
||||
}
|
||||
ForEach ($fileName in $modifiedFiles) {
|
||||
Write-Host " - " $fileName
|
||||
}
|
||||
|
||||
# TODO: add a "-Force" parameter to skip this check/question
|
||||
Write-Host "Continuing will overwrite these files."
|
||||
Write-host "Do you want to continue?"
|
||||
$userInput = Read-Host "[Y] Yes [N] No (default is ""Y"")"
|
||||
if ($userInput -ieq 'n') {
|
||||
Write-Host "Operation cancelled."
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Updating files in installation directory ($DestinationFolder)..."
|
||||
try {
|
||||
# Recursively copy all content from the extracted folder to the local directory.
|
||||
Copy-Item -Path (Join-Path $extractedFolder "*") -Destination $DestinationFolder -Recurse -Force
|
||||
} catch {
|
||||
Write-Error "Error copying files: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Clean up temporary files
|
||||
Remove-Item $tempZipPath -Force
|
||||
Remove-Item $tempExtractDir -Recurse -Force
|
||||
}
|
||||
|
||||
# ====================================
|
||||
# Main
|
||||
# ====================================
|
||||
|
||||
# load restic state
|
||||
$Script:ResticStateInstalledVersion = $null
|
||||
$Script:ResticStateInstalledBranchSHA = $null
|
||||
$Script:ResticStateInstalledDate = $null
|
||||
$Script:StateFile = Join-Path $InstallPath "state.xml"
|
||||
Get-State
|
||||
|
||||
# ====================================
|
||||
# Release mode
|
||||
# ====================================
|
||||
|
||||
if ($Mode -eq "release") {
|
||||
|
||||
# Read the version of the scripts installed
|
||||
$localVersion = $Script:ResticStateInstalledVersion
|
||||
if ([string]::IsNullOrEmpty($localVersion)) {
|
||||
# No version information stored locally
|
||||
$localVersion = "0.0.0"
|
||||
}
|
||||
|
||||
# Get the Latest Release Info from GitHub
|
||||
$releaseApiUrl = "https://api.github.com/repos/$repoOwner/$repoName/releases/latest"
|
||||
try {
|
||||
Write-Host "Checking GitHub for latest release of '$repoOwner/$repoName'..."
|
||||
$release = Invoke-RestMethod -Uri $releaseApiUrl -Headers $headers
|
||||
} catch {
|
||||
Write-Error "Error fetching release information from GitHub: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$latestTagRaw = $release.tag_name
|
||||
$latestTag = $latestTagRaw.Trim()
|
||||
|
||||
# Normalize versions (remove leading "v" if present)
|
||||
function Get-NormalizedVersion($versionString) {
|
||||
if ($versionString.StartsWith("v", [System.StringComparison]::InvariantCultureIgnoreCase)) {
|
||||
return $versionString.Substring(1)
|
||||
}
|
||||
return $versionString
|
||||
}
|
||||
$normalizedLocalVersion = Get-NormalizedVersion $localVersion
|
||||
$normalizedLatestVersion = Get-NormalizedVersion $latestTag
|
||||
|
||||
try {
|
||||
$localVersionObj = [Version]$normalizedLocalVersion
|
||||
$latestVersionObj = [Version]$normalizedLatestVersion
|
||||
} catch {
|
||||
Write-Error "Error parsing version strings. Local: $normalizedLocalVersion, Latest: $normalizedLatestVersion. $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($latestVersionObj -le $localVersionObj) {
|
||||
Write-Host "Installed version ($localVersionObj) is up-to-date. No update needed."
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host "Newer release available: $latestVersionObj (installed: $localVersionObj). Proceeding with update..."
|
||||
}
|
||||
|
||||
# get the zip URL from the release info
|
||||
$zipUrl = $release.zipball_url
|
||||
|
||||
# Download and update the installed scripts
|
||||
Update-InstalledScripts -ZipUrl $zipUrl -DestinationFolder $InstallPath
|
||||
|
||||
# Store the installed version number and time installed
|
||||
$Script:ResticStateInstalledVersion = $normalizedLatestVersion
|
||||
$Script:ResticStateInstalledDate = Get-Date
|
||||
$Script:ResticStateInstalledBranchSHA = $null
|
||||
Set-State
|
||||
|
||||
Write-Host "Update successful. Installed version is now $normalizedLatestVersion."
|
||||
}
|
||||
# ====================================
|
||||
# Branch mode
|
||||
# ====================================
|
||||
elseif ($Mode -eq "branch") {
|
||||
|
||||
# Read the SHA of the branch source installed
|
||||
$localCommitSHA = $Script:ResticStateInstalledBranchSHA
|
||||
if ([string]::IsNullOrEmpty($localCommitSHA)) {
|
||||
# Write-Host "No branch information stored locally."
|
||||
$localCommitSHA = "unknown"
|
||||
}
|
||||
|
||||
# Retrieve branch information from GitHub
|
||||
$branchApiUrl = "https://api.github.com/repos/$repoOwner/$repoName/branches/$BranchName"
|
||||
try {
|
||||
Write-Host "Checking GitHub for latest commit of '$repoOwner/$repoName' on branch '$BranchName'..."
|
||||
$branchInfo = Invoke-RestMethod -Uri $branchApiUrl -Headers $headers
|
||||
} catch {
|
||||
Write-Error "Error fetching branch information from GitHub: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$latestCommitSHA = $branchInfo.commit.sha
|
||||
|
||||
if ($localCommitSHA -eq $latestCommitSHA) {
|
||||
Write-Host "Installed commit ($latestCommitSHA) is up-to-date. No update needed."
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host "Latest commit: $latestCommitSHA (installed: $localCommitSHA). Proceeding with update..."
|
||||
}
|
||||
|
||||
# Construct the zip URL for the branch.
|
||||
# GitHub provides branch archives at:
|
||||
# https://github.com/{owner}/{repo}/archive/refs/heads/{branch}.zip
|
||||
$zipUrl = "https://github.com/$repoOwner/$repoName/archive/refs/heads/$BranchName.zip"
|
||||
|
||||
# Download and update the installed scripts
|
||||
Update-InstalledScripts -ZipUrl $zipUrl -DestinationFolder $InstallPath
|
||||
|
||||
# Store the installed branch commit SHA and time installed
|
||||
$Script:ResticStateInstalledVersion = $null
|
||||
$Script:ResticStateInstalledDate = Get-Date
|
||||
$Script:ResticStateInstalledBranchSHA = $latestCommitSHA
|
||||
Set-State
|
||||
|
||||
Write-Host "Update successful. Local branch is now at commit $latestCommitSHA."
|
||||
}
|
||||
else {
|
||||
Write-Error "Unsupported mode."
|
||||
exit 1
|
||||
}
|
||||
Reference in New Issue
Block a user