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}
10
11impl FileId {
12 fn name(self) -> &'static str {
13 match self {
14 FileId::Wal => "wal.bin",
15 FileId::Snapshot => "snapshot.bin",
16 }
17 }
18}
19
20pub trait Fs {
21 fn append(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()>;
22 fn sync(&mut self, file: FileId) -> std::io::Result<()>;
23 fn read(&self, file: FileId) -> std::io::Result<Vec<u8>>;
24 fn write_atomic(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()>;
25}
26
27pub trait FsIntrospect {
28 fn total_appended(&self) -> usize;
29 fn sync_count(&self) -> usize {
30 0
31 }
32}
33
34#[derive(Debug)]
35pub struct RealFs {
36 dir: PathBuf,
37}
38
39impl RealFs {
40 pub fn new(dir: &std::path::Path) -> std::io::Result<Self> {
41 std::fs::create_dir_all(dir)?;
42 Ok(Self {
43 dir: dir.to_path_buf(),
44 })
45 }
46
47 fn path(&self, file: FileId) -> PathBuf {
48 self.dir.join(file.name())
49 }
50}
51
52impl Fs for RealFs {
53 fn append(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()> {
54 let mut f = OpenOptions::new()
55 .create(true)
56 .append(true)
57 .open(self.path(file))?;
58 f.write_all(data)
59 }
60
61 fn sync(&mut self, file: FileId) -> std::io::Result<()> {
62 let f = File::open(self.path(file))?;
63 full_sync(&f)
64 }
65
66 fn read(&self, file: FileId) -> std::io::Result<Vec<u8>> {
67 match File::open(self.path(file)) {
68 Ok(mut f) => {
69 let mut buf = Vec::new();
70 f.read_to_end(&mut buf)?;
71 Ok(buf)
72 }
73 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
74 Err(e) => Err(e),
75 }
76 }
77
78 fn write_atomic(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()> {
79 let tmp = self.dir.join(format!("{}.tmp", file.name()));
80 {
81 let mut f = File::create(&tmp)?;
82 f.write_all(data)?;
83 full_sync(&f)?;
84 }
85 std::fs::rename(&tmp, self.path(file))?;
86 sync_dir(&self.dir)
87 }
88}
89
90fn full_sync(file: &File) -> std::io::Result<()> {
91 #[cfg(target_os = "macos")]
92 {
93 use std::os::unix::io::AsRawFd;
94 let fd = file.as_raw_fd();
95 let rc = unsafe { libc::fcntl(fd, libc::F_FULLFSYNC) };
96 if rc == -1 {
97 return Err(std::io::Error::last_os_error());
98 }
99 Ok(())
100 }
101 #[cfg(not(target_os = "macos"))]
102 {
103 file.sync_all()
104 }
105}
106
107fn sync_dir(dir: &std::path::Path) -> std::io::Result<()> {
108 let d = File::open(dir)?;
109 d.sync_all()
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 fn tmp() -> std::path::PathBuf {
117 let d = std::env::temp_dir().join(format!("graphdb-fs-{}", std::process::id()));
118 let _ = std::fs::remove_dir_all(&d);
119 d
120 }
121
122 #[test]
123 fn append_read_and_atomic_write() {
124 let mut fs = RealFs::new(&tmp()).unwrap();
125 assert_eq!(fs.read(FileId::Wal).unwrap(), Vec::<u8>::new()); fs.append(FileId::Wal, b"ab").unwrap();
127 fs.append(FileId::Wal, b"cd").unwrap();
128 fs.sync(FileId::Wal).unwrap();
129 assert_eq!(fs.read(FileId::Wal).unwrap(), b"abcd");
130 fs.write_atomic(FileId::Snapshot, b"snap1").unwrap();
131 fs.write_atomic(FileId::Snapshot, b"snap2").unwrap(); assert_eq!(fs.read(FileId::Snapshot).unwrap(), b"snap2");
133 fs.write_atomic(FileId::Wal, b"").unwrap(); assert_eq!(fs.read(FileId::Wal).unwrap(), Vec::<u8>::new());
135 }
136
137 #[test]
138 fn write_atomic_replaces_and_still_readable() {
139 let d = std::env::temp_dir().join(format!("graphdb-fs-atomic-{}", std::process::id()));
143 let _ = std::fs::remove_dir_all(&d);
144 let mut fs = RealFs::new(&d).unwrap();
145 fs.write_atomic(FileId::Snapshot, b"snap1").unwrap();
146 fs.write_atomic(FileId::Snapshot, b"snap2").unwrap();
147 assert_eq!(fs.read(FileId::Snapshot).unwrap(), b"snap2");
148 }
149}