use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process;
use std::sync::atomic::{AtomicU64, Ordering};
static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
pub(crate) fn create_text(path: &Path, contents: &str, mode: Option<u32>) -> io::Result<()> {
write_text(path, contents.as_bytes(), mode, FinalizeMode::CreateNew)
}
pub(crate) fn replace_text(path: &Path, contents: &str, mode: Option<u32>) -> io::Result<()> {
write_text(path, contents.as_bytes(), mode, FinalizeMode::Replace)
}
enum FinalizeMode {
CreateNew,
Replace,
}
fn write_text(
path: &Path,
contents: &[u8],
mode: Option<u32>,
finalize: FinalizeMode,
) -> io::Result<()> {
let parent = path.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("path has no parent: {}", path.display()),
)
})?;
fs::create_dir_all(parent)?;
let temp_path = unique_temp_path(parent, path);
let mut temp_file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&temp_path)?;
if let Err(error) = temp_file.write_all(contents) {
let _ = fs::remove_file(&temp_path);
return Err(error);
}
if let Err(error) = temp_file.sync_all() {
let _ = fs::remove_file(&temp_path);
return Err(error);
}
drop(temp_file);
if let Some(mode) = mode {
if let Err(error) = fs::set_permissions(&temp_path, fs::Permissions::from_mode(mode)) {
let _ = fs::remove_file(&temp_path);
return Err(error);
}
}
let result = match finalize {
FinalizeMode::CreateNew => finalize_create_new(&temp_path, path),
FinalizeMode::Replace => fs::rename(&temp_path, path),
};
if result.is_err() {
let _ = fs::remove_file(&temp_path);
}
result
}
fn finalize_create_new(temp_path: &Path, path: &Path) -> io::Result<()> {
fs::hard_link(temp_path, path)?;
fs::remove_file(temp_path)
}
fn unique_temp_path(parent: &Path, path: &Path) -> PathBuf {
let file_name = path
.file_name()
.map(|value| value.to_string_lossy())
.unwrap_or_else(|| "ccd".into());
let counter = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
let temp_name = format!(".{file_name}.ccd-tmp-{}-{counter}", process::id());
parent.join(temp_name)
}