conserve_fuse/
fs.rs

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}