1use std::fs;
2use std::io;
3use std::path::{Path, PathBuf};
4
5use log::{debug, error};
6
7pub struct FileLock {
10 lock_path: PathBuf,
11 #[cfg(unix)]
12 _file: fs::File,
13}
14
15impl FileLock {
16 pub fn acquire(path: &Path) -> io::Result<Self> {
20 let mut lock_name = path.file_name().unwrap_or_default().to_os_string();
21 lock_name.push(".purple_lock");
22 let lock_path = path.with_file_name(lock_name);
23
24 #[cfg(unix)]
25 {
26 use std::os::unix::fs::OpenOptionsExt;
27 let file = fs::OpenOptions::new()
28 .write(true)
29 .create(true)
30 .truncate(false)
31 .mode(0o600)
32 .open(&lock_path)?;
33
34 let ret =
36 unsafe { libc::flock(std::os::unix::io::AsRawFd::as_raw_fd(&file), libc::LOCK_EX) };
37 if ret != 0 {
38 return Err(io::Error::last_os_error());
39 }
40
41 Ok(FileLock {
42 lock_path,
43 _file: file,
44 })
45 }
46
47 #[cfg(not(unix))]
48 {
49 let file = fs::OpenOptions::new()
51 .write(true)
52 .create_new(true)
53 .open(&lock_path)
54 .or_else(|_| {
55 std::thread::sleep(std::time::Duration::from_millis(100));
57 fs::remove_file(&lock_path).ok();
58 fs::OpenOptions::new()
59 .write(true)
60 .create_new(true)
61 .open(&lock_path)
62 })?;
63 Ok(FileLock {
64 lock_path,
65 _file: file,
66 })
67 }
68 }
69}
70
71impl Drop for FileLock {
72 fn drop(&mut self) {
73 let _ = fs::remove_file(&self.lock_path);
76 }
77}
78
79pub fn atomic_write(path: &Path, content: &[u8]) -> io::Result<()> {
83 debug!("Atomic write: {}", path.display());
84 if let Some(parent) = path.parent() {
86 fs::create_dir_all(parent)?;
87 }
88
89 let mut tmp_name = path.file_name().unwrap_or_default().to_os_string();
90 tmp_name.push(format!(".purple_tmp.{}", std::process::id()));
91 let tmp_path = path.with_file_name(tmp_name);
92
93 #[cfg(unix)]
94 {
95 use std::io::Write;
96 use std::os::unix::fs::OpenOptionsExt;
97 let open = || {
100 fs::OpenOptions::new()
101 .write(true)
102 .create_new(true)
103 .mode(0o600)
104 .open(&tmp_path)
105 };
106 let mut file = match open() {
107 Ok(f) => f,
108 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
109 let _ = fs::remove_file(&tmp_path);
110 open().map_err(|e| {
111 io::Error::new(
112 e.kind(),
113 format!("Failed to create temp file {}: {}", tmp_path.display(), e),
114 )
115 })?
116 }
117 Err(e) => {
118 return Err(io::Error::new(
119 e.kind(),
120 format!("Failed to create temp file {}: {}", tmp_path.display(), e),
121 ));
122 }
123 };
124 if let Err(e) = file.write_all(content) {
125 drop(file);
126 let _ = fs::remove_file(&tmp_path);
127 return Err(e);
128 }
129 if let Err(e) = file.sync_all() {
130 drop(file);
131 let _ = fs::remove_file(&tmp_path);
132 return Err(e);
133 }
134 }
135
136 #[cfg(not(unix))]
137 {
138 if let Err(e) = fs::write(&tmp_path, content) {
139 let _ = fs::remove_file(&tmp_path);
140 return Err(e);
141 }
142 match fs::File::open(&tmp_path) {
144 Ok(f) => {
145 if let Err(e) = f.sync_all() {
146 let _ = fs::remove_file(&tmp_path);
147 return Err(e);
148 }
149 }
150 Err(e) => {
151 let _ = fs::remove_file(&tmp_path);
152 return Err(e);
153 }
154 }
155 }
156
157 let result = fs::rename(&tmp_path, path);
158 if let Err(ref err) = result {
159 let _ = fs::remove_file(&tmp_path);
160 error!("[purple] Atomic write failed: {}: {err}", path.display());
161 }
162 result
163}