Skip to main content

digdigdig3_station/storage/
rotation.rs

1//! RotatingWriter — opens a daily file, rotates on UTC midnight boundary.
2//!
3//! File naming: `{dir}/{YYYY-MM-DD}.bin`
4
5use std::fs::{create_dir_all, File, OpenOptions};
6use std::io::{BufWriter, Write};
7use std::path::PathBuf;
8
9use byteorder::{LittleEndian, WriteBytesExt};
10use chrono::{NaiveDate, Utc};
11
12/// Reads binary records `[i64 ts_ms LE][u32 len LE][payload]` from a single daily file,
13/// filtering to `[from_ms, to_ms]` (inclusive).
14pub fn read_file_range(
15    path: &std::path::Path,
16    from_ms: i64,
17    to_ms: i64,
18) -> std::io::Result<Vec<(i64, Vec<u8>)>> {
19    use byteorder::ReadBytesExt;
20    use std::io::{BufReader, Read};
21
22    let file = File::open(path)?;
23    let mut reader = BufReader::new(file);
24    let mut out = Vec::new();
25
26    loop {
27        let ts = match reader.read_i64::<LittleEndian>() {
28            Ok(v) => v,
29            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
30            Err(e) => return Err(e),
31        };
32        let len = match reader.read_u32::<LittleEndian>() {
33            Ok(v) => v as usize,
34            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
35            Err(e) => return Err(e),
36        };
37        let mut payload = vec![0u8; len];
38        match reader.read_exact(&mut payload) {
39            Ok(()) => {
40                if ts >= from_ms && ts <= to_ms {
41                    out.push((ts, payload));
42                }
43            }
44            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
45            Err(e) => return Err(e),
46        }
47    }
48    Ok(out)
49}
50
51/// Wraps a daily `{YYYY-MM-DD}.bin` file with automatic rotation at UTC midnight.
52pub struct RotatingWriter {
53    dir: PathBuf,
54    current_day: NaiveDate,
55    writer: BufWriter<File>,
56}
57
58impl RotatingWriter {
59    /// Open (or create) today's daily file under `dir`.
60    pub fn new(dir: impl Into<PathBuf>) -> std::io::Result<Self> {
61        let dir = dir.into();
62        create_dir_all(&dir)?;
63        let today = Utc::now().date_naive();
64        let path = dir.join(format!("{}.bin", today.format("%Y-%m-%d")));
65        let file = OpenOptions::new()
66            .create(true)
67            .append(true)
68            .open(&path)?;
69        Ok(Self {
70            dir,
71            current_day: today,
72            writer: BufWriter::new(file),
73        })
74    }
75
76    /// Append one record. Rotates to a new file if UTC date has advanced.
77    pub fn append(&mut self, ts_ms: i64, payload: &[u8]) -> std::io::Result<()> {
78        let today = Utc::now().date_naive();
79        if today != self.current_day {
80            self.rotate(today)?;
81        }
82        self.writer.write_i64::<LittleEndian>(ts_ms)?;
83        self.writer.write_u32::<LittleEndian>(payload.len() as u32)?;
84        self.writer.write_all(payload)?;
85        Ok(())
86    }
87
88    /// Flush buffered data to the OS.
89    pub fn flush(&mut self) -> std::io::Result<()> {
90        self.writer.flush()
91    }
92
93    fn rotate(&mut self, new_day: NaiveDate) -> std::io::Result<()> {
94        self.writer.flush()?;
95        let path = self.dir.join(format!("{}.bin", new_day.format("%Y-%m-%d")));
96        let file = OpenOptions::new()
97            .create(true)
98            .append(true)
99            .open(&path)?;
100        self.writer = BufWriter::new(file);
101        self.current_day = new_day;
102        Ok(())
103    }
104
105    /// Force rotate to a specific date. Used in tests to simulate midnight crossing.
106    pub fn rotate_to(&mut self, day: NaiveDate) -> std::io::Result<()> {
107        self.rotate(day)
108    }
109
110    /// Return the current day this writer is writing to.
111    pub fn current_day(&self) -> NaiveDate {
112        self.current_day
113    }
114}