file_seq/
lib.rs

1//! Fail-safe file sequence
2//!
3//! Works by versioning values of sequences and throwing away all versions,
4//! but the current and the previous one.
5//!
6//! Inspired by [this Java implementation](https://commons.apache.org/proper/commons-transaction/apidocs/org/apache/commons/transaction/file/FileSequence.html)
7//!
8//! # Usage
9//!
10//! ```
11//! use file_seq::FileSeq;
12//! use std::path::Path;
13//!
14//! let dir = Path::new("/tmp/example");
15//! let initial_value = 1;
16//!
17//! let seq = FileSeq::new(&dir, initial_value).unwrap();
18//!
19//! // Get current value
20//! assert_eq!(initial_value, seq.value().unwrap());
21//!
22//! // Increment and get
23//! assert_eq!(initial_value + 1, seq.increment_and_get(1).unwrap());
24//!
25//! // Get and then increment
26//! assert_eq!(initial_value + 1, seq.get_and_increment(1).unwrap());
27//! assert_eq!(initial_value + 2, seq.value().unwrap());
28//! ```
29
30use std::fs;
31use std::io::{Error, ErrorKind, Read};
32use std::path::{Path, PathBuf};
33
34use log::warn;
35
36#[derive(Debug)]
37pub struct FileSeq {
38    path_1: PathBuf,
39    path_2: PathBuf,
40}
41
42impl FileSeq {
43    pub fn new<P: AsRef<Path>>(store_dir: P, initial_value: u64) -> std::io::Result<Self> {
44        let store_path = store_dir.as_ref();
45
46        fs::create_dir_all(store_path)?;
47        let store_path_buf = store_path.to_path_buf();
48        let path_1 = store_path_buf.join("_1.seq");
49        let path_2 = store_path_buf.join("_2.seq");
50
51        let seq = Self { path_1, path_2 };
52
53        seq.initialize_if_necessary(initial_value)?;
54
55        Ok(seq)
56    }
57
58    fn initialize_if_necessary(&self, initial_value: u64) -> std::io::Result<()> {
59        if fs::metadata(&self.path_1).is_ok() || fs::metadata(&self.path_2).is_ok() {
60            Ok(())
61        } else {
62            self.write(initial_value)
63        }
64    }
65
66    /// Deletes this sequence
67    ///
68    /// Once deleted, the sequence must be recreated
69    ///
70    /// # Example
71    ///
72    /// ```
73    /// use file_seq::FileSeq;
74    /// use std::path::Path;
75    ///
76    /// let dir = Path::new("/tmp/example_delete");
77    /// let initial_value = 1;
78    ///
79    /// let seq = FileSeq::new(&dir, initial_value).unwrap();
80    ///
81    /// // Get current value
82    /// assert_eq!(initial_value, seq.value().unwrap());
83    ///
84    /// seq.delete();
85    ///
86    /// // Attempts to read the sequence after it's deleted returns an error
87    /// assert_eq!(seq.value().is_err(), true)
88    /// ```
89    pub fn delete(&self) -> () {
90        // The files might not exist already
91        let _ = fs::remove_file(&self.path_1);
92        let _ = fs::remove_file(&self.path_2);
93    }
94
95    /// Returns the current value of the sequence and then increments it.
96    ///
97    /// # Example
98    ///
99    /// ```
100    /// use file_seq::FileSeq;
101    /// use std::path::Path;
102    ///
103    /// let dir = Path::new("/tmp/example_get_and_increment");
104    /// let initial_value = 1;
105    ///
106    /// let seq = FileSeq::new(&dir, initial_value).unwrap();
107    ///
108    /// assert_eq!(initial_value, seq.get_and_increment(1).unwrap());
109    /// assert_eq!(initial_value + 1, seq.value().unwrap());
110    ///
111    /// ```
112    pub fn get_and_increment(&self, increment: u64) -> std::io::Result<u64> {
113        let value = self.read()?;
114        self.write(value + increment)?;
115        Ok(value)
116    }
117
118    /// Increments the sequence and return the value.
119    ///
120    /// # Example
121    ///
122    /// ```
123    /// use file_seq::FileSeq;
124    /// use std::path::Path;
125    ///
126    /// let dir = Path::new("/tmp/example_increment_and_get");
127    /// let initial_value = 1;
128    ///
129    /// let seq = FileSeq::new(&dir, initial_value).unwrap();
130    ///
131    /// assert_eq!(initial_value + 1, seq.increment_and_get(1).unwrap());
132    /// assert_eq!(initial_value + 1, seq.value().unwrap());
133    ///
134    /// ```
135    pub fn increment_and_get(&self, increment: u64) -> std::io::Result<u64> {
136        let value = self.get_and_increment(increment)?;
137        Ok(value + increment)
138    }
139
140    /// Returns the current value of the sequence.
141    ///
142    /// # Example
143    ///
144    /// ```
145    /// use file_seq::FileSeq;
146    /// use std::path::Path;
147    ///
148    /// let dir = Path::new("/tmp/example_value");
149    /// let initial_value = 1;
150    ///
151    /// let seq = FileSeq::new(&dir, initial_value).unwrap();
152    ///
153    /// assert_eq!(initial_value, seq.value().unwrap());
154    ///
155    /// ```
156    pub fn value(&self) -> std::io::Result<u64> {
157        self.read()
158    }
159
160    fn read(&self) -> std::io::Result<u64> {
161        let mut value1: Option<u64> = None;
162        if fs::metadata(&self.path_1).is_ok() {
163            let value = self.read_from_path(&self.path_1)?;
164            value1 = Some(value);
165        }
166
167        let mut value2: Option<u64> = None;
168        if fs::metadata(&self.path_2).is_ok() {
169            value2 = self.read_from_path(&self.path_2).ok();
170        }
171
172        match value2 {
173            Some(v2) => match value1 {
174                Some(v1) => {
175                    if v2 > v1 {
176                        Ok(v2)
177                    } else {
178                        warn!("Latest sequence value is smaller than backup, using backup.");
179                        fs::remove_file(&self.path_2).ok();
180                        Ok(v1)
181                    }
182                }
183                None => Ok(v2),
184            },
185            None => {
186                fs::remove_file(&self.path_2).ok();
187
188                match value1 {
189                    Some(v1) => Ok(v1),
190                    None => Err(Error::new(
191                        ErrorKind::InvalidData,
192                        "Looks like both backup and latest sequence files are corrupted.",
193                    )),
194                }
195            }
196        }
197    }
198
199    fn read_from_path<P: AsRef<Path>>(&self, path: P) -> std::io::Result<u64> {
200        let mut buff = [0; 8];
201        let mut f = fs::File::open(path.as_ref())?;
202        f.read_exact(&mut buff)?;
203        let value = u64::from_be_bytes(buff);
204        Ok(value)
205    }
206
207    fn write(&self, value: u64) -> std::io::Result<()> {
208        if fs::metadata(&self.path_2).is_ok() {
209            fs::rename(&self.path_2, &self.path_1)?;
210        }
211        self.write_to_path(&self.path_2, value)
212    }
213
214    fn write_to_path<P: AsRef<Path>>(&self, path: P, value: u64) -> std::io::Result<()> {
215        fs::write(path.as_ref(), value.to_be_bytes())
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use std::env;
222    use std::fs;
223    use std::path::PathBuf;
224
225    use rand::RngCore;
226
227    use crate::FileSeq;
228
229    pub fn tmpdir() -> PathBuf {
230        let p = env::temp_dir();
231        let mut r = rand::thread_rng();
232        let ret = p.join(&format!("file-seq-{}", r.next_u32()));
233        fs::create_dir(&ret).unwrap();
234        ret
235    }
236
237    #[test]
238    fn should_store_initial_seq_correctly() {
239        let dir = tmpdir();
240        let seq = FileSeq::new(&dir, 1).unwrap();
241        assert!(std::fs::metadata(dir).is_ok());
242        assert!(std::fs::metadata(seq.path_2).is_ok());
243    }
244
245    #[test]
246    fn should_cycle_seq_files() {
247        let dir = tmpdir();
248        let seq = FileSeq::new(&dir, 1).unwrap();
249        assert!(std::fs::metadata(dir).is_ok());
250        assert!(std::fs::metadata(&seq.path_2).is_ok());
251        let path_2_value = std::fs::read(&seq.path_2).unwrap();
252        seq.increment_and_get(1).unwrap();
253        let path_1_value = std::fs::read(&seq.path_1).unwrap();
254        assert_eq!(path_2_value, path_1_value);
255    }
256
257    #[test]
258    fn should_delete() {
259        let dir = tmpdir();
260        let seq = FileSeq::new(&dir, 1).unwrap();
261        assert!(std::fs::metadata(dir).is_ok());
262        assert!(std::fs::metadata(&seq.path_2).is_ok());
263        seq.increment_and_get(1).unwrap();
264        seq.delete();
265        assert!(!std::fs::metadata(&seq.path_1).is_ok());
266        assert!(!std::fs::metadata(&seq.path_2).is_ok());
267    }
268
269    #[test]
270    fn should_increment_and_get() {
271        let dir = tmpdir();
272        let seq = FileSeq::new(dir, 1).unwrap();
273        let prev_value = seq.value().unwrap();
274        let curr_value = seq.increment_and_get(1).unwrap();
275        assert_eq!(prev_value + 1, curr_value);
276        assert_eq!(curr_value, seq.value().unwrap());
277    }
278
279    #[test]
280    fn should_get_and_increment() {
281        let dir = tmpdir();
282        let seq = FileSeq::new(dir, 1).unwrap();
283        let prev_value = seq.value().unwrap();
284        let curr_value = seq.get_and_increment(1).unwrap();
285        assert_eq!(prev_value, curr_value);
286        assert_eq!(curr_value + 1, seq.value().unwrap())
287    }
288}