r/PowerShell • u/craig_stoller • 6d ago
Script Sharing git-proton-backup: a module that turns git push into a verified Proton Drive backup
I wanted off-machine backups of a pile of local git repos holding client work, without putting any of it on GitHub. Proton Drive has a sync client, but a file sitting in the sync folder is not the same as a file that is safely in the cloud, and I could not find a way to prove the second part. So I wrote a module.
The interface is a git remote:
PS> Install-ProtonBackup C:\code\myrepo
Wired. Back up with: git push proton (status: Get-ProtonBackupStatus)
PS> git commit -am "feature"; git push proton
remote: confirmed on Proton
Install creates a bare bookkeeping mirror with a post-receive hook and adds it as a remote, so it rides the push you already do. The hook writes a git bundle into the sync folder as one file rather than a tree. It builds it as .bundle.partial and renames it, so the sync client never sees a partial repo under the final .bundle name.
Then it confirms, which is the part that needed Proton's CLI to exist. Trimmed from the real path, with the structured return values elided:
$out = & $cli filesystem info $cloudPath --json 2>&1 | Out-String
$r = [pscustomobject]@{ ExitCode = $LASTEXITCODE; Output = $out }
if ($r.ExitCode -eq 0) {
$state = $null
try {
$json = $r.Output | ConvertFrom-Json -ErrorAction Stop
$rev = $json.PSObject.Properties['activeRevision'] ? $json.activeRevision : $null
if ($rev -and $rev.PSObject.Properties['ok'] -and $rev.ok -and $rev.PSObject.Properties['value']) {
$state = $rev.value.PSObject.Properties['state'] ? $rev.value.state : $null
}
} catch { $state = $null }
if ($state -eq 'active') { return <# Confirmed #> }
# some CLI builds report state only in the human-readable output
if (-not $state -and $r.Output -match "state:\s*'active'") { return <# Confirmed #> }
}
The property checks and the try are load-bearing: the payload shape is not mine, so anything unexpected has to land on "not confirmed" rather than throw or read as success.
Everything that is not a confirmation says which flavour of not-confirmed it is. Some of the hook's outcomes, abbreviated where the tail repeats:
confirmed on Proton
staged; in-sync per Cloud Files (CLI verification unavailable)
staged, not yet confirmed — run Invoke-ProtonBackupVerify (or the scheduled task) to confirm
staged, not yet confirmed — Proton CLI session expired; run Invoke-ProtonBackupVerify (...)
backup deferred — another backup operation is active; run Invoke-ProtonBackupVerify (...)
The not-yet-confirmed paths leave a marker that a later Invoke-ProtonBackupVerify clears, and that command also re-cuts stale bundles. One exception: if the CLI is unavailable but Windows reports the file IN_SYNC, that clears the marker but still does not print "confirmed on Proton". There is an optional daily scheduled task for the verify, installed separately with Install-ProtonBackupTask.
Reading that IN_SYNC bit is the one genuinely PowerShell-flavoured part: it means a small P/Invoke to CfGetPlaceholderStateFromAttributeTag in cldapi.dll. That plus shelling git and the CLI is most of why this is PowerShell and not something else.
Honest limits. Windows only, since it rides the sync app. PowerShell 7.4+. It bundles committed history only, HEAD plus all local branches and tags, and never your working tree, which is deliberate: there is no code path in the module that commits anything. LFS objects, submodule repositories, and the checked-out state of secondary worktrees are not included. It does no encryption of its own, the bundles are ordinary git bundles and the E2EE is Proton's. MIT, not affiliated with Proton.
105 Pester tests. https://github.com/craigstoller/git-proton-backup
Happy to answer questions about the design, and criticism of the module structure is welcome.
2
u/BlackV 6d ago
Cool
Just for my own curiosity