scripts/win/system/sync.ps1
# scripts/win/central/sync.ps1 # Mirrors configured folders between local station and the shared NetworkRoot. # Usage: # borg config upload # local -> hub (NetworkRoot) # borg config download # hub -> local # borg config upload <name> # just one folder # borg config download <name> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('upload','download')] [string]$Action, [string]$Name = 'all' ) # Load borg helpers (expects: Get-BorgStore, GetBorgStoreValue, Resolve-EnvTokens) . "$env:BORG_ROOT\config\globalfn.ps1" function New-DirIfMissing([string]$Path) { if (-not [string]::IsNullOrWhiteSpace($Path) -and -not (Test-Path -LiteralPath $Path)) { New-Item -ItemType Directory -Force -Path $Path | Out-Null } } # --- Read config (via new cached accessor) --- $store = Get-BorgStore $sync = $store.Sync if (-not $sync) { throw "Missing 'Sync' section in store.json." } $networkRoot = GetBorgStoreValue -Chapter Sync -Key 'General.NetworkRoot' -ExpandEnv if ([string]::IsNullOrWhiteSpace($networkRoot)) { throw "Sync.General.NetworkRoot is required." } # Ensure robocopy exists if (-not (Get-Command robocopy.exe -ErrorAction SilentlyContinue)) { throw "robocopy.exe not found in PATH. Please ensure it's available." } # Collect folders (optionally filtered) $folders = @($sync.Folders) if (-not $folders -or $folders.Count -eq 0) { throw "No Sync.Folders defined." } if ($Name -ne 'all') { $folders = $folders | Where-Object { $_.Name -eq $Name } if (-not $folders -or $folders.Count -eq 0) { throw "Sync folder '$Name' not found." } } foreach ($f in $folders) { # Resolve local path using new env expander from globalfn $localPath = ($f.Local | Resolve-EnvTokens) if ([string]::IsNullOrWhiteSpace($localPath) -or -not (Test-Path -LiteralPath $localPath)) { Write-Warning "Skip '$($f.Name)': local path missing or not found: $localPath" continue } $remoteRel = if ($f.Remote) { $f.Remote } else { $f.Name } $remotePath = Join-Path $networkRoot $remoteRel New-DirIfMissing $remotePath if ($Action -eq 'upload') { $src = $localPath; $dst = $remotePath } else { $src = $remotePath; $dst = $localPath } # Map excludes: # '*.log' -> /XF # 'logs\**' -> /XD logs $exFiles = @() $exDirs = @() foreach ($pat in ($f.Exclude ?? @())) { if ($pat -like '*\**') { $exDirs += ($pat -replace '\\\*\*$','') } else { $exFiles += $pat } } Write-Host "" Write-Host ("🔁 {0} [{1}]" -f $Action.ToUpper(), $f.Name) -ForegroundColor Cyan Write-Host " From: $src" Write-Host " To: $dst" # Ensure destination exists New-DirIfMissing $dst # Build robocopy args (mirror; one retry; no wait) $args = @( $src, $dst, '/MIR', # mirror (includes deletions) '/Z', # restartable mode '/R:1', # one retry '/W:0', # no wait '/COPY:DAT', # file data, attributes, timestamps '/DCOPY:DAT', # dir data, attributes, timestamps '/NFL','/NDL','/NP' # cleaner output ) if ($exFiles.Count) { $args += @('/XF') + $exFiles } if ($exDirs.Count) { $args += @('/XD') + $exDirs } $proc = Start-Process -FilePath robocopy.exe -ArgumentList $args -PassThru -Wait $code = $proc.ExitCode # robocopy: 0–7 = success-ish; >=8 = failure if ($code -ge 8) { Write-Warning "robocopy failed with code $code for [$($f.Name)]." } else { Write-Host "✅ Done (code $code)." -ForegroundColor Green } } |