bacnet_objects/log_persistence/
file.rs1use crate::prelude::*;
4use std::fs::{File, OpenOptions};
5use std::io::{ErrorKind, Read, Write};
6use std::path::{Path, PathBuf};
7
8use bacnet_types::constructed::{BACnetEventLogRecord, BACnetLogMultipleRecord, BACnetLogRecord};
9use bacnet_types::error::Error;
10use bacnet_types::primitives::ObjectIdentifier;
11
12use super::codec::{self, Decoded, MAX_SNAPSHOT_BYTES, SnapshotRecord};
13use super::{LogPersistence, LogSnapshot};
14
15#[derive(Clone, Debug)]
33pub struct FileLogPersistence {
34 slot_paths: [PathBuf; 2],
35}
36
37impl FileLogPersistence {
38 pub fn new(base_path: impl AsRef<Path>) -> Result<Self, Error> {
41 let base = base_path.as_ref().as_os_str();
42 if base.is_empty() {
43 return Err(Error::OutOfRange(
44 "log persistence base path must not be empty".into(),
45 ));
46 }
47 let slot = |suffix: &str| {
48 let mut path = base.to_os_string();
49 path.push(suffix);
50 PathBuf::from(path)
51 };
52 Ok(Self {
53 slot_paths: [slot(".slot0"), slot(".slot1")],
54 })
55 }
56
57 pub fn slot_paths(&self) -> [PathBuf; 2] {
59 self.slot_paths.clone()
60 }
61
62 fn load_slots<R: SnapshotRecord>(
63 &self,
64 expected: ObjectIdentifier,
65 ) -> Result<Option<LogSnapshot<R>>, Error> {
66 let mut newest: Option<LogSnapshot<R>> = None;
67 let mut damage = None;
68 for path in &self.slot_paths {
69 let Some(data) = read_slot(path)? else {
70 continue;
71 };
72 match codec::decode::<R>(&data, expected) {
73 Decoded::Refused(error) => return Err(error),
74 Decoded::Corrupt(error) => {
75 log_warn!(slot = %path.display(), %error, "Passing over a damaged log snapshot slot");
76 damage = Some(error);
77 }
78 Decoded::Valid(snapshot) => match &newest {
79 Some(current) if current.generation > snapshot.generation => {}
80 Some(current) if current.generation == snapshot.generation => {
81 if *current != snapshot {
82 return Err(Error::Encoding(
83 "log snapshot slots hold different snapshots of one generation"
84 .into(),
85 ));
86 }
87 }
88 _ => newest = Some(snapshot),
89 },
90 }
91 }
92 match (newest, damage) {
93 (Some(snapshot), _) => Ok(Some(snapshot)),
94 (None, Some(error)) => Err(error),
95 (None, None) => Ok(None),
96 }
97 }
98
99 fn commit_slot<R: SnapshotRecord>(&self, snapshot: &LogSnapshot<R>) -> Result<(), Error> {
100 let bytes = codec::encode(snapshot)?;
101 let path = &self.slot_paths[(snapshot.generation % 2) as usize];
102 if let Some(parent) = path
103 .parent()
104 .filter(|parent| !parent.as_os_str().is_empty())
105 && !parent.exists()
106 {
107 std::fs::create_dir_all(parent)?;
108 crate::durable::sync_parent_dir(parent);
111 }
112 let (mut file, created) = match OpenOptions::new().write(true).create_new(true).open(path) {
113 Ok(file) => (file, true),
114 Err(error) if error.kind() == ErrorKind::AlreadyExists => (
115 OpenOptions::new().write(true).truncate(true).open(path)?,
116 false,
117 ),
118 Err(error) => return Err(error.into()),
119 };
120 if let Err(error) = file.write_all(&bytes).and_then(|()| file.sync_all()) {
121 let _ = file.set_len(0);
123 let _ = file.sync_all();
124 return Err(error.into());
125 }
126 if created {
127 crate::durable::sync_parent_dir(path);
128 }
129 Ok(())
130 }
131}
132
133fn read_slot(path: &Path) -> Result<Option<Vec<u8>>, Error> {
137 let file = match File::open(path) {
138 Ok(file) => file,
139 Err(error) if error.kind() == ErrorKind::NotFound => return Ok(None),
140 Err(error) => return Err(error.into()),
141 };
142 let mut data = Vec::new();
143 file.take(MAX_SNAPSHOT_BYTES as u64 + 1)
144 .read_to_end(&mut data)?;
145 Ok(Some(data))
146}
147
148macro_rules! file_log_persistence {
149 ($($record:ty),*) => {$(
150 impl LogPersistence<$record> for FileLogPersistence {
151 fn load(
152 &self,
153 expected_object: ObjectIdentifier,
154 ) -> Result<Option<LogSnapshot<$record>>, Error> {
155 self.load_slots(expected_object)
156 }
157
158 fn commit(&self, snapshot: &LogSnapshot<$record>) -> Result<(), Error> {
159 self.commit_slot(snapshot)
160 }
161 }
162 )*};
163}
164
165file_log_persistence!(
166 BACnetLogRecord,
167 BACnetEventLogRecord,
168 BACnetLogMultipleRecord
169);