use std::io::Write as _;
use std::os::unix::fs::PermissionsExt as _;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
const NAME_TRIES: usize = 16;
static NEXT_NAME: AtomicU64 = AtomicU64::new(0);
pub fn shared_mode(shared: i32, mode: u32) -> u32 {
let mut tweak: u32 = shared.unsigned_abs();
if mode & 0o200 == 0 {
tweak &= !0o222;
}
if mode & 0o100 != 0 {
tweak |= (tweak & 0o444) >> 2;
}
return if shared < 0 { (mode & !0o777) | tweak } else { mode | tweak };
}
fn temporary_beside(target: &Path) -> Option<PathBuf> {
let count: u64 = NEXT_NAME.fetch_add(1, Ordering::Relaxed);
let clock: u128 = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).ok()?.as_nanos();
return Some(target.with_file_name(format!("{}.reset-{}-{count}-{clock:x}", target.file_name()?.to_string_lossy(), std::process::id())));
}
pub struct Steps<'a> {
pub write: &'a dyn Fn(&mut std::fs::File, &[u8]) -> std::io::Result<usize>,
pub chmod: &'a dyn Fn(&std::fs::File, u32) -> std::io::Result<()>,
pub sync: &'a dyn Fn(&std::fs::File) -> std::io::Result<()>,
pub rename: &'a dyn Fn(&Path, &Path) -> std::io::Result<()>,
pub mode: &'a dyn Fn(&std::fs::File) -> std::io::Result<u32>,
}
fn mode_of_file(file: &std::fs::File) -> std::io::Result<u32> {
return Ok(file.metadata()?.permissions().mode());
}
fn write_some(file: &mut std::fs::File, bytes: &[u8]) -> std::io::Result<usize> {
return file.write(bytes);
}
fn set_mode(file: &std::fs::File, mode: u32) -> std::io::Result<()> {
return file.set_permissions(std::fs::Permissions::from_mode(mode));
}
fn sync_all(file: &std::fs::File) -> std::io::Result<()> {
return crate::service_durable::flush_index(file, "index-publish");
}
fn rename_file(from: &Path, to: &Path) -> std::io::Result<()> {
return std::fs::rename(from, to);
}
pub const SYSTEM: Steps<'static> = Steps { write: &write_some, chmod: &set_mode, sync: &sync_all, rename: &rename_file, mode: &mode_of_file };
fn create_beside(steps: &Steps<'_>, target: &Path, shared: i32) -> std::io::Result<(std::fs::File, PathBuf)> {
use std::os::unix::fs::OpenOptionsExt as _;
for _ in 0..NAME_TRIES {
let Some(temporary) = temporary_beside(target) else {
break;
};
let file: std::fs::File = match std::fs::OpenOptions::new().write(true).create_new(true).mode(0o666).open(&temporary) {
Ok(created) => created,
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(error) => return Err(error),
};
if shared != 0 {
let changed: std::io::Result<()> = (steps.mode)(&file).and_then(|gits_file| { return (steps.chmod)(&file, shared_mode(shared, gits_file) & 0o7777); });
if let Err(error) = changed {
let _removed = std::fs::remove_file(&temporary);
return Err(error);
}
}
return Ok((file, temporary));
}
return Err(std::io::Error::other("no unused temporary name beside the index"));
}
fn write_all_with(steps: &Steps<'_>, file: &mut std::fs::File, bytes: &[u8]) -> std::io::Result<()> {
let mut rest: &[u8] = bytes;
while !rest.is_empty() {
let taken: usize = match (steps.write)(file, rest) {
Ok(count) => count,
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
Err(error) => return Err(error),
};
if taken == 0 {
return Err(std::io::Error::from(std::io::ErrorKind::WriteZero));
}
rest = &rest[taken..];
}
return Ok(());
}
pub fn publish_with(steps: &Steps<'_>, target: &Path, bytes: &[u8], shared: i32) -> std::io::Result<()> {
let (mut file, temporary) = create_beside(steps, target, shared)?;
let written: std::io::Result<()> = write_all_with(steps, &mut file, bytes).and_then(|()| { return (steps.sync)(&file); }).and_then(|()| { return (steps.rename)(&temporary, target); });
if written.is_err() {
let _removed = std::fs::remove_file(&temporary);
}
return written;
}
pub fn publish(target: &Path, bytes: &[u8], shared: i32) -> std::io::Result<()> {
return publish_with(&SYSTEM, target, bytes, shared);
}
#[cfg(test)]
#[path = "index_reset_publish_tests.rs"]
mod tests;