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 pub fn dir(&self) -> &std::path::Path {
173 &self.dir
174 }
175
176 fn path(&self, file: FileId) -> PathBuf {
177 self.dir.join(file.name())
178 }
179}
180
181impl Fs for RealFs {
182 fn append(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()> {
183 let mut f = OpenOptions::new()
184 .create(true)
185 .append(true)
186 .open(self.path(file))?;
187 f.write_all(data)
188 }
189
190 fn sync(&mut self, file: FileId) -> std::io::Result<()> {
191 let f = File::open(self.path(file))?;
192 full_sync(&f)
193 }
194
195 fn read(&self, file: FileId) -> std::io::Result<Vec<u8>> {
196 match File::open(self.path(file)) {
197 Ok(mut f) => {
198 let mut buf = Vec::new();
199 f.read_to_end(&mut buf)?;
200 Ok(buf)
201 }
202 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
203 Err(e) => Err(e),
204 }
205 }
206
207 fn write_atomic(&mut self, file: FileId, data: &[u8]) -> std::io::Result<()> {
208 let tmp = self.dir.join(format!("{}.tmp", file.name()));
209 {
210 let mut f = File::create(&tmp)?;
211 f.write_all(data)?;
212 full_sync(&f)?;
213 }
214 std::fs::rename(&tmp, self.path(file))?;
215 sync_dir(&self.dir)
216 }
217
218 fn snapshot_path(&self) -> Option<std::path::PathBuf> {
219 Some(self.path(FileId::Snapshot))
220 }
221
222 fn read_prefix(&self, file: FileId, n: usize) -> std::io::Result<Vec<u8>> {
223 use std::io::Read as _;
224 match File::open(self.path(file)) {
225 Ok(mut f) => {
226 let mut buf = vec![0u8; n];
227 let read = f.read(&mut buf)?;
228 buf.truncate(read);
229 Ok(buf)
230 }
231 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
232 Err(e) => Err(e),
233 }
234 }
235
236 fn list_archives(&self) -> std::io::Result<Vec<u64>> {
237 let mut ns = Vec::new();
238 for entry in std::fs::read_dir(&self.dir)? {
239 let entry = entry?;
240 let name = entry.file_name();
241 let s = name.to_string_lossy();
242 if let Some(mid) = s
243 .strip_prefix("wal.")
244 .and_then(|r| r.strip_suffix(".archive"))
245 {
246 if let Ok(n) = mid.parse::<u64>() {
247 ns.push(n);
248 }
249 }
250 }
251 ns.sort_unstable();
252 Ok(ns)
253 }
254
255 fn read_archive(&self, n: u64) -> std::io::Result<Vec<u8>> {
256 let path = self.dir.join(format!("wal.{n}.archive"));
257 match std::fs::read(&path) {
258 Ok(b) => Ok(b),
259 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(vec![]),
260 Err(e) => Err(e),
261 }
262 }
263
264 fn archive_wal(&mut self, n: u64) -> std::io::Result<()> {
265 let wal_path = self.path(FileId::Wal);
266 let archive_path = self.dir.join(format!("wal.{n}.archive"));
267 std::fs::rename(&wal_path, &archive_path)?;
268 sync_dir(&self.dir)
269 }
270
271 fn delete_archive(&mut self, n: u64) -> std::io::Result<()> {
272 let path = self.dir.join(format!("wal.{n}.archive"));
273 match std::fs::remove_file(&path) {
274 Ok(()) => sync_dir(&self.dir),
275 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
276 Err(e) => Err(e),
277 }
278 }
279
280 fn read_horizon_floor(&self) -> std::io::Result<u64> {
281 let path = self.dir.join("wal.floor");
282 match std::fs::read(&path) {
283 Ok(b) if b.len() >= 8 => Ok(u64::from_le_bytes([
284 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
285 ])),
286 Ok(_) => Ok(0),
287 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(0),
288 Err(e) => Err(e),
289 }
290 }
291
292 fn write_horizon_floor(&mut self, floor: u64) -> std::io::Result<()> {
293 let tmp = self.dir.join("wal.floor.tmp");
294 {
295 let mut f = File::create(&tmp)?;
296 f.write_all(&floor.to_le_bytes())?;
297 full_sync(&f)?;
298 }
299 std::fs::rename(&tmp, self.dir.join("wal.floor"))?;
300 sync_dir(&self.dir)
301 }
302
303 fn has_genesis_marker(&self) -> bool {
304 self.dir.join("wal.genesis").exists()
305 }
306
307 fn write_genesis_marker(&mut self) -> std::io::Result<()> {
308 let path = self.dir.join("wal.genesis");
309 {
310 let mut f = File::create(&path)?;
311 f.write_all(b"")?;
312 full_sync(&f)?;
313 }
314 sync_dir(&self.dir)
315 }
316
317 fn delete_genesis_marker(&mut self) -> std::io::Result<()> {
318 match std::fs::remove_file(self.dir.join("wal.genesis")) {
319 Ok(()) => sync_dir(&self.dir),
320 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
321 Err(e) => Err(e),
322 }
323 }
324}
325
326fn full_sync(file: &File) -> std::io::Result<()> {
327 #[cfg(target_os = "macos")]
328 {
329 use std::os::unix::io::AsRawFd;
330 let fd = file.as_raw_fd();
331 let rc = unsafe { libc::fcntl(fd, libc::F_FULLFSYNC) };
332 if rc == -1 {
333 return Err(std::io::Error::last_os_error());
334 }
335 Ok(())
336 }
337 #[cfg(not(target_os = "macos"))]
338 {
339 file.sync_all()
340 }
341}
342
343pub fn sync_wal_at(dir: &std::path::Path) -> std::io::Result<()> {
351 let path = dir.join(FileId::Wal.name());
352 let f = match std::fs::File::open(&path) {
353 Ok(f) => f,
354 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
355 Err(e) => return Err(e),
356 };
357 full_sync(&f)
358}
359
360pub fn truncate_wal_at(dir: &std::path::Path, len: u64) -> std::io::Result<()> {
370 let path = dir.join(FileId::Wal.name());
371 let f = match OpenOptions::new().write(true).open(&path) {
372 Ok(f) => f,
373 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
374 Err(e) => return Err(e),
375 };
376 f.set_len(len)?;
377 f.sync_all() }
379
380fn sync_dir(dir: &std::path::Path) -> std::io::Result<()> {
381 let d = File::open(dir)?;
382 d.sync_all()
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 fn tmp() -> std::path::PathBuf {
390 let d = std::env::temp_dir().join(format!("graphdb-fs-{}", std::process::id()));
391 let _ = std::fs::remove_dir_all(&d);
392 d
393 }
394
395 #[test]
396 fn append_read_and_atomic_write() {
397 let mut fs = RealFs::new(&tmp()).unwrap();
398 assert_eq!(fs.read(FileId::Wal).unwrap(), Vec::<u8>::new()); fs.append(FileId::Wal, b"ab").unwrap();
400 fs.append(FileId::Wal, b"cd").unwrap();
401 fs.sync(FileId::Wal).unwrap();
402 assert_eq!(fs.read(FileId::Wal).unwrap(), b"abcd");
403 fs.write_atomic(FileId::Snapshot, b"snap1").unwrap();
404 fs.write_atomic(FileId::Snapshot, b"snap2").unwrap(); assert_eq!(fs.read(FileId::Snapshot).unwrap(), b"snap2");
406 fs.write_atomic(FileId::Wal, b"").unwrap(); assert_eq!(fs.read(FileId::Wal).unwrap(), Vec::<u8>::new());
408 }
409
410 #[test]
411 fn write_atomic_replaces_and_still_readable() {
412 let d = std::env::temp_dir().join(format!("graphdb-fs-atomic-{}", std::process::id()));
416 let _ = std::fs::remove_dir_all(&d);
417 let mut fs = RealFs::new(&d).unwrap();
418 fs.write_atomic(FileId::Snapshot, b"snap1").unwrap();
419 fs.write_atomic(FileId::Snapshot, b"snap2").unwrap();
420 assert_eq!(fs.read(FileId::Snapshot).unwrap(), b"snap2");
421 }
422}