v0.1.3-pre.013
This commit is contained in:
113
crates/ksp-config-lib/src/persistence.rs
Normal file
113
crates/ksp-config-lib/src/persistence.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
// file: crates/ksp-config-lib/src/persistence.rs
|
||||
// version: 1
|
||||
|
||||
static NEXT_TEMPORARY_FILE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
|
||||
pub(crate) fn atomic_write(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
||||
return atomic_write_with_policy(path, content, false);
|
||||
}
|
||||
|
||||
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: "ksp-config-lib", 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);
|
||||
}
|
||||
Reference in New Issue
Block a user