use crate::errors::Error;
use crate::output::{BackupResponse, print_json};
use crate::sqlite::Database;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::time::Duration;
fn wrap_busy<T>(result: Result<T, Error>) -> Result<T, Error> {
match result {
Ok(v) => Ok(v),
Err(Error::SqliteModule(msg)) if msg.contains("database is locked") => {
Err(Error::Config(
"Database is locked. Another process (likely the MCP server) is holding a lock. Stop the MCP server and retry.".to_string()
))
}
Err(Error::SQLite(rusqlite_err))
if rusqlite_err.to_string().contains("database is locked") =>
{
Err(Error::Config(
"Database is locked. Another process (likely the MCP server) is holding a lock. Stop the MCP server and retry.".to_string()
))
}
Err(e) => Err(e),
}
}
fn run_online_backup(source: &Path, destination: &Path) -> Result<u64, Error> {
let source_db = Database::open(source).map_err(|e| {
let err_msg = e.to_string();
if err_msg.contains("database is locked") {
return Error::Config(
"Database is locked. Another process (likely the MCP server) is holding a lock. Stop the MCP server and retry.".to_string()
);
}
Error::Config(err_msg)
})?;
wrap_busy(
source_db
.set_busy_timeout(Duration::ZERO)
.map_err(Error::from),
)?;
if destination.exists() {
std::fs::remove_file(destination)?;
}
let mut dest_conn = rusqlite::Connection::open(destination)?;
let backup = wrap_busy(
rusqlite::backup::Backup::new(source_db.conn(), &mut dest_conn).map_err(Error::from),
)?;
use rusqlite::backup::StepResult;
loop {
match backup.step(-1)? {
StepResult::Done => break,
StepResult::Busy | StepResult::Locked => std::thread::sleep(Duration::from_millis(1)),
StepResult::More => {}
_ => std::thread::sleep(Duration::from_millis(1)),
}
}
drop(backup);
drop(dest_conn);
let dest_check = rusqlite::Connection::open(destination)?;
let mut defects = 0usize;
{
let mut stmt = dest_check.prepare("PRAGMA integrity_check")?;
let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
for row in rows {
let msg = row?;
if msg != "ok" {
defects += 1;
}
}
}
drop(dest_check);
if defects > 0 {
return Err(Error::Config(format!(
"Backup integrity check failed ({} defect(s)); destination: {}",
defects,
destination.display()
)));
}
let bytes = std::fs::metadata(destination)?.len();
Ok(bytes)
}
pub fn handle_backup(db_path: &Path, output: Option<&Path>, json: bool) -> Result<ExitCode, Error> {
let destination = resolve_destination(db_path, output);
let bytes = run_online_backup(db_path, &destination)?;
let rows = {
let dest = rusqlite::Connection::open(&destination)?;
let count: i64 = dest.query_row("SELECT COUNT(*) FROM memories", [], |row| row.get(0))?;
count as usize
};
let response = BackupResponse {
source: db_path.display().to_string(),
destination: destination.display().to_string(),
rows,
bytes,
};
if json {
print_json(&response);
} else {
println!(
"Backed up {} rows ({} bytes) to {}",
response.rows, response.bytes, response.destination
);
}
Ok(ExitCode::SUCCESS)
}
fn resolve_destination(source: &Path, output: Option<&Path>) -> PathBuf {
output.map(Path::to_path_buf).unwrap_or_else(|| {
let stem = source
.file_stem()
.map(|s| s.to_os_string())
.unwrap_or_else(|| "memories".into());
let ext = source
.extension()
.map(|e| e.to_os_string())
.unwrap_or_else(|| "db".into());
let parent = source.parent().map(Path::new).unwrap_or(Path::new("."));
let mut buf = parent.to_path_buf();
buf.push(format!(
"{}-backup.{}",
stem.to_string_lossy(),
ext.to_string_lossy()
));
buf
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_destination_default_ext() {
let src = Path::new("/tmp/memories.db");
let dest = resolve_destination(src, None);
assert_eq!(dest, PathBuf::from("/tmp/memories-backup.db"));
}
#[test]
fn test_resolve_destination_explicit_path() {
let src = Path::new("/tmp/memories.db");
let explicit = Path::new("/backups/memories.db");
let dest = resolve_destination(src, Some(explicit));
assert_eq!(dest, PathBuf::from("/backups/memories.db"));
}
#[test]
fn test_resolve_destination_source_without_extension() {
let src = Path::new("/tmp/memories");
let dest = resolve_destination(src, None);
assert_eq!(dest, PathBuf::from("/tmp/memories-backup.db"));
}
}