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 try_lock_exclusive(&self) -> std::io::Result<bool> {
82 Ok(true)
83 }
84
85 fn unlock(&self) -> std::io::Result<()> {
89 Ok(())
90 }
91
92 fn wal_len(&self) -> std::io::Result<u64> {
101 Ok(self.read(FileId::Wal)?.len() as u64)
102 }
103
104 fn read_range(&self, file: FileId, from: u64) -> std::io::Result<Vec<u8>> {
113 let bytes = self.read(file)?;
114 let from = from.min(bytes.len() as u64) as usize;
115 Ok(bytes[from..].to_vec())
116 }
117
118 fn snapshot_ident(&self) -> std::io::Result<Option<(u64, u64)>> {
129 let len = self.read(FileId::Snapshot)?.len() as u64;
130 Ok(if len == 0 { None } else { Some((len, 0)) })
131 }
132
133 fn list_archives(&self) -> std::io::Result<Vec<u64>> {
142 Ok(vec![])
143 }
144
145 fn read_archive(&self, _n: u64) -> std::io::Result<Vec<u8>> {
151 Ok(vec![])
152 }
153
154 fn archive_wal(&mut self, _n: u64) -> std::io::Result<()> {
164 Err(std::io::Error::other(
165 "archive_wal not supported by this Fs implementation",
166 ))
167 }
168
169 fn delete_archive(&mut self, _n: u64) -> std::io::Result<()> {
175 Ok(())
176 }
177
178 fn read_horizon_floor(&self) -> std::io::Result<u64> {
183 Ok(0)
184 }
185
186 fn write_horizon_floor(&mut self, _floor: u64) -> std::io::Result<()> {
191 Ok(())
192 }
193
194 fn has_genesis_marker(&self) -> bool {
203 false
204 }
205
206 fn write_genesis_marker(&mut self) -> std::io::Result<()> {
213 Ok(())
214 }
215
216 fn delete_genesis_marker(&mut self) -> std::io::Result<()> {
224 Ok(())
225 }
226}
227
228pub trait FsIntrospect {
229 fn total_appended(&self) -> usize;
230 fn sync_count(&self) -> usize {
231 0
232 }
233}
234
235pub const LOCK_FILE: &str = "LOCK";
241
242#[derive(Debug, Default)]
250struct LockState {
251 file: Option<File>,
252 held: bool,
253}
254
255#[derive(Debug)]
256pub struct RealFs {
257 dir: PathBuf,
258 lock: std::sync::Mutex<LockState>,
263}
264
265impl RealFs {
266 pub fn new(dir: &std::path::Path) -> std::io::Result<Self> {
267 std::fs::create_dir_all(dir)?;
268 Ok(Self {
269 dir: dir.to_path_buf(),
270 lock: std::sync::Mutex::new(LockState::default()),
271 })
272 }
273
274 pub fn dir(&self) -> &std::path::Path {
276 &self.dir
277 }
278
279 fn path(&self, file: FileId) -> PathBuf {
280 self.dir.join(file.name())
281 }
282}
283
284impl Fs for RealFs {
285 fn append(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()> {
286 let mut f = OpenOptions::new()
287 .create(true)
288 .append(true)
289 .open(self.path(file))?;
290 f.write_all(data)
291 }
292
293 fn sync(&mut self, file: FileId) -> std::io::Result<()> {
294 let f = File::open(self.path(file))?;
295 full_sync(&f)
296 }
297
298 fn read(&self, file: FileId) -> std::io::Result<Vec<u8>> {
299 match File::open(self.path(file)) {
300 Ok(mut f) => {
301 let mut buf = Vec::new();
302 f.read_to_end(&mut buf)?;
303 Ok(buf)
304 }
305 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
306 Err(e) => Err(e),
307 }
308 }
309
310 fn write_atomic(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()> {
311 let tmp = self.dir.join(format!("{}.tmp", file.name()));
312 {
313 let mut f = File::create(&tmp)?;
314 f.write_all(data)?;
315 full_sync(&f)?;
316 }
317 std::fs::rename(&tmp, self.path(file))?;
318 sync_dir(&self.dir)
319 }
320
321 fn snapshot_path(&self) -> Option<std::path::PathBuf> {
322 Some(self.path(FileId::Snapshot))
323 }
324
325 fn wal_path(&self) -> Option<std::path::PathBuf> {
326 Some(self.path(FileId::Wal))
327 }
328
329 fn read_prefix(&self, file: FileId, n: usize) -> std::io::Result<Vec<u8>> {
330 use std::io::Read as _;
331 match File::open(self.path(file)) {
332 Ok(mut f) => {
333 let mut buf = vec![0u8; n];
334 let read = f.read(&mut buf)?;
335 buf.truncate(read);
336 Ok(buf)
337 }
338 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
339 Err(e) => Err(e),
340 }
341 }
342
343 fn try_lock_exclusive(&self) -> std::io::Result<bool> {
344 let mut state = self.lock.lock().unwrap_or_else(|e| e.into_inner());
345 if state.held {
346 return Ok(true);
347 }
348 if state.file.is_none() {
349 state.file = Some(
350 OpenOptions::new()
351 .create(true)
352 .read(true)
353 .write(true)
354 .truncate(false)
355 .open(self.dir.join(LOCK_FILE))?,
356 );
357 }
358 let f = state.file.as_ref().expect("lock file just opened");
359 match f.try_lock() {
360 Ok(()) => {
361 state.held = true;
362 Ok(true)
363 }
364 Err(std::fs::TryLockError::WouldBlock) => Ok(false),
365 Err(std::fs::TryLockError::Error(e)) => Err(e),
366 }
367 }
368
369 fn unlock(&self) -> std::io::Result<()> {
370 let mut state = self.lock.lock().unwrap_or_else(|e| e.into_inner());
371 if !state.held {
372 return Ok(());
373 }
374 state.held = false;
377 match state.file.as_ref() {
378 Some(f) => f.unlock(),
379 None => Ok(()),
380 }
381 }
382
383 fn wal_len(&self) -> std::io::Result<u64> {
384 match std::fs::metadata(self.path(FileId::Wal)) {
385 Ok(m) => Ok(m.len()),
386 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(0),
387 Err(e) => Err(e),
388 }
389 }
390
391 fn read_range(&self, file: FileId, from: u64) -> std::io::Result<Vec<u8>> {
392 use std::io::{Read as _, Seek as _, SeekFrom};
393 let mut f = match File::open(self.path(file)) {
394 Ok(f) => f,
395 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
396 Err(e) => return Err(e),
397 };
398 let len = f.metadata()?.len();
399 if from >= len {
400 return Ok(Vec::new());
401 }
402 f.seek(SeekFrom::Start(from))?;
403 let mut buf = Vec::with_capacity((len - from) as usize);
404 f.read_to_end(&mut buf)?;
405 Ok(buf)
406 }
407
408 fn snapshot_ident(&self) -> std::io::Result<Option<(u64, u64)>> {
409 let m = match std::fs::metadata(self.path(FileId::Snapshot)) {
410 Ok(m) => m,
411 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
412 Err(e) => return Err(e),
413 };
414 let mtime_nanos = m
417 .modified()
418 .ok()
419 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
420 .map(|d| d.as_nanos() as u64)
421 .unwrap_or(0);
422 Ok(Some((m.len(), mtime_nanos)))
423 }
424
425 fn list_archives(&self) -> std::io::Result<Vec<u64>> {
426 let mut ns = Vec::new();
427 for entry in std::fs::read_dir(&self.dir)? {
428 let entry = entry?;
429 let name = entry.file_name();
430 let s = name.to_string_lossy();
431 if let Some(mid) = s
432 .strip_prefix("wal.")
433 .and_then(|r| r.strip_suffix(".archive"))
434 {
435 if let Ok(n) = mid.parse::<u64>() {
436 ns.push(n);
437 }
438 }
439 }
440 ns.sort_unstable();
441 Ok(ns)
442 }
443
444 fn read_archive(&self, n: u64) -> std::io::Result<Vec<u8>> {
445 let path = self.dir.join(format!("wal.{n}.archive"));
446 match std::fs::read(&path) {
447 Ok(b) => Ok(b),
448 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(vec![]),
449 Err(e) => Err(e),
450 }
451 }
452
453 fn archive_wal(&mut self, n: u64) -> std::io::Result<()> {
454 let wal_path = self.path(FileId::Wal);
455 let archive_path = self.dir.join(format!("wal.{n}.archive"));
456 std::fs::rename(&wal_path, &archive_path)?;
457 sync_dir(&self.dir)
458 }
459
460 fn delete_archive(&mut self, n: u64) -> std::io::Result<()> {
461 let path = self.dir.join(format!("wal.{n}.archive"));
462 match std::fs::remove_file(&path) {
463 Ok(()) => sync_dir(&self.dir),
464 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
465 Err(e) => Err(e),
466 }
467 }
468
469 fn read_horizon_floor(&self) -> std::io::Result<u64> {
470 let path = self.dir.join("wal.floor");
471 match std::fs::read(&path) {
472 Ok(b) if b.len() >= 8 => Ok(u64::from_le_bytes([
473 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
474 ])),
475 Ok(_) => Ok(0),
476 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(0),
477 Err(e) => Err(e),
478 }
479 }
480
481 fn write_horizon_floor(&mut self, floor: u64) -> std::io::Result<()> {
482 let tmp = self.dir.join("wal.floor.tmp");
483 {
484 let mut f = File::create(&tmp)?;
485 f.write_all(&floor.to_le_bytes())?;
486 full_sync(&f)?;
487 }
488 std::fs::rename(&tmp, self.dir.join("wal.floor"))?;
489 sync_dir(&self.dir)
490 }
491
492 fn has_genesis_marker(&self) -> bool {
493 self.dir.join("wal.genesis").exists()
494 }
495
496 fn write_genesis_marker(&mut self) -> std::io::Result<()> {
497 let path = self.dir.join("wal.genesis");
498 {
499 let mut f = File::create(&path)?;
500 f.write_all(b"")?;
501 full_sync(&f)?;
502 }
503 sync_dir(&self.dir)
504 }
505
506 fn delete_genesis_marker(&mut self) -> std::io::Result<()> {
507 match std::fs::remove_file(self.dir.join("wal.genesis")) {
508 Ok(()) => sync_dir(&self.dir),
509 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
510 Err(e) => Err(e),
511 }
512 }
513}
514
515fn full_sync(file: &File) -> std::io::Result<()> {
516 #[cfg(target_os = "macos")]
517 {
518 use std::os::unix::io::AsRawFd;
519 let fd = file.as_raw_fd();
520 let rc = unsafe { libc::fcntl(fd, libc::F_FULLFSYNC) };
521 if rc == -1 {
522 return Err(std::io::Error::last_os_error());
523 }
524 Ok(())
525 }
526 #[cfg(not(target_os = "macos"))]
527 {
528 file.sync_all()
529 }
530}
531
532pub fn sync_wal_at(dir: &std::path::Path) -> std::io::Result<()> {
540 let path = dir.join(FileId::Wal.name());
541 let f = match std::fs::File::open(&path) {
542 Ok(f) => f,
543 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
544 Err(e) => return Err(e),
545 };
546 full_sync(&f)
547}
548
549pub fn truncate_wal_at(dir: &std::path::Path, len: u64) -> std::io::Result<()> {
559 let path = dir.join(FileId::Wal.name());
560 let f = match OpenOptions::new().write(true).open(&path) {
561 Ok(f) => f,
562 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
563 Err(e) => return Err(e),
564 };
565 f.set_len(len)?;
566 f.sync_all() }
568
569fn sync_dir(dir: &std::path::Path) -> std::io::Result<()> {
570 let d = File::open(dir)?;
571 d.sync_all()
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577
578 fn tmp() -> std::path::PathBuf {
579 let d = std::env::temp_dir().join(format!("graphdb-fs-{}", std::process::id()));
580 let _ = std::fs::remove_dir_all(&d);
581 d
582 }
583
584 #[test]
585 fn append_read_and_atomic_write() {
586 let mut fs = RealFs::new(&tmp()).unwrap();
587 assert_eq!(fs.read(FileId::Wal).unwrap(), Vec::<u8>::new()); fs.append(FileId::Wal, b"ab").unwrap();
589 fs.append(FileId::Wal, b"cd").unwrap();
590 fs.sync(FileId::Wal).unwrap();
591 assert_eq!(fs.read(FileId::Wal).unwrap(), b"abcd");
592 fs.write_atomic(FileId::Snapshot, b"snap1").unwrap();
593 fs.write_atomic(FileId::Snapshot, b"snap2").unwrap(); assert_eq!(fs.read(FileId::Snapshot).unwrap(), b"snap2");
595 fs.write_atomic(FileId::Wal, b"").unwrap(); assert_eq!(fs.read(FileId::Wal).unwrap(), Vec::<u8>::new());
597 }
598
599 #[test]
600 fn write_atomic_replaces_and_still_readable() {
601 let d = std::env::temp_dir().join(format!("graphdb-fs-atomic-{}", std::process::id()));
605 let _ = std::fs::remove_dir_all(&d);
606 let mut fs = RealFs::new(&d).unwrap();
607 fs.write_atomic(FileId::Snapshot, b"snap1").unwrap();
608 fs.write_atomic(FileId::Snapshot, b"snap2").unwrap();
609 assert_eq!(fs.read(FileId::Snapshot).unwrap(), b"snap2");
610 }
611}