1use std::fs::{File, OpenOptions};
2use std::io::{Read, Write};
3use std::path::PathBuf;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum FileId {
7 Wal,
8 Snapshot,
9 SnapshotBak,
13 Roles,
16}
17
18impl FileId {
19 fn name(self) -> &'static str {
20 match self {
21 FileId::Wal => "wal.bin",
22 FileId::Snapshot => "snapshot.bin",
23 FileId::SnapshotBak => "snapshot.bin.bak",
24 FileId::Roles => "roles.json",
25 }
26 }
27}
28
29pub trait Fs {
30 fn append(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()>;
31 fn sync(&mut self, file: FileId) -> std::io::Result<()>;
32 fn read(&self, file: FileId) -> std::io::Result<Vec<u8>>;
33 fn write_atomic(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()>;
34 fn snapshot_path(&self) -> Option<std::path::PathBuf> {
40 None
41 }
42
43 fn wal_path(&self) -> Option<std::path::PathBuf> {
48 None
49 }
50 fn read_prefix(&self, file: FileId, n: usize) -> std::io::Result<Vec<u8>> {
59 let mut bytes = self.read(file)?;
60 bytes.truncate(n);
61 Ok(bytes)
62 }
63
64 fn list_archives(&self) -> std::io::Result<Vec<u64>> {
73 Ok(vec![])
74 }
75
76 fn read_archive(&self, _n: u64) -> std::io::Result<Vec<u8>> {
82 Ok(vec![])
83 }
84
85 fn archive_wal(&mut self, _n: u64) -> std::io::Result<()> {
95 Err(std::io::Error::other(
96 "archive_wal not supported by this Fs implementation",
97 ))
98 }
99
100 fn delete_archive(&mut self, _n: u64) -> std::io::Result<()> {
106 Ok(())
107 }
108
109 fn read_horizon_floor(&self) -> std::io::Result<u64> {
114 Ok(0)
115 }
116
117 fn write_horizon_floor(&mut self, _floor: u64) -> std::io::Result<()> {
122 Ok(())
123 }
124
125 fn has_genesis_marker(&self) -> bool {
134 false
135 }
136
137 fn write_genesis_marker(&mut self) -> std::io::Result<()> {
144 Ok(())
145 }
146
147 fn delete_genesis_marker(&mut self) -> std::io::Result<()> {
155 Ok(())
156 }
157}
158
159pub trait FsIntrospect {
160 fn total_appended(&self) -> usize;
161 fn sync_count(&self) -> usize {
162 0
163 }
164}
165
166#[derive(Debug)]
167pub struct RealFs {
168 dir: PathBuf,
169}
170
171impl RealFs {
172 pub fn new(dir: &std::path::Path) -> std::io::Result<Self> {
173 std::fs::create_dir_all(dir)?;
174 Ok(Self {
175 dir: dir.to_path_buf(),
176 })
177 }
178
179 pub fn dir(&self) -> &std::path::Path {
181 &self.dir
182 }
183
184 fn path(&self, file: FileId) -> PathBuf {
185 self.dir.join(file.name())
186 }
187}
188
189impl Fs for RealFs {
190 fn append(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()> {
191 let mut f = OpenOptions::new()
192 .create(true)
193 .append(true)
194 .open(self.path(file))?;
195 f.write_all(data)
196 }
197
198 fn sync(&mut self, file: FileId) -> std::io::Result<()> {
199 let f = File::open(self.path(file))?;
200 full_sync(&f)
201 }
202
203 fn read(&self, file: FileId) -> std::io::Result<Vec<u8>> {
204 match File::open(self.path(file)) {
205 Ok(mut f) => {
206 let mut buf = Vec::new();
207 f.read_to_end(&mut buf)?;
208 Ok(buf)
209 }
210 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
211 Err(e) => Err(e),
212 }
213 }
214
215 fn write_atomic(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()> {
216 let tmp = self.dir.join(format!("{}.tmp", file.name()));
217 {
218 let mut f = File::create(&tmp)?;
219 f.write_all(data)?;
220 full_sync(&f)?;
221 }
222 std::fs::rename(&tmp, self.path(file))?;
223 sync_dir(&self.dir)
224 }
225
226 fn snapshot_path(&self) -> Option<std::path::PathBuf> {
227 Some(self.path(FileId::Snapshot))
228 }
229
230 fn wal_path(&self) -> Option<std::path::PathBuf> {
231 Some(self.path(FileId::Wal))
232 }
233
234 fn read_prefix(&self, file: FileId, n: usize) -> std::io::Result<Vec<u8>> {
235 use std::io::Read as _;
236 match File::open(self.path(file)) {
237 Ok(mut f) => {
238 let mut buf = vec![0u8; n];
239 let read = f.read(&mut buf)?;
240 buf.truncate(read);
241 Ok(buf)
242 }
243 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
244 Err(e) => Err(e),
245 }
246 }
247
248 fn list_archives(&self) -> std::io::Result<Vec<u64>> {
249 let mut ns = Vec::new();
250 for entry in std::fs::read_dir(&self.dir)? {
251 let entry = entry?;
252 let name = entry.file_name();
253 let s = name.to_string_lossy();
254 if let Some(mid) = s
255 .strip_prefix("wal.")
256 .and_then(|r| r.strip_suffix(".archive"))
257 {
258 if let Ok(n) = mid.parse::<u64>() {
259 ns.push(n);
260 }
261 }
262 }
263 ns.sort_unstable();
264 Ok(ns)
265 }
266
267 fn read_archive(&self, n: u64) -> std::io::Result<Vec<u8>> {
268 let path = self.dir.join(format!("wal.{n}.archive"));
269 match std::fs::read(&path) {
270 Ok(b) => Ok(b),
271 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(vec![]),
272 Err(e) => Err(e),
273 }
274 }
275
276 fn archive_wal(&mut self, n: u64) -> std::io::Result<()> {
277 let wal_path = self.path(FileId::Wal);
278 let archive_path = self.dir.join(format!("wal.{n}.archive"));
279 std::fs::rename(&wal_path, &archive_path)?;
280 sync_dir(&self.dir)
281 }
282
283 fn delete_archive(&mut self, n: u64) -> std::io::Result<()> {
284 let path = self.dir.join(format!("wal.{n}.archive"));
285 match std::fs::remove_file(&path) {
286 Ok(()) => sync_dir(&self.dir),
287 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
288 Err(e) => Err(e),
289 }
290 }
291
292 fn read_horizon_floor(&self) -> std::io::Result<u64> {
293 let path = self.dir.join("wal.floor");
294 match std::fs::read(&path) {
295 Ok(b) if b.len() >= 8 => Ok(u64::from_le_bytes([
296 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
297 ])),
298 Ok(_) => Ok(0),
299 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(0),
300 Err(e) => Err(e),
301 }
302 }
303
304 fn write_horizon_floor(&mut self, floor: u64) -> std::io::Result<()> {
305 let tmp = self.dir.join("wal.floor.tmp");
306 {
307 let mut f = File::create(&tmp)?;
308 f.write_all(&floor.to_le_bytes())?;
309 full_sync(&f)?;
310 }
311 std::fs::rename(&tmp, self.dir.join("wal.floor"))?;
312 sync_dir(&self.dir)
313 }
314
315 fn has_genesis_marker(&self) -> bool {
316 self.dir.join("wal.genesis").exists()
317 }
318
319 fn write_genesis_marker(&mut self) -> std::io::Result<()> {
320 let path = self.dir.join("wal.genesis");
321 {
322 let mut f = File::create(&path)?;
323 f.write_all(b"")?;
324 full_sync(&f)?;
325 }
326 sync_dir(&self.dir)
327 }
328
329 fn delete_genesis_marker(&mut self) -> std::io::Result<()> {
330 match std::fs::remove_file(self.dir.join("wal.genesis")) {
331 Ok(()) => sync_dir(&self.dir),
332 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
333 Err(e) => Err(e),
334 }
335 }
336}
337
338fn full_sync(file: &File) -> std::io::Result<()> {
339 #[cfg(target_os = "macos")]
340 {
341 use std::os::unix::io::AsRawFd;
342 let fd = file.as_raw_fd();
343 let rc = unsafe { libc::fcntl(fd, libc::F_FULLFSYNC) };
344 if rc == -1 {
345 return Err(std::io::Error::last_os_error());
346 }
347 Ok(())
348 }
349 #[cfg(not(target_os = "macos"))]
350 {
351 file.sync_all()
352 }
353}
354
355pub fn sync_wal_at(dir: &std::path::Path) -> std::io::Result<()> {
363 let path = dir.join(FileId::Wal.name());
364 let f = match std::fs::File::open(&path) {
365 Ok(f) => f,
366 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
367 Err(e) => return Err(e),
368 };
369 full_sync(&f)
370}
371
372pub fn truncate_wal_at(dir: &std::path::Path, len: u64) -> std::io::Result<()> {
382 let path = dir.join(FileId::Wal.name());
383 let f = match OpenOptions::new().write(true).open(&path) {
384 Ok(f) => f,
385 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
386 Err(e) => return Err(e),
387 };
388 f.set_len(len)?;
389 f.sync_all() }
391
392fn sync_dir(dir: &std::path::Path) -> std::io::Result<()> {
393 let d = File::open(dir)?;
394 d.sync_all()
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 fn tmp() -> std::path::PathBuf {
402 let d = std::env::temp_dir().join(format!("graphdb-fs-{}", std::process::id()));
403 let _ = std::fs::remove_dir_all(&d);
404 d
405 }
406
407 #[test]
408 fn append_read_and_atomic_write() {
409 let mut fs = RealFs::new(&tmp()).unwrap();
410 assert_eq!(fs.read(FileId::Wal).unwrap(), Vec::<u8>::new()); fs.append(FileId::Wal, b"ab").unwrap();
412 fs.append(FileId::Wal, b"cd").unwrap();
413 fs.sync(FileId::Wal).unwrap();
414 assert_eq!(fs.read(FileId::Wal).unwrap(), b"abcd");
415 fs.write_atomic(FileId::Snapshot, b"snap1").unwrap();
416 fs.write_atomic(FileId::Snapshot, b"snap2").unwrap(); assert_eq!(fs.read(FileId::Snapshot).unwrap(), b"snap2");
418 fs.write_atomic(FileId::Wal, b"").unwrap(); assert_eq!(fs.read(FileId::Wal).unwrap(), Vec::<u8>::new());
420 }
421
422 #[test]
423 fn write_atomic_replaces_and_still_readable() {
424 let d = std::env::temp_dir().join(format!("graphdb-fs-atomic-{}", std::process::id()));
428 let _ = std::fs::remove_dir_all(&d);
429 let mut fs = RealFs::new(&d).unwrap();
430 fs.write_atomic(FileId::Snapshot, b"snap1").unwrap();
431 fs.write_atomic(FileId::Snapshot, b"snap2").unwrap();
432 assert_eq!(fs.read(FileId::Snapshot).unwrap(), b"snap2");
433 }
434}