105 lines
3.4 KiB
PowerShell
105 lines
3.4 KiB
PowerShell
# Bulk, incremental backup of vault assets/threads/* to the configured remote.
|
|
#
|
|
# Reads [backup].root from secrets/credentials.toml. The remote path is never
|
|
# echoed or logged — handle it like any other secret.
|
|
#
|
|
# Behaviour: robocopy /E mirror (add or refresh, never delete). Re-runs only
|
|
# push deltas. assets/WIP/ is deliberately excluded — it is local-only.
|
|
#
|
|
# Robocopy exit codes we accept as success: 0 (no change), 1 (files pushed),
|
|
# 2 / 3 (extras on destination, kept). 4-7 are transient / partial — re-run
|
|
# after the warning. 8+ are hard fails.
|
|
|
|
[CmdletBinding()]
|
|
param()
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# Resolve paths relative to this script.
|
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
$Vault = (Resolve-Path (Join-Path $ScriptDir '..')).Path
|
|
$Source = Join-Path $Vault 'assets'
|
|
$ConfigFile = Join-Path $Vault 'secrets\config.toml'
|
|
|
|
if (-not (Test-Path -LiteralPath $Source)) {
|
|
Write-Error "source not found: $Source"
|
|
exit 2
|
|
}
|
|
if (-not (Test-Path -LiteralPath $ConfigFile)) {
|
|
Write-Error "config file not found: $ConfigFile"
|
|
exit 2
|
|
}
|
|
|
|
# Parse [backup].root from config.toml. Handles both TOML basic strings
|
|
# ("...", backslashes escaped) and literal strings ('...', backslashes literal).
|
|
$lines = Get-Content -LiteralPath $ConfigFile
|
|
$inBackup = $false
|
|
$rawValue = $null
|
|
foreach ($line in $lines) {
|
|
$trimmed = $line.Trim()
|
|
if ($trimmed -match '^\[(.+)\]$') {
|
|
$inBackup = ($Matches[1] -eq 'backup')
|
|
continue
|
|
}
|
|
if ($inBackup -and $trimmed -match '^root\s*=\s*(.*)$') {
|
|
$rawValue = $Matches[1].Trim()
|
|
break
|
|
}
|
|
}
|
|
if (-not $rawValue) {
|
|
Write-Error "[backup].root missing in $CredFile"
|
|
exit 2
|
|
}
|
|
|
|
$remote = $null
|
|
if ($rawValue.StartsWith("'") -and $rawValue.EndsWith("'") -and $rawValue.Length -ge 2) {
|
|
$remote = $rawValue.Substring(1, $rawValue.Length - 2)
|
|
} elseif ($rawValue.StartsWith('"') -and $rawValue.EndsWith('"') -and $rawValue.Length -ge 2) {
|
|
$inner = $rawValue.Substring(1, $rawValue.Length - 2)
|
|
try {
|
|
$remote = [System.Text.RegExp.Regex]::Unescape($inner)
|
|
} catch {
|
|
Write-Error "[backup].root has unparseable escapes"
|
|
exit 2
|
|
}
|
|
} else {
|
|
Write-Error "[backup].root must be a quoted TOML string"
|
|
exit 2
|
|
}
|
|
if (-not $remote) {
|
|
Write-Error "[backup].root is empty"
|
|
exit 2
|
|
}
|
|
|
|
# Reachability check — do not echo the path on failure.
|
|
if (-not (Test-Path -LiteralPath $remote)) {
|
|
Write-Error "destination unreachable (check [backup].root in credentials.toml)"
|
|
exit 3
|
|
}
|
|
|
|
# /E copy subdirs incl. empty
|
|
# /XD WIP exclude the local-only WIP directory
|
|
# /R:3 /W:5 retries with backoff
|
|
# /MT:4 multithread (conservative for UNC)
|
|
# /NP /NDL /NFL keep the log short — no progress, no directory list, no per-file list
|
|
#
|
|
# Robocopy's banner prints the destination path. Suppress ALL its output
|
|
# to keep the secret off stdout/stderr. Exit code is preserved.
|
|
robocopy $Source $remote /E /XD WIP /R:3 /W:5 /MT:4 /NP /NDL /NFL *>&1 | Out-Null
|
|
$rc = $LASTEXITCODE
|
|
|
|
switch ($rc) {
|
|
0 { Write-Host "backup ok (no change)" }
|
|
1 { Write-Host "backup ok (files pushed)" }
|
|
2 { Write-Host "backup ok (extras on destination, kept)" }
|
|
3 { Write-Host "backup ok (files pushed, extras on destination, kept)" }
|
|
{ 4 -le $_ -and $_ -le 7 } {
|
|
Write-Warning "backup partial (code $rc) — re-run to retry"
|
|
exit 0
|
|
}
|
|
default {
|
|
Write-Error "backup failed (code $rc)"
|
|
exit $rc
|
|
}
|
|
}
|