use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context, Result};
use rusqlite::{Connection, OpenFlags};
use super::fs_utils::{make_temp_dir, path_exists, remove_dir_quietly};
const SNAPSHOT_PREFIX: &str = "browser-commander-snap-";
const SQLITE_SIDECARS: [&str; 3] = ["-wal", "-shm", "-journal"];
fn file_name(source_path: &Path) -> Result<&std::ffi::OsStr> {
source_path
.file_name()
.ok_or_else(|| anyhow!("{} has no file name", source_path.display()))
}
fn with_suffix(path: &Path, suffix: &str) -> PathBuf {
let mut name = path.as_os_str().to_os_string();
name.push(suffix);
PathBuf::from(name)
}
fn copy_database_files(source_path: &Path, dir: &Path) -> Result<PathBuf> {
let snapshot_path = dir.join(file_name(source_path)?);
fs::copy(source_path, &snapshot_path)
.with_context(|| format!("Could not copy {}", source_path.display()))?;
for suffix in SQLITE_SIDECARS {
let sidecar = with_suffix(source_path, suffix);
if path_exists(&sidecar) {
fs::copy(&sidecar, with_suffix(&snapshot_path, suffix))
.with_context(|| format!("Could not copy {}", sidecar.display()))?;
}
}
Ok(snapshot_path)
}
fn open_read_only(path: &Path) -> rusqlite::Result<Connection> {
Connection::open_with_flags(
path,
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
}
fn snapshot_into(source_path: &Path, dir: &Path) -> Result<PathBuf> {
let snapshot_path = dir.join(file_name(source_path)?);
let backed_up = open_read_only(source_path).and_then(|source| {
source.backup(rusqlite::MAIN_DB, &snapshot_path, None)
});
match backed_up {
Ok(()) => Ok(snapshot_path),
Err(_) => {
let _ = fs::remove_file(&snapshot_path);
copy_database_files(source_path, dir)
}
}
}
pub(crate) fn with_database_snapshot<T>(
source_path: &Path,
read: impl FnOnce(&Path) -> Result<T>,
) -> Result<T> {
if !path_exists(source_path) {
return Err(anyhow!(
"Source database does not exist: {}",
source_path.display()
));
}
let dir = make_temp_dir(SNAPSHOT_PREFIX)?;
let result = snapshot_into(source_path, &dir).and_then(|path| read(&path));
remove_dir_quietly(&dir);
result
}
pub(crate) fn read_database_snapshot<T>(
source_path: &Path,
read: impl FnOnce(&Connection) -> Result<T>,
) -> Result<T> {
with_database_snapshot(source_path, |snapshot_path| {
let database = open_read_only(snapshot_path)
.with_context(|| format!("Could not open {}", snapshot_path.display()))?;
read(&database)
})
}