kunobi_daemon/
publication.rs1use std::{
4 ffi::OsString,
5 fs::{File, OpenOptions},
6 io::{self, Read, Write},
7 path::{Path, PathBuf},
8 sync::atomic::{AtomicU64, Ordering},
9};
10
11static NEXT: AtomicU64 = AtomicU64::new(0);
12
13struct Temporary(PathBuf);
14impl Drop for Temporary {
15 fn drop(&mut self) {
16 let _ = std::fs::remove_file(&self.0);
17 }
18}
19
20pub fn publish_record(path: &Path, contents: &[u8]) -> io::Result<()> {
29 let parent = path
30 .parent()
31 .filter(|p| !p.as_os_str().is_empty())
32 .unwrap_or_else(|| Path::new("."));
33 let filename = path.file_name().ok_or_else(|| {
34 io::Error::new(
35 io::ErrorKind::InvalidInput,
36 "discovery path needs a filename",
37 )
38 })?;
39 let (temporary, mut file): (Temporary, File) = loop {
40 let mut name = OsString::from(".");
41 name.push(filename);
42 name.push(format!(
43 ".{}.{}.tmp",
44 std::process::id(),
45 NEXT.fetch_add(1, Ordering::Relaxed)
46 ));
47 let temporary = parent.join(name);
48 let mut options = OpenOptions::new();
49 options.create_new(true).write(true);
50 #[cfg(unix)]
51 {
52 use std::os::unix::fs::OpenOptionsExt;
53 options.mode(0o600);
54 }
55 match options.open(&temporary) {
56 Ok(file) => break (Temporary(temporary), file),
57 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
58 Err(e) => return Err(e),
59 }
60 };
61 #[cfg(unix)]
62 {
63 use std::os::unix::fs::PermissionsExt;
64 file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
65 }
66 file.write_all(contents)?;
67 file.sync_all()?;
68 drop(file);
69 std::fs::rename(&temporary.0, path)
70}
71
72#[derive(Clone, Debug)]
75pub struct RecordSlot(PathBuf);
76impl RecordSlot {
77 pub fn new(path: impl Into<PathBuf>) -> Self {
79 Self(path.into())
80 }
81
82 fn lock(&self) -> io::Result<crate::ProcessLock> {
83 let mut lock = self.0.as_os_str().to_owned();
84 lock.push(".record.lock");
85 crate::readiness::wait_until(
86 std::time::Instant::now() + std::time::Duration::from_secs(2),
87 |_| crate::ProcessLock::try_acquire(Path::new(&lock)),
88 )?
89 .ok_or_else(|| io::ErrorKind::TimedOut.into())
90 }
91
92 pub fn replace(&self, contents: &[u8]) -> io::Result<()> {
94 let _lock = self.lock()?;
95 publish_record(&self.0, contents)
96 }
97
98 pub fn repair(&self, contents: &[u8]) -> io::Result<bool> {
101 let _lock = self.lock()?;
102 if self.matches(contents)? {
103 return Ok(false);
104 }
105 publish_record(&self.0, contents)?;
106 Ok(true)
107 }
108
109 pub fn remove_if_matches(&self, contents: &[u8]) -> io::Result<bool> {
112 let _lock = self.lock()?;
113 if !self.matches(contents)? {
114 return Ok(false);
115 }
116 std::fs::remove_file(&self.0)?;
117 Ok(true)
118 }
119
120 fn matches(&self, contents: &[u8]) -> io::Result<bool> {
121 let file = match File::open(&self.0) {
122 Ok(file) => file,
123 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
124 Err(error) => return Err(error),
125 };
126 let mut current = Vec::new();
127 file.take(contents.len().saturating_add(1) as u64)
128 .read_to_end(&mut current)?;
129 Ok(current == contents)
130 }
131}