-
Notifications
You must be signed in to change notification settings - Fork 73
/
Copy pathupdate.ps1
337 lines (282 loc) · 11.5 KB
/
update.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
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
}