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>
This commit is contained in:
140
docs/windows-backup-setup.md
Normal file
140
docs/windows-backup-setup.md
Normal file
@@ -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.
|
||||
87
scripts/windows/atocore-backup-pull.ps1
Normal file
87
scripts/windows/atocore-backup-pull.ps1
Normal file
@@ -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 }
|
||||
Reference in New Issue
Block a user