From 33a6c61ca60a4da1931420417247be54768e393c Mon Sep 17 00:00:00 2001 From: Anto01 Date: Thu, 16 Apr 2026 20:04:00 -0400 Subject: [PATCH] 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) --- docs/windows-backup-setup.md | 140 ++++++++++++++++++++++++ scripts/windows/atocore-backup-pull.ps1 | 87 +++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 docs/windows-backup-setup.md create mode 100644 scripts/windows/atocore-backup-pull.ps1 diff --git a/docs/windows-backup-setup.md b/docs/windows-backup-setup.md new file mode 100644 index 0000000..10f18db --- /dev/null +++ b/docs/windows-backup-setup.md @@ -0,0 +1,140 @@ +# Windows Main-Computer Backup Setup + +The AtoCore backup pipeline runs nightly on Dalidou and already pushes snapshots +off-host to the T420 (`papa@192.168.86.39`). This doc sets up a **second**, +pull-based daily backup to your Windows main computer at +`C:\Users\antoi\Documents\ATOCore_Backups\`. + +Pull-based means the Windows machine pulls from Dalidou. This is simpler than +push because Dalidou doesn't need SSH keys to reach Windows, and the backup +only runs when the Windows machine is powered on and can reach Dalidou. + +## Prerequisites + +- Windows 10/11 with OpenSSH client (built-in since Win10 1809) +- SSH key-based auth to `papa@dalidou` already working (you're using it today) +- `C:\Users\antoi\ATOCore\scripts\windows\atocore-backup-pull.ps1` present + +## Test the script manually + +```powershell +powershell.exe -ExecutionPolicy Bypass -File ` + C:\Users\antoi\ATOCore\scripts\windows\atocore-backup-pull.ps1 +``` + +Expected output: +``` +[timestamp] === AtoCore backup pull starting === +[timestamp] Dalidou reachable. +[timestamp] Pulling snapshots via scp... +[timestamp] Pulled N snapshots successfully (total X MB, latest: ...) +[timestamp] === backup complete === +``` + +Target directory: `C:\Users\antoi\Documents\ATOCore_Backups\snapshots\` +Logs: `C:\Users\antoi\Documents\ATOCore_Backups\_logs\backup-*.log` + +## Register the Task Scheduler task + +### Option A — automatic registration (recommended) + +Run this PowerShell command **as your user** (no admin needed — uses HKCU task): + +```powershell +$action = New-ScheduledTaskAction -Execute 'powershell.exe' ` + -Argument '-ExecutionPolicy Bypass -NonInteractive -WindowStyle Hidden -File C:\Users\antoi\ATOCore\scripts\windows\atocore-backup-pull.ps1' + +# Run daily at 10:00 local time; if missed (computer off), run at next logon +$trigger = New-ScheduledTaskTrigger -Daily -At 10:00AM +$trigger.StartBoundary = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ss') + +$settings = New-ScheduledTaskSettingsSet ` + -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries ` + -StartWhenAvailable ` + -ExecutionTimeLimit (New-TimeSpan -Minutes 10) ` + -RestartCount 2 ` + -RestartInterval (New-TimeSpan -Minutes 30) + +Register-ScheduledTask -TaskName 'AtoCore Backup Pull' ` + -Description 'Daily pull of AtoCore backup snapshots from Dalidou' ` + -Action $action -Trigger $trigger -Settings $settings ` + -User $env:USERNAME +``` + +Key settings: +- `-StartWhenAvailable`: if the computer was off at 10:00, run as soon as it + comes online +- `-AllowStartIfOnBatteries`: works on laptop battery too +- `-ExecutionTimeLimit 10min`: kill hung tasks +- `-RestartCount 2`: retry twice if it fails (Dalidou temporarily unreachable) + +### Option B -- Task Scheduler GUI + +1. Open Task Scheduler (`taskschd.msc`) +2. Create Basic Task -> name: `AtoCore Backup Pull` +3. Trigger: Daily, 10:00 AM, recur every 1 day +4. Action: Start a program + - Program: `powershell.exe` + - Arguments: `-ExecutionPolicy Bypass -NonInteractive -WindowStyle Hidden -File "C:\Users\antoi\ATOCore\scripts\windows\atocore-backup-pull.ps1"` +5. Finish, then edit the task: + - Settings tab: check "Run task as soon as possible after a scheduled start is missed" + - Settings tab: "If the task fails, restart every 30 minutes, up to 2 times" + - Conditions tab: uncheck "Start only if computer is on AC power" (if you want it on battery) + +## Verify + +After the first scheduled run: + +```powershell +# Most recent log +Get-ChildItem C:\Users\antoi\Documents\ATOCore_Backups\_logs\ | + Sort-Object Name -Descending | + Select-Object -First 1 | + Get-Content + +# Latest snapshot present? +Get-ChildItem C:\Users\antoi\Documents\ATOCore_Backups\snapshots\ | + Sort-Object Name -Descending | + Select-Object -First 3 +``` + +## Unregister (if needed) + +```powershell +Unregister-ScheduledTask -TaskName 'AtoCore Backup Pull' -Confirm:$false +``` + +## How it behaves + +- **Computer on, Dalidou reachable**: pulls latest snapshots silently in ~15s +- **Computer on, Dalidou unreachable** (remote work, network down): fail-open, + exits without error, logs "Dalidou unreachable" +- **Computer off at scheduled time**: Task Scheduler runs it as soon as the + computer wakes up +- **Many days off**: one run catches up; scp only transfers files not already + present (snapshots are date-stamped directories, idempotent overwrites) + +## What gets backed up + +The snapshots tree contains: +- `YYYYMMDDTHHMMSSZ/config/` — project registry, AtoCore config +- `YYYYMMDDTHHMMSSZ/db/` — SQLite snapshot of all memory, state, interactions +- `YYYYMMDDTHHMMSSZ/backup-metadata.json` — SHA, timestamp, source info + +Chroma vectors are **not** in the snapshot by default +(`ATOCORE_BACKUP_CHROMA=false` on Dalidou). They can be rebuilt from the +source documents if lost. To include them, set `ATOCORE_BACKUP_CHROMA=true` +in the Dalidou cron environment. + +## Three-tier backup summary + +After this setup: + +| Tier | Location | Cadence | Purpose | +|---|---|---|---| +| Live | Dalidou `/srv/storage/atocore/backups/snapshots/` | Nightly 03:00 UTC | Fast restore | +| Off-host | T420 `papa@192.168.86.39:/home/papa/atocore-backups/` | Nightly after Dalidou | Dalidou dies | +| User machine | `C:\Users\antoi\Documents\ATOCore_Backups\` | Daily 10:00 local | Full home-network failure | + +Three independent copies. Any two can be lost simultaneously without data loss. diff --git a/scripts/windows/atocore-backup-pull.ps1 b/scripts/windows/atocore-backup-pull.ps1 new file mode 100644 index 0000000..c26cad4 --- /dev/null +++ b/scripts/windows/atocore-backup-pull.ps1 @@ -0,0 +1,87 @@ +# 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 }