Files

122 lines
5.7 KiB
Rust

// file: crates/ksp-config-lib/src/persistence.rs
// version: 4
static NEXT_TEMPORARY_FILE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
/// Executes the crate-internal atomic write operation for the owning module.
pub(crate) fn atomic_write(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
return atomic_write_with_policy(path, content, false);
}
/// Executes the crate-internal atomic write private operation for the owning module.
pub(crate) fn atomic_write_private(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
return atomic_write_with_policy(path, content, true);
}
fn atomic_write_with_policy(path: &std::path::Path, content: &[u8], private_when_new: bool) -> ksp_core_lib::Result<()> {
let parent = match path.parent() {
std::option::Option::Some(value) if !value.as_os_str().is_empty() => value,
_ => std::path::Path::new("."),
};
let filename = match path.file_name().and_then(std::ffi::OsStr::to_str) {
std::option::Option::Some(value) if !value.is_empty() => value,
_ => return std::result::Result::Err(persistence_error(path, "managed Config path has no UTF-8 file name")),
};
let existing_permissions = destination_permissions(path);
let existing_permissions = match existing_permissions {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let temporary_id = NEXT_TEMPORARY_FILE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let temporary_name = format!(".{filename}.ksp-tmp-{}-{temporary_id}", std::process::id());
let temporary_path = parent.join(temporary_name);
let opened = std::fs::OpenOptions::new().write(true).create_new(true).open(temporary_path.as_path());
let mut file = match opened {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(persistence_io_error(path, "temporary Config file cannot be created", error)),
};
let permissions = apply_temporary_permissions(&file, existing_permissions, private_when_new);
if let std::result::Result::Err(error) = permissions {
cleanup_temporary_file(temporary_path.as_path());
return std::result::Result::Err(persistence_io_error(path, "temporary Config file permissions cannot be applied", error));
}
let write = std::io::Write::write_all(&mut file, content);
if let std::result::Result::Err(error) = write {
cleanup_temporary_file(temporary_path.as_path());
return std::result::Result::Err(persistence_io_error(path, "temporary Config file cannot be written", error));
}
let sync = file.sync_all();
if let std::result::Result::Err(error) = sync {
cleanup_temporary_file(temporary_path.as_path());
return std::result::Result::Err(persistence_io_error(path, "temporary Config file cannot be synchronized", error));
}
drop(file);
let rename = std::fs::rename(temporary_path.as_path(), path);
if let std::result::Result::Err(error) = rename {
cleanup_temporary_file(temporary_path.as_path());
return std::result::Result::Err(persistence_io_error(path, "atomic Config file replacement failed", error));
}
return std::result::Result::Ok(());
}
fn destination_permissions(path: &std::path::Path) -> ksp_core_lib::Result<std::option::Option<std::fs::Permissions>> {
let metadata = std::fs::metadata(path);
return match metadata {
std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value.permissions())),
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => std::result::Result::Ok(std::option::Option::None),
std::result::Result::Err(error) => {
std::result::Result::Err(persistence_io_error(path, "managed Config file metadata cannot be read before replacement", error))
},
};
}
fn apply_temporary_permissions(
file: &std::fs::File,
existing_permissions: std::option::Option<std::fs::Permissions>,
private_when_new: bool,
) -> std::io::Result<()> {
if let std::option::Option::Some(permissions) = existing_permissions {
return file.set_permissions(permissions);
}
return apply_new_file_permissions(file, private_when_new);
}
#[cfg(unix)]
fn apply_new_file_permissions(file: &std::fs::File, private_when_new: bool) -> std::io::Result<()> {
if private_when_new {
let permissions = <std::fs::Permissions as std::os::unix::fs::PermissionsExt>::from_mode(0o600);
return file.set_permissions(permissions);
}
return std::result::Result::Ok(());
}
#[cfg(not(unix))]
fn apply_new_file_permissions(_file: &std::fs::File, _private_when_new: bool) -> std::io::Result<()> {
return std::result::Result::Ok(());
}
fn cleanup_temporary_file(path: &std::path::Path) {
let removal = std::fs::remove_file(path);
if let std::result::Result::Err(error) = removal
&& error.kind() != std::io::ErrorKind::NotFound
{
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = "config.persistence",
path = %path.to_string_lossy(),
error = %error,
"unable to cleanup temporary Config file"
);
}
}
fn persistence_error(path: &std::path::Path, reason: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_PERSISTENCE_WRITE_FAILED, "Config persistence failed")
.with_context("path", path.to_string_lossy().into_owned())
.with_context("reason", reason);
}
fn persistence_io_error(path: &std::path::Path, reason: &'static str, source: std::io::Error) -> ksp_core_lib::Error {
return persistence_error(path, reason).with_source(source);
}