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 =
38 unsafe { libc::flock(std::os::unix::io::AsRawFd::as_raw_fd(&file), libc::LOCK_EX) };
39 if ret != 0 {
40 return Err(io::Error::last_os_error());
41 }
42
43 Ok(FileLock {
44 lock_path,
45 _file: file,
46 })
47 }
48
49 #[cfg(not(unix))]
50 {
51 let file = fs::OpenOptions::new()
53 .write(true)
54 .create_new(true)
55 .open(&lock_path)
56 .or_else(|_| {
57 std::thread::sleep(std::time::Duration::from_millis(100));
59 fs::remove_file(&lock_path).ok();
60 fs::OpenOptions::new()
61 .write(true)
62 .create_new(true)
63 .open(&lock_path)
64 })?;
65 Ok(FileLock {
66 lock_path,
67 _file: file,
68 })
69 }
70 }
71}
72
73impl Drop for FileLock {
74 fn drop(&mut self) {
75 let _ = fs::remove_file(&self.lock_path);
78 }
79}
80
81pub fn atomic_write(path: &Path, content: &[u8]) -> io::Result<()> {
85 debug!("Atomic write: {}", path.display());
86 if let Some(parent) = path.parent() {
88 fs::create_dir_all(parent)?;
89 }
90
91 let mut tmp_name = path.file_name().unwrap_or_default().to_os_string();
92 tmp_name.push(format!(".purple_tmp.{}", std::process::id()));
93 let tmp_path = path.with_file_name(tmp_name);
94
95 #[cfg(unix)]
96 {
97 use std::io::Write;
98 use std::os::unix::fs::OpenOptionsExt;
99 let open = || {
102 fs::OpenOptions::new()
103 .write(true)
104 .create_new(true)
105 .mode(0o600)
106 .open(&tmp_path)
107 };
108 let mut file = match open() {
109 Ok(f) => f,
110 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
111 let _ = fs::remove_file(&tmp_path);
112 open().map_err(|e| {
113 io::Error::new(
114 e.kind(),
115 format!("Failed to create temp file {}: {}", tmp_path.display(), e),
116 )
117 })?
118 }
119 Err(e) => {
120 return Err(io::Error::new(
121 e.kind(),
122 format!("Failed to create temp file {}: {}", tmp_path.display(), e),
123 ));
124 }
125 };
126 if let Err(e) = file.write_all(content) {
127 drop(file);
128 let _ = fs::remove_file(&tmp_path);
129 return Err(e);
130 }
131 if let Err(e) = file.sync_all() {
132 drop(file);
133 let _ = fs::remove_file(&tmp_path);
134 return Err(e);
135 }
136 }
137
138 #[cfg(not(unix))]
139 {
140 if let Err(e) = fs::write(&tmp_path, content) {
141 let _ = fs::remove_file(&tmp_path);
142 return Err(e);
143 }
144 match fs::File::open(&tmp_path) {
146 Ok(f) => {
147 if let Err(e) = f.sync_all() {
148 let _ = fs::remove_file(&tmp_path);
149 return Err(e);
150 }
151 }
152 Err(e) => {
153 let _ = fs::remove_file(&tmp_path);
154 return Err(e);
155 }
156 }
157 }
158
159 let result = fs::rename(&tmp_path, path);
160 if let Err(ref err) = result {
161 let _ = fs::remove_file(&tmp_path);
162 error!("[purple] Atomic write failed: {}: {err}", path.display());
163 }
164 result
165}