adana_db/
file_lock.rs

1use std::{
2    fmt::Display,
3    fs::{File, remove_file},
4    io::{BufReader, BufWriter, Write},
5    path::{Path, PathBuf},
6};
7
8use log::{debug, error};
9
10#[derive(Debug, Clone)]
11pub struct FileLock {
12    _lock_p: PathBuf,
13    inner_p: PathBuf,
14}
15
16fn pid_exists(pid: u32) -> bool {
17    Path::new(&format!("/proc/{pid}")).exists()
18}
19
20#[derive(Debug)]
21pub enum FileLockError {
22    PidExist(u32),
23    PidFileDoesntExist,
24    Unknown(String),
25}
26
27impl Display for FileLockError {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match self {
30            FileLockError::PidExist(pid) => {
31                write!(f, "Could not acquire lock (pid exists: {pid})")
32            }
33            FileLockError::PidFileDoesntExist => write!(
34                f,
35                "Lock exist but pid file doesn't! this is probably a bug."
36            ),
37            FileLockError::Unknown(e) => write!(f, "{e}"),
38        }
39    }
40}
41
42pub fn read_file(p: &PathBuf) -> anyhow::Result<BufReader<File>> {
43    let _inner = File::options().read(true).open(p)?;
44    let reader = BufReader::new(_inner);
45    Ok(reader)
46}
47
48impl FileLock {
49    pub fn get_path(&self) -> &PathBuf {
50        &self.inner_p
51    }
52    pub fn open<P: AsRef<Path>>(path: P) -> Result<FileLock, FileLockError> {
53        let _lock_p = path.as_ref().with_extension("lock");
54        let inner_p = path.as_ref().to_path_buf();
55        if Path::exists(&_lock_p) {
56            let pid = Self::read_pid(&path);
57
58            match pid {
59                Ok(pid) => {
60                    if pid_exists(pid) {
61                        error!("{pid} exist!");
62                        return Err(FileLockError::PidExist(pid));
63                    }
64                }
65                _ => {
66                    return Err(FileLockError::PidFileDoesntExist);
67                }
68            }
69
70            // otherwise, we create a file lock to force cleanup
71            let _ = {
72                let _ = FileLock {
73                    _lock_p: _lock_p.clone(),
74                    inner_p: inner_p.clone(),
75                };
76                Some(())
77            };
78        }
79        // create files if not exist
80        let _ = File::options()
81            .create(true)
82            .append(true)
83            .open(&path)
84            .map_err(|e| FileLockError::Unknown(e.to_string()))?;
85        let _ = File::create(&_lock_p)
86            .map_err(|e| FileLockError::Unknown(e.to_string()))?;
87        Self::write_pid(&path)
88            .map_err(|e| FileLockError::Unknown(e.to_string()))?;
89
90        std::fs::copy(&path, &_lock_p)
91            .map_err(|e| FileLockError::Unknown(e.to_string()))?;
92
93        Ok(FileLock { _lock_p, inner_p })
94    }
95
96    pub fn read(&self) -> anyhow::Result<BufReader<File>> {
97        read_file(&self.inner_p)
98    }
99
100    pub fn write(&self, buf: &[u8]) -> anyhow::Result<()> {
101        let _lock = File::create(&self._lock_p)?;
102        let mut writer = BufWriter::new(_lock);
103        writer.write_all(buf)?;
104        writer.flush()?;
105        Ok(())
106    }
107
108    fn write_pid<P: AsRef<Path>>(path: P) -> anyhow::Result<()> {
109        let pid_p = path.as_ref().with_extension("pid");
110        let pid_id = std::process::id();
111        std::fs::write(pid_p, pid_id.to_string().as_bytes())?;
112        Ok(())
113    }
114
115    fn read_pid<P: AsRef<Path>>(path: P) -> anyhow::Result<u32> {
116        let pid_p = path.as_ref().with_extension("pid");
117        let pid = std::fs::read_to_string(pid_p)?;
118        Ok(str::parse::<u32>(&pid)?)
119    }
120
121    pub fn flush(&self) -> anyhow::Result<()> {
122        debug!("flush file");
123        let swp = &self.inner_p.with_extension("swp");
124        let _ = File::create(swp)?;
125        let _ = File::options()
126            .write(true)
127            .create(true)
128            .truncate(true)
129            .open(&self.inner_p)
130            .unwrap();
131
132        std::fs::rename(&self.inner_p, swp)?;
133
134        std::fs::copy(&self._lock_p, &self.inner_p)
135            .map_err(|e| anyhow::format_err!("{e}"))?;
136
137        remove_file(swp)?;
138        Ok(())
139    }
140
141    fn cleanup_and_flush(&mut self) -> anyhow::Result<()> {
142        debug!("remove lock for {}", self._lock_p.as_path().to_string_lossy());
143
144        let pid = &self.inner_p.with_extension("pid");
145
146        self.flush()?;
147
148        remove_file(&self._lock_p)?;
149        remove_file(pid)?;
150
151        Ok(())
152    }
153}
154
155impl Drop for FileLock {
156    fn drop(&mut self) {
157        self.cleanup_and_flush().unwrap();
158    }
159}
160
161#[cfg(test)]
162mod test {
163    use std::io::BufRead;
164
165    use super::FileLock;
166
167    #[test]
168    fn test_lock() {
169        let path = "/tmp/db.json";
170        let file = FileLock::open(path).unwrap();
171        let text = "\ni wanna wanna way";
172
173        file.write(text.as_bytes()).unwrap();
174
175        let mut reader = file.read().unwrap();
176        let mut line = String::new();
177        let len = reader.read_line(&mut line).unwrap();
178        println!("First line is {len} bytes long");
179
180        let open_file_twice = FileLock::open(path);
181
182        if let Err(e) = open_file_twice {
183            assert!(
184                e.to_string()
185                    .starts_with("Could not acquire lock (pid exists: ")
186            );
187        }
188    }
189}