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 fn read_prefix(&self, file: FileId, n: usize) -> std::io::Result<Vec<u8>> {
51 let mut bytes = self.read(file)?;
52 bytes.truncate(n);
53 Ok(bytes)
54 }
55
56 fn list_archives(&self) -> std::io::Result<Vec<u64>> {
65 Ok(vec![])
66 }
67
68 fn read_archive(&self, _n: u64) -> std::io::Result<Vec<u8>> {
74 Ok(vec![])
75 }
76
77 fn archive_wal(&mut self, _n: u64) -> std::io::Result<()> {
87 Err(std::io::Error::other(
88 "archive_wal not supported by this Fs implementation",
89 ))
90 }
91
92 fn delete_archive(&mut self, _n: u64) -> std::io::Result<()> {
98 Ok(())
99 }
100
101 fn read_horizon_floor(&self) -> std::io::Result<u64> {
106 Ok(0)
107 }
108
109 fn write_horizon_floor(&mut self, _floor: u64) -> std::io::Result<()> {
114 Ok(())
115 }
116
117 fn has_genesis_marker(&self) -> bool {
126 false
127 }
128
129 fn write_genesis_marker(&mut self) -> std::io::Result<()> {
136 Ok(())
137 }
138
139 fn delete_genesis_marker(&mut self) -> std::io::Result<()> {
147 Ok(())
148 }
149}
150
151pub trait FsIntrospect {
152 fn total_appended(&self) -> usize;
153 fn sync_count(&self) -> usize {
154 0
155 }
156}
157
158#[derive(Debug)]
159pub struct RealFs {
160 dir: PathBuf,
161}
162
163impl RealFs {
164 pub fn new(dir: &std::path::Path) -> std::io::Result<Self> {
165 std::fs::create_dir_all(dir)?;
166 Ok(Self {
167 dir: dir.to_path_buf(),
168 })
169 }
170
171 fn path(&self, file: FileId) -> PathBuf {
172 self.dir.join(file.name())
173 }
174}
175
176impl Fs for RealFs {
177 fn append(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()> {
178 let mut f = OpenOptions::new()
179 .create(true)
180 .append(true)
181 .open(self.path(file))?;
182 f.write_all(data)
183 }
184
185 fn sync(&mut self, file: FileId) -> std::io::Result<()> {
186 let f = File::open(self.path(file))?;
187 full_sync(&f)
188 }
189
190 fn read(&self, file: FileId) -> std::io::Result<Vec<u8>> {
191 match File::open(self.path(file)) {
192 Ok(mut f) => {
193 let mut buf = Vec::new();
194 f.read_to_end(&mut buf)?;
195 Ok(buf)
196 }
197 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
198 Err(e) => Err(e),
199 }
200 }
201
202 fn write_atomic(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()> {
203 let tmp = self.dir.join(format!("{}.tmp", file.name()));
204 {
205 let mut f = File::create(&tmp)?;
206 f.write_all(data)?;
207 full_sync(&f)?;
208 }
209 std::fs::rename(&tmp, self.path(file))?;
210 sync_dir(&self.dir)
211 }
212
213 fn snapshot_path(&self) -> Option<std::path::PathBuf> {
214 Some(self.path(FileId::Snapshot))
215 }
216
217 fn read_prefix(&self, file: FileId, n: usize) -> std::io::Result<Vec<u8>> {
218 use std::io::Read as _;
219 match File::open(self.path(file)) {
220 Ok(mut f) => {
221 let mut buf = vec![0u8; n];
222 let read = f.read(&mut buf)?;
223 buf.truncate(read);
224 Ok(buf)
225 }
226 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
227 Err(e) => Err(e),
228 }
229 }
230
231 fn list_archives(&self) -> std::io::Result<Vec<u64>> {
232 let mut ns = Vec::new();
233 for entry in std::fs::read_dir(&self.dir)? {
234 let entry = entry?;
235 let name = entry.file_name();
236 let s = name.to_string_lossy();
237 if let Some(mid) = s
238 .strip_prefix("wal.")
239 .and_then(|r| r.strip_suffix(".archive"))
240 {
241 if let Ok(n) = mid.parse::<u64>() {
242 ns.push(n);
243 }
244 }
245 }
246 ns.sort_unstable();
247 Ok(ns)
248 }
249
250 fn read_archive(&self, n: u64) -> std::io::Result<Vec<u8>> {
251 let path = self.dir.join(format!("wal.{n}.archive"));
252 match std::fs::read(&path) {
253 Ok(b) => Ok(b),
254 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(vec![]),
255 Err(e) => Err(e),
256 }
257 }
258
259 fn archive_wal(&mut self, n: u64) -> std::io::Result<()> {
260 let wal_path = self.path(FileId::Wal);
261 let archive_path = self.dir.join(format!("wal.{n}.archive"));
262 std::fs::rename(&wal_path, &archive_path)?;
263 sync_dir(&self.dir)
264 }
265
266 fn delete_archive(&mut self, n: u64) -> std::io::Result<()> {
267 let path = self.dir.join(format!("wal.{n}.archive"));
268 match std::fs::remove_file(&path) {
269 Ok(()) => sync_dir(&self.dir),
270 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
271 Err(e) => Err(e),
272 }
273 }
274
275 fn read_horizon_floor(&self) -> std::io::Result<u64> {
276 let path = self.dir.join("wal.floor");
277 match std::fs::read(&path) {
278 Ok(b) if b.len() >= 8 => Ok(u64::from_le_bytes([
279 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
280 ])),
281 Ok(_) => Ok(0),
282 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(0),
283 Err(e) => Err(e),
284 }
285 }
286
287 fn write_horizon_floor(&mut self, floor: u64) -> std::io::Result<()> {
288 let tmp = self.dir.join("wal.floor.tmp");
289 {
290 let mut f = File::create(&tmp)?;
291 f.write_all(&floor.to_le_bytes())?;
292 full_sync(&f)?;
293 }
294 std::fs::rename(&tmp, self.dir.join("wal.floor"))?;
295 sync_dir(&self.dir)
296 }
297
298 fn has_genesis_marker(&self) -> bool {
299 self.dir.join("wal.genesis").exists()
300 }
301
302 fn write_genesis_marker(&mut self) -> std::io::Result<()> {
303 let path = self.dir.join("wal.genesis");
304 {
305 let mut f = File::create(&path)?;
306 f.write_all(b"")?;
307 full_sync(&f)?;
308 }
309 sync_dir(&self.dir)
310 }
311
312 fn delete_genesis_marker(&mut self) -> std::io::Result<()> {
313 match std::fs::remove_file(self.dir.join("wal.genesis")) {
314 Ok(()) => sync_dir(&self.dir),
315 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
316 Err(e) => Err(e),
317 }
318 }
319}
320
321fn full_sync(file: &File) -> std::io::Result<()> {
322 #[cfg(target_os = "macos")]
323 {
324 use std::os::unix::io::AsRawFd;
325 let fd = file.as_raw_fd();
326 let rc = unsafe { libc::fcntl(fd, libc::F_FULLFSYNC) };
327 if rc == -1 {
328 return Err(std::io::Error::last_os_error());
329 }
330 Ok(())
331 }
332 #[cfg(not(target_os = "macos"))]
333 {
334 file.sync_all()
335 }
336}
337
338pub fn sync_wal_at(dir: &std::path::Path) -> std::io::Result<()> {
346 let path = dir.join(FileId::Wal.name());
347 let f = match std::fs::File::open(&path) {
348 Ok(f) => f,
349 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
350 Err(e) => return Err(e),
351 };
352 full_sync(&f)
353}
354
355pub fn truncate_wal_at(dir: &std::path::Path, len: u64) -> std::io::Result<()> {
365 let path = dir.join(FileId::Wal.name());
366 let f = match OpenOptions::new().write(true).open(&path) {
367 Ok(f) => f,
368 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
369 Err(e) => return Err(e),
370 };
371 f.set_len(len)?;
372 f.sync_all() }
374
375fn sync_dir(dir: &std::path::Path) -> std::io::Result<()> {
376 let d = File::open(dir)?;
377 d.sync_all()
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 fn tmp() -> std::path::PathBuf {
385 let d = std::env::temp_dir().join(format!("graphdb-fs-{}", std::process::id()));
386 let _ = std::fs::remove_dir_all(&d);
387 d
388 }
389
390 #[test]
391 fn append_read_and_atomic_write() {
392 let mut fs = RealFs::new(&tmp()).unwrap();
393 assert_eq!(fs.read(FileId::Wal).unwrap(), Vec::<u8>::new()); fs.append(FileId::Wal, b"ab").unwrap();
395 fs.append(FileId::Wal, b"cd").unwrap();
396 fs.sync(FileId::Wal).unwrap();
397 assert_eq!(fs.read(FileId::Wal).unwrap(), b"abcd");
398 fs.write_atomic(FileId::Snapshot, b"snap1").unwrap();
399 fs.write_atomic(FileId::Snapshot, b"snap2").unwrap(); assert_eq!(fs.read(FileId::Snapshot).unwrap(), b"snap2");
401 fs.write_atomic(FileId::Wal, b"").unwrap(); assert_eq!(fs.read(FileId::Wal).unwrap(), Vec::<u8>::new());
403 }
404
405 #[test]
406 fn write_atomic_replaces_and_still_readable() {
407 let d = std::env::temp_dir().join(format!("graphdb-fs-atomic-{}", std::process::id()));
411 let _ = std::fs::remove_dir_all(&d);
412 let mut fs = RealFs::new(&d).unwrap();
413 fs.write_atomic(FileId::Snapshot, b"snap1").unwrap();
414 fs.write_atomic(FileId::Snapshot, b"snap2").unwrap();
415 assert_eq!(fs.read(FileId::Snapshot).unwrap(), b"snap2");
416 }
417}