● Zion Boggan
repos/Darwin/ingest/sync-white-pc.sh
zionboggan.com ↗
195 lines · bash
History for this file →
1
 
2
set -Eeuo pipefail
3
 
4
STATE_DB="${HOMELAB_RAG_STATE_DB:-/var/tmp/homelab-rag/state.sqlite}"
5
PROJECT_DIR="${HOMELAB_RAG_PROJECT_DIR:-/shared/projects/homelab-rag}"
6
SSH_KEY="${HOME:-/root}/.ssh/termius_homelab"
7
HOST="100.76.195.52"
8
SSH_USER="zion"
9
WALKER_REMOTE_WIN='C:\Users\zion\AppData\Local\Temp\homelab-rag-walker.ps1'
10
WALKER_REMOTE_PS='C:\\Users\\zion\\AppData\\Local\\Temp\\homelab-rag-walker.ps1'
11
 
12
DRY_RUN=0
13
for arg in "$@"; do [ "$arg" = "--dry-run" ] && DRY_RUN=1; done
14
 
15
log() { printf '[%s] %s\n' "$(date -Is)" "$*" >&2; }
16
 
17
SSH_BASE="ssh -o BatchMode=yes -o ConnectTimeout=12 -o StrictHostKeyChecking=accept-new -i ${SSH_KEY} ${SSH_USER}@${HOST}"
18
 
19
WALKER_LOCAL=$(mktemp /tmp/homelab-rag-walker-XXXXXX.ps1)
20
trap 'rm -f "$WALKER_LOCAL"' EXIT
21
 
