Files
ATOCore/scripts/windows/atocore-backup-pull.ps1
Anto01 33a6c61ca6 feat: daily backup to Windows main computer via pull-based scp
Third backup tier (after Dalidou local + T420 off-host): pull-based
backup to the user's Windows main computer.

- scripts/windows/atocore-backup-pull.ps1: PowerShell script using
  built-in OpenSSH scp. Fail-open: exits cleanly if Dalidou
  unreachable (e.g., laptop on the road). Pulls whole snapshots dir
  (~45MB, bounded by Dalidou's retention policy).
- docs/windows-backup-setup.md: Task Scheduler setup (automated +
  manual). Runs daily 10:00 local, catches up missed days via
  StartWhenAvailable, retries 2x on failure.

Verified: pulled 3 snapshots (45MB) to
C:\Users\antoi\Documents\ATOCore_Backups\. Task "AtoCore Backup
Pull" registered in Task Scheduler, State: Ready.

Three independent backup tiers now: Dalidou local, T420 off-host,
user Windows machine. Any two can fail without data loss.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 20:04:00 -04:00

88 lines
3.2 KiB
PowerShell

# atocore-backup-pull.ps1
#
# Pull the latest AtoCore backup snapshot from Dalidou to this Windows machine.
# Designed to be run by Windows Task Scheduler. Fail-open by design -- if
# Dalidou is unreachable (laptop on the road, etc.), exit cleanly without error.
#
# Usage (manual test):
# powershell.exe -ExecutionPolicy Bypass -File atocore-backup-pull.ps1
#
# Scheduled task: see docs/windows-backup-setup.md for Task Scheduler config.
$ErrorActionPreference = "Continue"
# --- Configuration ---
$Remote = "papa@dalidou"
$RemoteSnapshots = "/srv/storage/atocore/backups/snapshots"
$LocalBackupDir = "$env:USERPROFILE\Documents\ATOCore_Backups"
$LogDir = "$LocalBackupDir\_logs"
$ReachabilityTest = 5 # seconds timeout for SSH probe
# --- Setup ---
if (-not (Test-Path $LocalBackupDir)) {
New-Item -ItemType Directory -Path $LocalBackupDir -Force | Out-Null
}
if (-not (Test-Path $LogDir)) {
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
}
$Timestamp = Get-Date -Format "yyyy-MM-dd_HHmmss"
$LogFile = "$LogDir\backup-$Timestamp.log"
function Log($msg) {
$line = "[{0}] {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $msg
Write-Host $line
Add-Content -Path $LogFile -Value $line
}
Log "=== AtoCore backup pull starting ==="
Log "Remote: $Remote"
Log "Local target: $LocalBackupDir"
# --- Reachability check: fail open if Dalidou is offline ---
Log "Checking Dalidou reachability..."
$probe = & ssh -o ConnectTimeout=$ReachabilityTest -o BatchMode=yes `
-o StrictHostKeyChecking=accept-new `
$Remote "echo ok" 2>&1
if ($LASTEXITCODE -ne 0 -or $probe -ne "ok") {
Log "Dalidou unreachable ($probe) -- fail-open exit"
exit 0
}
Log "Dalidou reachable."
# --- Pull the entire snapshots directory ---
# Dalidou's retention policy (7 daily + 4 weekly + 6 monthly) already caps
# the snapshot count, so pulling the whole dir is bounded and simple. scp
# will overwrite local files -- we rely on this to pick up new snapshots.
Log "Pulling snapshots via scp..."
$LocalSnapshotsDir = Join-Path $LocalBackupDir "snapshots"
if (-not (Test-Path $LocalSnapshotsDir)) {
New-Item -ItemType Directory -Path $LocalSnapshotsDir -Force | Out-Null
}
& scp -o BatchMode=yes -r "${Remote}:${RemoteSnapshots}/*" "$LocalSnapshotsDir\" 2>&1 |
ForEach-Object { Add-Content -Path $LogFile -Value $_ }
if ($LASTEXITCODE -ne 0) {
Log "scp failed with exit $LASTEXITCODE"
exit 0 # fail-open
}
# --- Stats ---
$snapshots = Get-ChildItem -Path $LocalSnapshotsDir -Directory |
Where-Object { $_.Name -match "^\d{8}T\d{6}Z$" } |
Sort-Object Name -Descending
$totalSize = (Get-ChildItem $LocalSnapshotsDir -Recurse -File | Measure-Object -Property Length -Sum).Sum
$SizeMB = [math]::Round($totalSize / 1MB, 2)
$latest = if ($snapshots.Count -gt 0) { $snapshots[0].Name } else { "(none)" }
Log ("Pulled {0} snapshots successfully (total {1} MB, latest: {2})" -f $snapshots.Count, $SizeMB, $latest)
Log "=== backup complete ==="
# --- Log retention: keep last 30 log files ---
Get-ChildItem -Path $LogDir -Filter "backup-*.log" |
Sort-Object Name -Descending |
Select-Object -Skip 30 |
ForEach-Object { Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue }