rustic_rs/commands/mount/
fusefs.rs

1#[cfg(not(windows))]
2use std::os::unix::prelude::OsStrExt;
3use std::{
4    collections::BTreeMap,
5    ffi::{CString, OsStr},
6    path::Path,
7    sync::RwLock,
8    time::{Duration, SystemTime},
9};
10
11use rustic_core::{
12    IndexedFull, Repository,
13    repofile::{Node, NodeType},
14    vfs::{FilePolicy, OpenFile, Vfs},
15};
16
17use fuse_mt::{
18    CallbackResult, DirectoryEntry, FileAttr, FileType, FilesystemMT, RequestInfo, ResultData,
19    ResultEmpty, ResultEntry, ResultOpen, ResultReaddir, ResultSlice, ResultXattr, Xattr,
20};
21use itertools::Itertools;
22
23pub struct FuseFS<P, S> {
24    repo: Repository<P, S>,
25    vfs: Vfs,
26    open_files: RwLock<BTreeMap<u64, OpenFile>>,
27    now: SystemTime,
28    file_policy: FilePolicy,
29}
30
31impl<P, S: IndexedFull> FuseFS<P, S> {
32    pub(crate) fn new(repo: Repository<P, S>, vfs: Vfs, file_policy: FilePolicy) -> Self {
33        let open_files = RwLock::new(BTreeMap::new());
34
35        Self {
36            repo,
37            vfs,
38            open_files,
39            now: SystemTime::now(),
40            file_policy,
41        }
42    }
43
44    fn node_from_path(&self, path: &Path) -> Result<Node, i32> {
45        self.vfs
46            .node_from_path(&self.repo, path)
47            .map_err(|_| libc::ENOENT)
48    }
49
50    fn dir_entries_from_path(&self, path: &Path) -> Result<Vec<Node>, i32> {
51        self.vfs
52            .dir_entries_from_path(&self.repo, path)
53            .map_err(|_| libc::ENOENT)
54    }
55}
56
57fn node_to_filetype(node: &Node) -> FileType {
58    match node.node_type {
59        NodeType::File => FileType::RegularFile,
60        NodeType::Dir => FileType::Directory,
61        NodeType::Symlink { .. } => FileType::Symlink,
62        NodeType::Chardev { .. } => FileType::CharDevice,
63        NodeType::Dev { .. } => FileType::BlockDevice,
64        NodeType::Fifo => FileType::NamedPipe,
65        NodeType::Socket => FileType::Socket,
66    }
67}
68
69fn node_type_to_rdev(tpe: &NodeType) -> u32 {
70    u32::try_from(match tpe {
71        NodeType::Dev { device } | NodeType::Chardev { device } => *device,
72        _ => 0,
73    })
74    .unwrap()
75}
76
77fn node_to_linktarget(node: &Node) -> Option<&OsStr> {
78    if node.is_symlink() {
79        Some(node.node_type.to_link().as_os_str())
80    } else {
81        None
82    }
83}
84
85fn node_to_file_attr(node: &Node, now: SystemTime) -> FileAttr {
86    FileAttr {
87        // Size in bytes
88        size: node.meta.size,
89        // Size in blocks
90        blocks: 0,
91        // Time of last access
92        atime: node.meta.atime.map(SystemTime::from).unwrap_or(now),
93        // Time of last modification
94        mtime: node.meta.mtime.map(SystemTime::from).unwrap_or(now),
95        // Time of last metadata change
96        ctime: node.meta.ctime.map(SystemTime::from).unwrap_or(now),
97        // Time of creation (macOS only)
98        crtime: now,
99        // Kind of file (directory, file, pipe, etc.)
100        kind: node_to_filetype(node),
101        // Permissions
102        perm: node.meta.mode.unwrap_or(0o755) as u16,
103        // Number of hard links
104        nlink: node.meta.links.try_into().unwrap_or(1),
105        // User ID
106        uid: node.meta.uid.unwrap_or(0),
107        // Group ID
108        gid: node.meta.gid.unwrap_or(0),
109        // Device ID (if special file)
110        rdev: node_type_to_rdev(&node.node_type),
111        // Flags (macOS only; see chflags(2))
112        flags: 0,
113    }
114}
115
116impl<P, S: IndexedFull> FilesystemMT for FuseFS<P, S> {
117    fn getattr(&self, _req: RequestInfo, path: &Path, _fh: Option<u64>) -> ResultEntry {
118        let node = self.node_from_path(path)?;
119        Ok((Duration::from_secs(1), node_to_file_attr(&node, self.now)))
120    }
121
122    #[cfg(not(windows))]
123    fn readlink(&self, _req: RequestInfo, path: &Path) -> ResultData {
124        let target = node_to_linktarget(&self.node_from_path(path)?)
125            .ok_or(libc::ENOSYS)?
126            .as_bytes()
127            .to_vec();
128
129        Ok(target)
130    }
131
132    fn open(&self, _req: RequestInfo, path: &Path, _flags: u32) -> ResultOpen {
133        if matches!(self.file_policy, FilePolicy::Forbidden) {
134            return Err(libc::ENOTSUP);
135        }
136        let node = self.node_from_path(path)?;
137        let open = self.repo.open_file(&node).map_err(|_| libc::ENOSYS)?;
138        let fh = {
139            let mut open_files = self.open_files.write().unwrap();
140            let fh = open_files.last_key_value().map_or(0, |(fh, _)| *fh + 1);
141            _ = open_files.insert(fh, open);
142            fh
143        };
144        Ok((fh, 0))
145    }
146
147    fn release(
148        &self,
149        _req: RequestInfo,
150        _path: &Path,
151        fh: u64,
152        _flags: u32,
153        _lock_owner: u64,
154        _flush: bool,
155    ) -> ResultEmpty {
156        _ = self.open_files.write().unwrap().remove(&fh);
157        Ok(())
158    }
159
160    fn read(
161        &self,
162        _req: RequestInfo,
163        _path: &Path,
164        fh: u64,
165        offset: u64,
166        size: u32,
167        callback: impl FnOnce(ResultSlice<'_>) -> CallbackResult,
168    ) -> CallbackResult {
169        if let Some(open_file) = self.open_files.read().unwrap().get(&fh) {
170            if let Ok(data) =
171                self.repo
172                    .read_file_at(open_file, offset.try_into().unwrap(), size as usize)
173            {
174                return callback(Ok(&data));
175            }
176        }
177        callback(Err(libc::ENOSYS))
178    }
179
180    fn opendir(&self, _req: RequestInfo, _path: &Path, _flags: u32) -> ResultOpen {
181        Ok((0, 0))
182    }
183
184    fn readdir(&self, _req: RequestInfo, path: &Path, _fh: u64) -> ResultReaddir {
185        let nodes = self.dir_entries_from_path(path)?;
186
187        let result = nodes
188            .into_iter()
189            .map(|node| DirectoryEntry {
190                name: node.name(),
191                kind: node_to_filetype(&node),
192            })
193            .collect();
194        Ok(result)
195    }
196
197    fn releasedir(&self, _req: RequestInfo, _path: &Path, _fh: u64, _flags: u32) -> ResultEmpty {
198        Ok(())
199    }
200
201    fn listxattr(&self, _req: RequestInfo, path: &Path, size: u32) -> ResultXattr {
202        let node = self.node_from_path(path)?;
203        let xattrs = node
204            .meta
205            .extended_attributes
206            .into_iter()
207            // convert into null-terminated [u8]
208            .map(|a| CString::new(a.name).unwrap().into_bytes_with_nul())
209            .concat();
210
211        if size == 0 {
212            Ok(Xattr::Size(u32::try_from(xattrs.len()).unwrap()))
213        } else {
214            Ok(Xattr::Data(xattrs))
215        }
216    }
217
218    fn getxattr(&self, _req: RequestInfo, path: &Path, name: &OsStr, size: u32) -> ResultXattr {
219        let node = self.node_from_path(path)?;
220        match node
221            .meta
222            .extended_attributes
223            .into_iter()
224            .find(|a| name == OsStr::new(&a.name))
225        {
226            None => Err(libc::ENOSYS),
227            Some(attr) => {
228                let value = attr.value.unwrap_or_default();
229                if size == 0 {
230                    Ok(Xattr::Size(u32::try_from(value.len()).unwrap()))
231                } else {
232                    Ok(Xattr::Data(value))
233                }
234            }
235        }
236    }
237}