Skip to content

Instantly share code, notes, and snippets.

@webstrand
Created November 19, 2025 16:20
Show Gist options
  • Select an option

  • Save webstrand/945c738c5d60ffd7657845a6546901b3 to your computer and use it in GitHub Desktop.

Select an option

Save webstrand/945c738c5d60ffd7657845a6546901b3 to your computer and use it in GitHub Desktop.
Tool for synchronizing keepass repositories over SSH
#!/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.", &timestamp])?;
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.{}", &timestamp))
.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