Created
November 19, 2025 16:20
-
-
Save webstrand/945c738c5d60ffd7657845a6546901b3 to your computer and use it in GitHub Desktop.
Tool for synchronizing keepass repositories over SSH
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S cargo +nightly -Z script | |
| --- | |
| edition = "2024" | |
| [dependencies] | |
| tempfile = "3" | |
| thiserror = "1" | |
| reflink-copy = "0.1" | |
| chrono = "0.4" | |
| --- | |
| #![feature(bool_to_result)] | |
| use reflink_copy::reflink_or_copy; | |
| use std::io; | |
| use std::os::unix::fs::PermissionsExt; | |
| use std::path::PathBuf; | |
| use std::process::Command; | |
| use thiserror::Error; | |
| #[derive(Error, Debug)] | |
| enum Error { | |
| #[error("missing local kdbx path")] | |
| MissingLocalDbPath, | |
| #[error("missing remote ssh path")] | |
| MissingRemoteSshPath, | |
| #[error("failed to download remote database")] | |
| ScpDownloadFailed, | |
| #[error("failed to upload merged database")] | |
| ScpUploadFailed, | |
| #[error("local merge failed (local database is not updated)")] | |
| MergeFailed, | |
| #[error("{0}")] | |
| Io(#[from] io::Error), | |
| } | |
| macro_rules! oscat { | |
| ($($part:expr),+ $(,)?) => {{ | |
| let capacity = 0 $(+ AsRef::<std::ffi::OsStr>::as_ref(&$part).len())+; | |
| let mut result = std::ffi::OsString::with_capacity(capacity); | |
| $(result.push($part);)+ | |
| result | |
| }}; | |
| } | |
| fn main() { | |
| if let Err(e) = run() { | |
| eprintln!("Error: {}", e); | |
| std::process::exit(1); | |
| } | |
| } | |
| fn run() -> Result<(), Error> { | |
| let mut args = std::env::args_os().skip(1); | |
| let local_db = PathBuf::from(args.next().ok_or(Error::MissingLocalDbPath)?); | |
| let ssh_path = args.next().ok_or(Error::MissingRemoteSshPath)?; | |
| let timestamp = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string(); | |
| reflink_or_copy(&local_db, oscat![&local_db, ".backup.", ×tamp])?; | |
| let temp_dir = tempfile::Builder::new() | |
| .permissions(std::fs::Permissions::from_mode(0o700)) | |
| .tempdir()?; | |
| let temp_remote = temp_dir.path().join("remote.kdbx"); | |
| Command::new("scp") | |
| .arg(&ssh_path) | |
| .arg(&temp_remote) | |
| .status()? | |
| .success() | |
| .ok_or(Error::ScpDownloadFailed)?; | |
| Command::new("keepassxc-cli") | |
| .args(&["merge", "--same-credentials"]) | |
| .arg(&local_db) | |
| .arg(&temp_remote) | |
| .status()? | |
| .success() | |
| .ok_or(Error::MergeFailed)?; | |
| Command::new("rsync") | |
| .args(&["-avc", "--backup", "--backup-dir=."]) | |
| .arg("--suffix") | |
| .arg(format!(".backup.{}", ×tamp)) | |
| .arg(&local_db) | |
| .arg(&ssh_path) | |
| .status()? | |
| .success() | |
| .ok_or(Error::ScpUploadFailed)?; | |
| Ok(()) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment