1use std::{
2 borrow::Cow,
3 ffi::OsStr,
4 io, iter,
5 path::Path,
6 sync::Arc,
7 time::{Duration, SystemTime, UNIX_EPOCH},
8};
9
10use bytes::{Buf, Bytes};
11use conserve::{monitor::Monitor, Apath, Exclude, IndexEntry, Kind, ReadTree, StoredTree};
12use fuser::{FileAttr, FileType, Filesystem};
13use libc::{EINVAL, ENOENT};
14use log::{debug, error};
15use snafu::prelude::*;
16
17mod tree;
18
19use tree::FilesystemTree;
20
21use self::tree::INodeEntry;
22
23#[derive(Debug, Snafu)]
24enum Error {
25 #[snafu(display("INode {ino} does not exists"))]
26 NoExists {
27 ino: INode,
28 },
29 FilesystemTree {
30 source: tree::Error,
31 },
32 Conserve {
33 source: conserve::Error,
34 },
35 Transport {
36 source: conserve::transport::Error,
37 },
38 Io {
39 source: io::Error,
40 },
41}
42
43type Result<T> = std::result::Result<T, Error>;
44
45type EntryRef = Arc<Entry>;
46
47#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
48struct INode(u64);
49
50impl INode {
51 pub const ROOT: Self = Self(1);
52 pub const ROOT_PARENT: Self = Self(0);
53
54 fn from_u64(ino: u64) -> Option<Self> {
55 if ino == 0 {
56 return None;
57 }
58 INode(ino).into()
59 }
60
61 fn is_root(&self) -> bool {
62 self == &Self::ROOT
63 }
64}
65
66impl std::fmt::Display for INode {
67 #[inline]
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 write!(f, "0x{:x}/{}", self.0, self.0)
70 }
71}
72
73impl From<INode> for u64 {
74 fn from(value: INode) -> Self {
75 value.0
76 }
77}
78
79pub struct ConserveFilesystem {
80 tree: StoredTree,
81 fs: FilesystemTree,
82}
83
84impl ConserveFilesystem {
85 pub fn new(tree: StoredTree) -> Self {
86 let root_entry = tree
87 .band()
88 .index()
89 .iter_hunks()
90 .flatten()
91 .take(1)
92 .next()
93 .expect("root");
94
95 assert_eq!(root_entry.apath, "/");
96 let fs = FilesystemTree::new(root_entry);
97 Self { tree, fs }
98 }
99
100 fn lookup_child_by_name(&mut self, parent: INode, name: &OsStr) -> Result<Option<&EntryRef>> {
101 if !self.fs.is_dir_loaded(parent) {
102 let Some(parent_entry) = self
103 .fs
104 .lookup(parent)
105 .map(|INodeEntry { parent: _, entry }| entry.clone())
106 else {
107 return Ok(None);
108 };
109 self.load_dir(parent, parent_entry.apath())?;
110 }
111 Ok(self.fs.lookup_child_by_name(parent, name))
112 }
113
114 fn open_dir(&mut self, ino: INode) -> Result<()> {
115 let Some(dir_entry) = self
116 .fs
117 .lookup(ino)
118 .map(|INodeEntry { parent: _, entry }| entry)
119 .filter(|entry| entry.is_dir())
120 else {
121 return Err(Error::NoExists { ino });
122 };
123 if !self.fs.is_dir_loaded(dir_entry.ino) {
124 self.load_dir(ino, dir_entry.clone().apath())?;
125 }
126 Ok(())
127 }
128
129 fn load_dir(&mut self, ino: INode, dir_path: &Apath) -> Result<()> {
130 let iter = self
131 .tree
132 .iter_entries(dir_path.clone(), Exclude::nothing())
133 .context(ConserveSnafu)?
134 .skip(1)
135 .take_while(|entry| {
136 let path: &Path = entry.apath.as_ref();
137 let parent_path: &Path = dir_path.as_ref();
138 debug!("inspect entry path = {path:?} ?==? parent path = {parent_path:?}");
139 path.parent() == Some(parent_path)
140 });
141 for entry in iter {
142 debug!("insert entry parent = {ino}, entry = {entry:?}");
143 self.fs
144 .insert_entry(ino, entry)
145 .context(FilesystemTreeSnafu)?;
146 }
147
148 Ok(())
149 }
150
151 fn read_file(&self, ino: INode, mut offset: u64) -> Result<Option<Bytes>> {
152 let Some(entry) = self.fs.lookup_entry(ino).filter(|entry| entry.is_file()) else {
153 return Ok(None);
154 };
155
156 let addr = entry
157 .inner
158 .addrs
159 .iter()
160 .find(|addr| {
161 if offset > addr.len {
162 offset -= addr.len;
163 false
164 } else {
165 true
166 }
167 })
168 .ok_or_else(|| {
169 io::Error::new(
170 io::ErrorKind::UnexpectedEof,
171 "end of file reached before offset",
172 )
173 })
174 .context(IoSnafu)?;
175
176 let content = self
177 .tree
178 .block_dir()
179 .read_address(addr, DiscardMonitor::arc())
180 .context(ConserveSnafu)?;
181 Ok(content.slice((offset as usize)..).into())
182 }
183
184 fn list_dir(&self, ino: INode) -> Option<impl Iterator<Item = ListEntry<'_>>> {
185 let INodeEntry { parent, entry: dir } = self.fs.lookup(ino)?;
186 let dir = ListEntry {
187 ino: dir.ino,
188 name: ".".into(),
189 file_type: FileType::Directory,
190 };
191 let parent = ListEntry {
192 ino: *parent,
193 name: "..".into(),
194 file_type: FileType::Directory,
195 };
196
197 Some(
198 iter::once(dir)
199 .chain(iter::once(parent))
200 .chain(self.fs.children(ino).filter_map(|(name, entry)| {
201 ListEntry {
202 ino: entry.ino,
203 name: name.into(),
204 file_type: entry.file_type()?,
205 }
206 .into()
207 })),
208 )
209 }
210
211 fn get(&self, ino: INode) -> Option<&EntryRef> {
212 let INodeEntry { parent: _, entry } = self.fs.lookup(ino)?;
213 entry.into()
214 }
215
216 fn get_dir(&self, ino: INode) -> Option<&EntryRef> {
217 self.get(ino)
218 .filter(|entry| entry.file_type() == Some(FileType::Directory))
219 }
220}
221
222#[derive(Debug, Clone)]
223struct ListEntry<'s> {
224 ino: INode,
225 name: Cow<'s, str>,
226 file_type: FileType,
227}
228
229const TTL: Duration = Duration::from_secs(1);
230
231impl Filesystem for ConserveFilesystem {
232 fn init(
233 &mut self,
234 _req: &fuser::Request<'_>,
235 _config: &mut fuser::KernelConfig,
236 ) -> std::result::Result<(), libc::c_int> {
237 Ok(())
238 }
239
240 fn lookup(
241 &mut self,
242 _req: &fuser::Request<'_>,
243 parent: u64,
244 name: &std::ffi::OsStr,
245 reply: fuser::ReplyEntry,
246 ) {
247 debug!(
248 "lookup parent = {} name = {}",
249 parent,
250 name.to_str().unwrap_or_default()
251 );
252
253 let Some(parent) = INode::from_u64(parent) else {
254 reply.error(EINVAL);
255 return;
256 };
257
258 let Some(parent_dir) = self.get_dir(parent) else {
259 reply.error(ENOENT);
260 return;
261 };
262
263 debug!("loopkup parent dir {parent_dir:?} {name:?}");
264 match self.lookup_child_by_name(parent_dir.ino, name) {
265 Err(err) => {
266 error!("lookup child {name:?} in {parent} failed: {err}");
267 reply.error(EINVAL);
268 }
269 Ok(None) => reply.error(ENOENT),
270 Ok(Some(entry)) => {
271 let file_attr = entry.file_attr().unwrap();
272 debug!("lookup match {name:?} {file_attr:?}");
273 reply.entry(&TTL, &file_attr, 0);
274 }
275 }
276 }
277
278 fn read(
279 &mut self,
280 _req: &fuser::Request<'_>,
281 ino: u64,
282 fh: u64,
283 offset: i64,
284 size: u32,
285 flags: i32,
286 lock_owner: Option<u64>,
287 reply: fuser::ReplyData,
288 ) {
289 let Some(ino) = INode::from_u64(ino) else {
290 reply.error(EINVAL);
291 return;
292 };
293
294 if offset < 0 {
295 reply.error(EINVAL);
296 return;
297 }
298
299 debug!(
300 "read {} {} {} {} {} {:?}",
301 ino, fh, offset, size, flags, lock_owner
302 );
303
304 match self.read_file(ino, offset as u64) {
305 Err(err) => {
306 error!("read file error: {err}");
307 reply.error(EINVAL);
308 }
309 Ok(None) => reply.error(ENOENT),
310 Ok(Some(content)) => reply.data(content.chunk()),
311 }
312 }
313
314 fn opendir(
315 &mut self,
316 _req: &fuser::Request<'_>,
317 ino: u64,
318 _flags: i32,
319 reply: fuser::ReplyOpen,
320 ) {
321 let Some(ino) = INode::from_u64(ino) else {
322 reply.error(EINVAL);
323 return;
324 };
325
326 match self.open_dir(ino) {
327 Err(Error::NoExists { ino: _ }) => reply.error(ENOENT),
328 Err(err) => {
329 error!("failed to open dir {ino}: {err}");
330 reply.error(EINVAL)
331 }
332 Ok(_) => reply.opened(0, 0),
333 }
334 }
335
336 fn getattr(&mut self, _req: &fuser::Request, ino: u64, reply: fuser::ReplyAttr) {
337 debug!("getattr ino = {}", ino);
338 let Some(ino) = INode::from_u64(ino) else {
339 reply.error(EINVAL);
340 return;
341 };
342 let Some(file_attr) = self.get(ino).and_then(|entry| entry.file_attr()) else {
343 reply.error(ENOENT);
344 return;
345 };
346
347 reply.attr(&TTL, &file_attr);
348 }
349
350 fn readdir(
351 &mut self,
352 _req: &fuser::Request<'_>,
353 ino: u64,
354 fh: u64,
355 offset: i64,
356 mut reply: fuser::ReplyDirectory,
357 ) {
358 debug!("readdir ino = {}, fh = {}, offset = {}", ino, fh, offset);
359 let Some(ino) = INode::from_u64(ino) else {
360 reply.error(EINVAL);
361 return;
362 };
363
364 let Some(entries) = self.list_dir(ino) else {
365 reply.error(ENOENT);
366 return;
367 };
368
369 for (i, entry) in entries.enumerate().skip(offset as usize) {
370 debug!("readdir entry: {entry:?}");
371
372 if reply.add(
373 entry.ino.into(),
374 (i + 1) as i64,
375 entry.file_type,
376 entry.name.as_ref(),
377 ) {
378 break;
379 }
380 }
381 reply.ok();
382 }
383}
384
385#[derive(Debug, Clone)]
386struct Entry {
387 ino: INode,
388 inner: IndexEntry,
389}
390
391impl Entry {
392 #[inline]
393 fn file_type(&self) -> Option<FileType> {
394 kind_to_filetype(self.inner.kind)
395 }
396
397 #[inline]
398 fn is_dir(&self) -> bool {
399 self.inner.kind == Kind::Dir
400 }
401
402 #[inline]
403 fn is_file(&self) -> bool {
404 self.inner.kind == Kind::File
405 }
406
407 #[inline]
408 fn apath(&self) -> &Apath {
409 &self.inner.apath
410 }
411
412 fn file_attr(&self) -> Option<FileAttr> {
413 FileAttr {
414 ino: self.ino.into(),
415 size: self.inner.addrs.iter().map(|addr| addr.len).sum(),
416 blocks: self.inner.addrs.len() as u64,
417 atime: UNIX_EPOCH,
418 mtime: into_time(self.inner.mtime, self.inner.mtime_nanos),
419 ctime: UNIX_EPOCH,
420 crtime: UNIX_EPOCH,
421 kind: self.file_type()?,
422 perm: 0o777,
423 nlink: 0,
424 uid: 0,
425 gid: 0,
426 rdev: 0,
427 flags: 0,
428 blksize: self
429 .inner
430 .addrs
431 .first()
432 .map(|addr| addr.len)
433 .unwrap_or_default() as u32,
434 }
435 .into()
436 }
437}
438
439fn into_time(secs: i64, subsec_nanos: u32) -> SystemTime {
440 let t = if secs >= 0 {
441 UNIX_EPOCH + Duration::from_secs(secs as u64)
442 } else {
443 UNIX_EPOCH - Duration::from_secs(secs.unsigned_abs())
444 };
445
446 t + Duration::from_nanos(subsec_nanos as u64)
447}
448
449fn kind_to_filetype(kind: Kind) -> Option<FileType> {
450 match kind {
451 Kind::File => FileType::RegularFile.into(),
452 Kind::Dir => FileType::Directory.into(),
453 Kind::Symlink => FileType::Symlink.into(),
454 Kind::Unknown => None,
455 }
456}
457
458#[derive(Debug, Clone, Copy)]
459struct DiscardMonitor;
460
461impl DiscardMonitor {
462 fn arc() -> Arc<dyn Monitor> {
463 Arc::new(Self)
464 }
465}
466
467impl Monitor for DiscardMonitor {
468 fn count(&self, _counter: conserve::counters::Counter, _increment: usize) {}
469
470 fn set_counter(&self, _counter: conserve::counters::Counter, _value: usize) {}
471
472 fn problem(&self, _problem: conserve::monitor::Problem) {}
473
474 fn start_task(&self, name: String) -> conserve::monitor::task::Task {
475 conserve::monitor::task::TaskList::default().start_task(name)
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use pretty_assertions::assert_eq;
482 use std::{ffi::OsStr, path::Path};
483
484 use conserve::{Archive, BlockHash};
485
486 use super::{ConserveFilesystem, INode};
487
488 fn load_fs(name: &str) -> ConserveFilesystem {
489 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
490 let base = Path::new(&manifest_dir);
491 let path = base.join("fixtures").join(name);
492 let archive = Archive::open_path(&path).expect("archive");
493
494 let tree = archive
495 .open_stored_tree(conserve::BandSelectionPolicy::LatestClosed)
496 .expect("tree");
497
498 ConserveFilesystem::new(tree)
499 }
500
501 #[test]
502 fn open_root() {
503 let mut filesystem = load_fs("backup-treeroot");
504 let root_ino = INode::from_u64(1).unwrap();
505 filesystem.open_dir(root_ino).expect("open root dir");
506 let Some(dir_iter) = filesystem.list_dir(root_ino) else {
507 panic!("root not loaded");
508 };
509
510 let content: Vec<_> = dir_iter
511 .map(|ls_entry| (ls_entry.name.to_string(), ls_entry.ino))
512 .collect();
513
514 assert_eq!(
515 vec![
516 (String::from("."), INode(1)),
517 (String::from(".."), INode(0)),
518 (String::from("file0.txt"), INode(3)),
519 (String::from("subdir0"), INode(4)),
520 (String::from("subdir1"), INode(5)),
521 (String::from("words"), INode(6)),
522 ],
523 content
524 );
525 }
526
527 #[test]
528 fn open_subdir() {
529 let mut filesystem = load_fs("backup-treeroot");
530 let root_ino = INode::from_u64(1).unwrap();
531 filesystem.open_dir(root_ino).expect("open root dir");
532
533 filesystem.open_dir(INode(4)).expect("open subdir0");
534 let Some(dir_iter) = filesystem.list_dir(INode(4)) else {
535 panic!("root not loaded");
536 };
537
538 let content: Vec<_> = dir_iter
539 .map(|ls_entry| (ls_entry.name.to_string(), ls_entry.ino))
540 .collect();
541
542 assert_eq!(
543 vec![
544 (String::from("."), INode(4)),
545 (String::from(".."), INode(1)),
546 (String::from("subdir0_1"), INode(7)),
547 (String::from("subdir0_2"), INode(8)),
548 ],
549 content
550 );
551 }
552
553 #[test]
554 fn test_read_small_file() {
555 let mut filesystem = load_fs("backup-treeroot");
556
557 filesystem.open_dir(INode::ROOT).expect("open root");
558 let child = filesystem
559 .lookup_child_by_name(INode::ROOT, OsStr::new("file0.txt"))
560 .expect("child")
561 .expect("child")
562 .clone();
563 let content = filesystem
564 .read_file(child.ino, 0)
565 .expect("read file")
566 .expect("some content");
567 let content = content.chunks(1024).take(1).next().expect("content");
568 assert_eq!(String::from_utf8_lossy(content).as_ref(), "Hello FUSE!\n");
569 }
570
571 #[test]
572 fn test_read_large_file() {
573 let mut filesystem = load_fs("backup-treeroot");
574
575 filesystem.open_dir(INode::ROOT).expect("open root");
576 let child = filesystem
577 .lookup_child_by_name(INode::ROOT, OsStr::new("words"))
578 .expect("child")
579 .expect("child")
580 .clone();
581
582 let size = child.file_attr().unwrap().size;
583 let mut hasher = blake2_rfc::blake2b::Blake2b::new(64);
584 let mut offset = 0;
585
586 while offset < size {
587 let buffer = filesystem
588 .read_file(child.ino, offset)
589 .expect("read file")
590 .expect("some content");
591 offset += buffer.len() as u64;
592 for chunk in buffer.chunks(4096) {
593 hasher.update(chunk);
594 }
595 }
596
597 let hash_result = hasher.finalize();
598 let hash = BlockHash::from(hash_result);
599 assert_eq!(hash.to_string(), "53b246febde6a54d4f9995a3c7b68a38e1dd93159a196c642fabafa09e7eec113cc4061856d12997901dbc1ba95bd7bff517a312c6de3f01b1d380ea157bc122");
600 }
601}