22
cat > "$WALKER_LOCAL" <<'WALKER_BODY'
23
param([string]$PayloadJson)
24
$p = $PayloadJson | ConvertFrom-Json
25
$sid = [string]$p.source_id
26
$roots = @($p.roots)
27
$excl = @($p.exclude | ForEach-Object { ([string]$_).Replace('\','/').ToLowerInvariant() })
28
$maxD = [int]$p.max_depth
29
$maxF = [int]$p.max_files
30
$maxB = [int64]$p.max_file_bytes
31
$script:recs = New-Object System.Collections.Generic.List[object]
32
$script:st = [ordered]@{ filesSeen=0; skipped=0; errors=0; truncated=$false }
33
$ep = [datetime]'1970-01-01T00:00:00Z'
34
function cslash([string]$v) { $v.Replace('\','/') }
35
function is_excl([string]$path, [string]$name) {
36
  $n = (cslash $path).ToLowerInvariant(); $l = $name.ToLowerInvariant()
37
  foreach ($x in $excl) { if ($l -eq $x -or $n.EndsWith('/'+$x) -or $n.Contains('/'+$x+'/')) { return $true } }
38
  return $false
39
}
40
function kh([string]$path) {
41
  $l = $path.ToLowerInvariant()
42
  if ($l -match '\.(md|txt|rst|docx|pdf|pptx)$') { return 'doc' }
43
  if ($l -match '\.(json|ya?ml|toml|ini|conf|env|service|ps1)$') { return 'config' }
44
  if ($l -match '\.(log|out|err)$') { return 'log' }
45
  if ($l -match '\.(js|ts|py|sh|go|rs|html|css)$') { return 'code' }
46
  return 'other'
47
}
48
function walk([string]$root, [int]$depth) {
49
  if ($script:st['truncated']) { return }
50
  if ($maxD -ge 0 -and $depth -gt $maxD) { return }
51
  try { $entries = Get-ChildItem -LiteralPath $root -Force -ErrorAction Stop } catch { $script:st['errors'] += 1; return }
52
  foreach ($entry in $entries) {
53
    if ($script:st['truncated']) { return }
54
    if (is_excl $entry.FullName $entry.Name) { continue }
55
    if ($entry.PSIsContainer) { walk ([string]$entry.FullName) ($depth + 1); continue }
56
    try {
57
      $sz = [int64]$entry.Length
58
      $mt = [int64](($entry.LastWriteTimeUtc - $ep).TotalMilliseconds)
59
      $fp = cslash $entry.FullName
60
      $sk = if ($sz -gt $maxB) { 'large>' + $maxB } else { '' }
61
      if ($sk) { $script:st['skipped'] += 1 } else { $script:st['filesSeen'] += 1 }
62
      $script:recs.Add([ordered]@{
63
        source_id = $sid; path = $fp; normalized_path = $fp
64
        size = $sz; mtime_ms = $mt; kind_hint = (kh $entry.FullName)
65
        seen_at = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
66
        skip_reason = $sk
67
      }) | Out-Null
68
      if ($maxF -gt 0 -and $script:recs.Count -ge $maxF) { $script:st['truncated'] = $true; return }
69
    } catch { $script:st['errors'] += 1 }
70
  }
71
}
72
foreach ($r in $roots) { walk ([string]$r) 0 }
73
$script:st['records'] = $script:recs.ToArray()
74
$script:st | ConvertTo-Json -Depth 8 -Compress
75
WALKER_BODY
76
 
77
log "Walker size: $(wc -c < "$WALKER_LOCAL") bytes"
78
 
79
if [ "$DRY_RUN" -eq 1 ]; then
80
  log "[DRY-RUN] Would deploy walker to $HOST and run sync for all desktop-qi823gr* sources"
81
  exit 0
82
fi
83
 
84
log "Deploying walker to $HOST:$WALKER_REMOTE_WIN"
85
DEPLOY_RESULT=$(cat "$WALKER_LOCAL" | $SSH_BASE \
86
  "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \"\$content=[Console]::In.ReadToEnd(); [System.IO.File]::WriteAllText('$WALKER_REMOTE_WIN',\$content,[System.Text.Encoding]::UTF8); Write-Output 'DEPLOYED_OK'\"" 2>&1 || true)
87
if ! echo "$DEPLOY_RESULT" | grep -q "DEPLOYED_OK"; then
88
  log "ERROR: Deploy failed. Output: ${DEPLOY_RESULT:0:300}"
89
  exit 1
90
fi
91
log "Walker deployed OK"
92
 
93
run_source() {
94
  local SOURCE_ID="$1"
95
  local PAYLOAD="$2"
96
  local SOURCE_META="$3"
97
 
98
  log "  Running walker for $SOURCE_ID"
99
  local PAYLOAD_PS="${PAYLOAD//\'/\'\'}"
100
  local RUN_PS=". '$WALKER_REMOTE_PS' '$PAYLOAD_PS'"
101
  local RUN_ENC
102
  RUN_ENC=$(python3 -c "import base64,sys; print(base64.b64encode(sys.argv[1].encode('utf-16-le')).decode())" "$RUN_PS")
103
  log "  EncodedCommand length: ${#RUN_ENC}"
104
 
105
  local MANIFEST_RAW
106
  MANIFEST_RAW=$($SSH_BASE "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $RUN_ENC" 2>&1 || true)
107
 
108
  local MANIFEST_JSON
109
  MANIFEST_JSON=$(python3 -c "
110
import sys, re, json
111
data = sys.stdin.read()
112
for cand in re.findall(r'(\{[^\x00]{10,}\})', data, re.DOTALL):
113
    try:
114
        obj = json.loads(cand)
115
        if 'filesSeen' in obj:
116
            print(cand)
117
            break
118
    except Exception:
119
        pass
120
" <<< "$MANIFEST_RAW" 2>/dev/null || true)
121
 
122
  if [ -z "$MANIFEST_JSON" ]; then
123
    log "  ERROR: No JSON in manifest output for $SOURCE_ID. Raw (first 400):"
124
    echo "${MANIFEST_RAW:0:400}" >&2
125
    return 1
126
  fi
127
 
128
  local FILES_SEEN
129
  FILES_SEEN=$(python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('filesSeen',0))" <<< "$MANIFEST_JSON" 2>/dev/null || echo "?")
130
  log "  Files seen: $FILES_SEEN"
131
 
132
  local STARTED_AT
133
  STARTED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
134
 
135
  local BRIDGE_OUT
136
  BRIDGE_OUT=$(python3 - <<PYEOF
137
import json, sys, subprocess, os
138
 
139
state_db    = "$STATE_DB"
140
source_id   = "$SOURCE_ID"
141
host        = "$HOST"
142
started_at  = "$STARTED_AT"
143
project_dir = "$PROJECT_DIR"
144
 
145
manifest = json.loads(r"""$MANIFEST_JSON""")
146
records  = manifest.get("records", [])
147
errors   = manifest.get("errors", 0)
148
 
149
source_meta = json.loads(r"""$SOURCE_META""")
150
 
151
payload = {"action":"sync","statePath":state_db,"source":source_meta,"startedAt":started_at,"records":records,"errors":errors}
152
bridge = os.path.join(project_dir, "src", "sqlite_bridge.py")
153
result = subprocess.run(["python3", bridge], input=json.dumps(payload).encode(), capture_output=True, timeout=60)
154
if result.returncode != 0:
155
    print("BRIDGE_ERROR:" + result.stderr.decode()[:300], file=sys.stderr)
156
    sys.exit(1)
157
print(result.stdout.decode().strip())
158
PYEOF
159
)
160
  log "  Bridge result for $SOURCE_ID: $BRIDGE_OUT"
161
  local DELTA
162
  DELTA=$(python3 -c "
163
import json,sys
164
d=json.loads(sys.argv[1])
165
print('new=%d changed=%d unchanged=%d tombstoned=%d' % (d.get('newFiles',0),d.get('changedFiles',0),d.get('unchangedFiles',0),d.get('tombstonedFiles',0)))
166
" "$BRIDGE_OUT" 2>/dev/null || echo "parse error")
167
  log "  Change-only delta ($SOURCE_ID): $DELTA"
168
}
169
 
170
run_source \
171
  "desktop-qi823gr" \
172
  '{"source_id":"desktop-qi823gr","roots":["C:\\Users\\zion\\Documents","C:\\Users\\zion\\Desktop"],"exclude":["AppData","node_modules","cache",".git","Videos","Downloads"],"max_depth":4,"max_files":25000,"max_file_bytes":67108864}' \
173
  '{"id":"desktop-qi823gr","host":"100.76.195.52","type":"windows-desktop","transport":"ssh-powershell","authority":6,"cadence":"2h","canonical":false,"enabled":true,"notes":"White PC over Tailscale. Darwin BUILD-1. SSH stdin deploy workaround.","roots":["C:\\\\Users\\\\zion\\\\Documents","C:\\\\Users\\\\zion\\\\Desktop"],"exclude":["AppData","node_modules","cache",".git","Videos","Downloads"],"maxDepth":4,"maxFiles":25000,"user":"zion"}'
174
 
175
run_source \
176
  "desktop-qi823gr-worky" \
177
  '{"source_id":"desktop-qi823gr-worky","roots":["C:\\Users\\worky\\Documents","C:\\Users\\worky\\Desktop","C:\\Users\\worky\\Downloads","C:\\Users\\worky\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine"],"exclude":["AppData","node_modules",".git","cache","Videos"],"max_depth":3,"max_files":10000,"max_file_bytes":67108864}' \
178
  '{"id":"desktop-qi823gr-worky","host":"100.76.195.52","type":"windows-desktop","transport":"ssh-powershell","authority":8,"cadence":"1h","canonical":false,"enabled":true,"notes":"Darwin BUILD-5. White PC worky user files + PSReadLine history.","roots":["C:\\\\Users\\\\worky\\\\Documents","C:\\\\Users\\\\worky\\\\Desktop","C:\\\\Users\\\\worky\\\\Downloads","C:\\\\Users\\\\worky\\\\AppData\\\\Roaming\\\\Microsoft\\\\Windows\\\\PowerShell\\\\PSReadLine"],"exclude":["AppData","node_modules",".git","cache","Videos"],"maxDepth":3,"maxFiles":10000,"user":"zion"}'
179
 
180
run_source \
181
  "desktop-qi823gr-browser-brave" \
182
  '{"source_id":"desktop-qi823gr-browser-brave","roots":["C:\\Users\\worky\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Default"],"exclude":["Code Cache","GPUCache","ShaderCache","Service Worker","CacheStorage","Cache","cache","IndexedDB","VideoDecodeStats","optimization_guide_hint_cache_store","optimization_guide_model_metadata_store","GCM Store","Sync Data","Extension Rules","Extension Scripts","Platform Notifications"],"max_depth":2,"max_files":5000,"max_file_bytes":67108864}' \
183
  '{"id":"desktop-qi823gr-browser-brave","host":"100.76.195.52","type":"windows-desktop","transport":"ssh-powershell","authority":9,"cadence":"30m","canonical":false,"enabled":true,"notes":"Darwin BUILD-5. Brave browser profile for White PC worky user. 25.7MB History, active daily.","roots":["C:\\\\Users\\\\worky\\\\AppData\\\\Local\\\\BraveSoftware\\\\Brave-Browser\\\\User Data\\\\Default"],"exclude":["Code Cache","GPUCache","ShaderCache","Service Worker","CacheStorage","Cache","cache","IndexedDB"],"maxDepth":2,"maxFiles":5000,"user":"zion"}'
184
 
185
run_source \
186
  "desktop-qi823gr-browser-edge" \
187
  '{"source_id":"desktop-qi823gr-browser-edge","roots":["C:\\Users\\worky\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default"],"exclude":["Code Cache","GPUCache","ShaderCache","Service Worker","CacheStorage","Cache","cache","IndexedDB","VideoDecodeStats","optimization_guide_hint_cache_store","optimization_guide_model_metadata_store","GCM Store","Sync Data","Extension Rules","Extension Scripts","Platform Notifications"],"max_depth":2,"max_files":5000,"max_file_bytes":67108864}' \
188
  '{"id":"desktop-qi823gr-browser-edge","host":"100.76.195.52","type":"windows-desktop","transport":"ssh-powershell","authority":7,"cadence":"1h","canonical":false,"enabled":true,"notes":"Darwin BUILD-5. Edge browser profile for White PC worky user. 288KB History.","roots":["C:\\\\Users\\\\worky\\\\AppData\\\\Local\\\\Microsoft\\\\Edge\\\\User Data\\\\Default"],"exclude":["Code Cache","GPUCache","ShaderCache","Service Worker","CacheStorage","Cache","cache","IndexedDB"],"maxDepth":2,"maxFiles":5000,"user":"zion"}'
189
 
190
run_source \
191
  "desktop-qi823gr-browser-firefox" \
192
  '{"source_id":"desktop-qi823gr-browser-firefox","roots":["C:\\Users\\worky\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\ojo1adxx.default-release"],"exclude":["cache2","startupCache","thumbnails","storage","datareporting","crashes","minidumps"],"max_depth":2,"max_files":3000,"max_file_bytes":67108864}' \
193
  '{"id":"desktop-qi823gr-browser-firefox","host":"100.76.195.52","type":"windows-desktop","transport":"ssh-powershell","authority":9,"cadence":"30m","canonical":false,"enabled":true,"notes":"Darwin BUILD-5. Firefox places.sqlite for White PC worky user. 5MB, last modified 2026-06-15.","roots":["C:\\\\Users\\\\worky\\\\AppData\\\\Roaming\\\\Mozilla\\\\Firefox\\\\Profiles\\\\ojo1adxx.default-release"],"exclude":["cache2","startupCache","thumbnails","storage","datareporting","crashes","minidumps"],"maxDepth":2,"maxFiles":3000,"user":"zion"}'
194
 
195
log "All desktop-qi823gr* sources complete"