use std::path::{Path, PathBuf};
use crate::storage::meta::meta_path_for;
use crate::storage::store::{remove_if_exists, sync_dir};
use crate::{Error, Result};
#[cfg(feature = "encrypt")]
use crate::encryption::EncryptionInput;
#[cfg(feature = "encrypt")]
use crate::storage::{Engine, EngineConfig, DEFAULT_NAMESPACE_ID};
fn sibling(path: &Path, suffix: &str) -> PathBuf {
let mut name = path
.file_name()
.map_or_else(|| std::ffi::OsString::from("emdb"), |n| n.to_os_string());
name.push(suffix);
path.with_file_name(name)
}
fn temp_path_for(path: &Path) -> PathBuf {
sibling(path, ".enc.tmp")
}
fn backup_path_for(path: &Path) -> PathBuf {
sibling(path, ".encbak")
}
fn marker_path_for(path: &Path) -> PathBuf {
sibling(path, ".encadmin")
}
pub(crate) fn finish_interrupted_rewrite(path: &Path) -> Result<()> {
let marker = marker_path_for(path);
if !marker.exists() {
return refuse_orphaned_backup(path);
}
let tmp = temp_path_for(path);
let tmp_meta = meta_path_for(&tmp);
if tmp_meta.exists() {
std::fs::rename(&tmp_meta, meta_path_for(path))?;
}
if tmp.exists() {
std::fs::rename(&tmp, path)?;
}
sync_dir(path)?;
remove_if_exists(&marker)?;
sync_dir(path)
}
fn refuse_orphaned_backup(path: &Path) -> Result<()> {
let data_len = match std::fs::metadata(path) {
Ok(meta) => meta.len(),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => 0,
Err(err) => return Err(Error::Io(err)),
};
if data_len > 0 {
return Ok(());
}
match std::fs::metadata(backup_path_for(path)) {
Ok(bak) if bak.len() > 0 => Err(Error::InvalidConfig(
"database file is missing or empty but <path>.encbak holds data from an interrupted \
encryption admin operation; rename <path>.encbak and <path>.encbak.meta back to \
<path> and <path>.meta before opening",
)),
_ => Ok(()),
}
}
#[cfg(feature = "encrypt")]
fn engine_config(path: &Path, mode: Option<&EncryptionInput>) -> EngineConfig {
let mut config = EngineConfig {
path: path.to_path_buf(),
..EngineConfig::default()
};
match mode {
None => {}
Some(EncryptionInput::Key(key)) => {
config.encryption_key = Some(crate::encryption::KeyBytes::from(*key));
}
Some(EncryptionInput::Passphrase(passphrase)) => {
config.encryption_passphrase =
Some(crate::encryption::Passphrase::new(passphrase.clone()));
}
}
config
}
#[cfg(feature = "encrypt")]
fn copy_records(
path: &Path,
tmp: &Path,
from: Option<&EncryptionInput>,
to: Option<&EncryptionInput>,
) -> Result<()> {
let src = Engine::open(engine_config(path, from))?;
let dst = Engine::open(engine_config(tmp, to))?;
#[cfg(feature = "ttl")]
let now = crate::ttl::now_unix_millis();
for (ns_id, name) in src.list_namespaces()? {
let dst_ns = if ns_id == DEFAULT_NAMESPACE_ID {
DEFAULT_NAMESPACE_ID
} else {
dst.create_or_open_namespace(&name)?
};
src.for_each_record_batch(ns_id, |batch| {
#[cfg(feature = "ttl")]
let batch: Vec<_> = batch
.into_iter()
.filter(|(_, _, expires_at)| *expires_at == 0 || *expires_at > now)
.collect();
dst.insert_many(dst_ns, batch)
})?;
}
dst.flush()
}
#[cfg(feature = "encrypt")]
fn link_or_copy(from: &Path, to: &Path) -> Result<()> {
if std::fs::hard_link(from, to).is_ok() {
return Ok(());
}
let mut copy = crate::private_fs::create_new_private_file(to)?;
let _bytes = std::io::copy(&mut std::fs::File::open(from)?, &mut copy)?;
copy.sync_all()?;
Ok(())
}
#[cfg(feature = "encrypt")]
fn write_marker(marker: &Path) -> Result<()> {
use std::io::Write;
let mut file = crate::private_fs::open_or_create_private_file(marker)?;
file.set_len(0)?;
file.write_all(b"emdb encryption admin: rename <path>.enc.tmp over <path>\n")?;
file.sync_all()?;
sync_dir(marker)
}
#[cfg(feature = "encrypt")]
pub(crate) fn rewrite_database(
path: &Path,
from: Option<&EncryptionInput>,
to: Option<&EncryptionInput>,
) -> Result<()> {
let path = &crate::data_dir::canonical_database_path(path)?;
let _lock = crate::lockfile::LockFile::acquire(path)?;
finish_interrupted_rewrite(path)?;
if !path.exists() {
return Err(Error::InvalidConfig(
"encryption admin: source database file does not exist",
));
}
let tmp = temp_path_for(path);
let tmp_meta = meta_path_for(&tmp);
let bak = backup_path_for(path);
let bak_meta = meta_path_for(&bak);
let marker = marker_path_for(path);
remove_if_exists(&tmp)?;
remove_if_exists(&tmp_meta)?;
let _ignored = remove_if_exists(&sibling(&tmp, ".lock"));
let _ignored = remove_if_exists(&sibling(&tmp, ".lock-meta"));
drop(crate::private_fs::create_new_private_file(&tmp)?);
if let Err(err) = copy_records(path, &tmp, from, to) {
let _ignored = remove_if_exists(&tmp);
let _ignored = remove_if_exists(&tmp_meta);
return Err(err);
}
sync_dir(path)?;
remove_if_exists(&bak)?;
remove_if_exists(&bak_meta)?;
link_or_copy(path, &bak)?;
let path_meta = meta_path_for(path);
if path_meta.exists() {
link_or_copy(&path_meta, &bak_meta)?;
}
sync_dir(path)?;
crate::private_fs::keep_permissions(path, &tmp)?;
if path_meta.exists() {
crate::private_fs::keep_permissions(&path_meta, &tmp_meta)?;
}
write_marker(&marker)?;
std::fs::rename(&tmp_meta, &path_meta)?;
std::fs::rename(&tmp, path)?;
sync_dir(path)?;
remove_if_exists(&marker)?;
sync_dir(path)
}
#[cfg(feature = "encrypt")]
pub fn enable_encryption(path: impl AsRef<Path>, target: EncryptionInput) -> Result<()> {
let path = path.as_ref();
if let Some(header) = crate::storage::meta::read(path)? {
if header.flags & crate::storage::meta::FLAG_ENCRYPTED != 0 {
return Err(Error::InvalidConfig(
"enable_encryption: file is already encrypted",
));
}
} else {
return Err(Error::InvalidConfig(
"enable_encryption: file does not exist",
));
}
rewrite_database(path, None, Some(&target))
}
#[cfg(feature = "encrypt")]
pub fn disable_encryption(path: impl AsRef<Path>, current: EncryptionInput) -> Result<()> {
let path = path.as_ref();
if let Some(header) = crate::storage::meta::read(path)? {
if header.flags & crate::storage::meta::FLAG_ENCRYPTED == 0 {
return Err(Error::InvalidConfig(
"disable_encryption: file is already unencrypted",
));
}
} else {
return Err(Error::InvalidConfig(
"disable_encryption: file does not exist",
));
}
rewrite_database(path, Some(¤t), None)
}
#[cfg(feature = "encrypt")]
pub fn rotate_encryption_key(
path: impl AsRef<Path>,
from: EncryptionInput,
to: EncryptionInput,
) -> Result<()> {
let path = path.as_ref();
if let Some(header) = crate::storage::meta::read(path)? {
if header.flags & crate::storage::meta::FLAG_ENCRYPTED == 0 {
return Err(Error::InvalidConfig(
"rotate_encryption_key: file is not encrypted; use enable_encryption \
to add encryption to an unencrypted database",
));
}
} else {
return Err(Error::InvalidConfig(
"rotate_encryption_key: file does not exist",
));
}
rewrite_database(path, Some(&from), Some(&to))
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp_dir(label: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0_u128, |d| d.as_nanos());
let mut p = std::env::temp_dir();
p.push(format!("emdb-admin-{label}-{}-{nanos}", std::process::id()));
std::fs::create_dir_all(&p).expect("mkdir");
p
}
#[test]
fn test_finish_interrupted_rewrite_completes_pending_swap() {
let dir = tmp_dir("finish");
let path = dir.join("db");
std::fs::write(&path, b"old").expect("write");
std::fs::write(meta_path_for(&path), b"old-meta").expect("write");
std::fs::write(temp_path_for(&path), b"new").expect("write");
std::fs::write(meta_path_for(&temp_path_for(&path)), b"new-meta").expect("write");
std::fs::write(marker_path_for(&path), b"x").expect("write");
finish_interrupted_rewrite(&path).expect("finish");
assert_eq!(std::fs::read(&path).expect("read"), b"new");
assert_eq!(
std::fs::read(meta_path_for(&path)).expect("read"),
b"new-meta"
);
assert!(!marker_path_for(&path).exists());
assert!(!temp_path_for(&path).exists());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_finish_interrupted_rewrite_without_marker_is_noop() {
let dir = tmp_dir("noop");
let path = dir.join("db");
std::fs::write(&path, b"data").expect("write");
std::fs::write(temp_path_for(&path), b"stale").expect("write");
finish_interrupted_rewrite(&path).expect("noop");
assert_eq!(std::fs::read(&path).expect("read"), b"data");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_orphaned_encbak_with_missing_database_refused() {
let dir = tmp_dir("orphan");
let path = dir.join("db");
std::fs::write(backup_path_for(&path), b"precious").expect("write");
assert!(matches!(
finish_interrupted_rewrite(&path),
Err(Error::InvalidConfig(_))
));
std::fs::write(&path, b"").expect("write");
assert!(finish_interrupted_rewrite(&path).is_err());
std::fs::write(&path, b"data").expect("write");
assert!(finish_interrupted_rewrite(&path).is_ok());
let _ = std::fs::remove_dir_all(&dir);
}
}