Skip to content

Commit 84ccf79

Browse files
committed
Add mount command
1 parent 16ea6de commit 84ccf79

File tree

4 files changed

+279
-3
lines changed

4 files changed

+279
-3
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ merge = { workspace = true }
7878
bytesize = { workspace = true }
7979
comfy-table = { workspace = true }
8080
dialoguer = { workspace = true }
81+
fuse_mt = { workspace = true }
8182
directories = { workspace = true }
8283
gethostname = { workspace = true }
8384
humantime = { workspace = true }
@@ -128,6 +129,7 @@ comfy-table = "7.1.0"
128129
merge = "0.1"
129130
directories = "5"
130131
dialoguer = "0.11.0"
132+
fuse_mt = "0.6"
131133
indicatif = "0.17"
132134
gethostname = "0.4"
133135
bytesize = "1"

src/commands.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub(crate) mod key;
1414
pub(crate) mod list;
1515
pub(crate) mod ls;
1616
pub(crate) mod merge;
17+
pub(crate) mod mount;
1718
pub(crate) mod prune;
1819
pub(crate) mod repair;
1920
pub(crate) mod repoinfo;
@@ -32,9 +33,10 @@ use crate::{
3233
commands::{
3334
backup::BackupCmd, cat::CatCmd, check::CheckCmd, completions::CompletionsCmd,
3435
config::ConfigCmd, copy::CopyCmd, diff::DiffCmd, dump::DumpCmd, forget::ForgetCmd,
35-
init::InitCmd, key::KeyCmd, list::ListCmd, ls::LsCmd, merge::MergeCmd, prune::PruneCmd,
36-
repair::RepairCmd, repoinfo::RepoInfoCmd, restore::RestoreCmd, self_update::SelfUpdateCmd,
37-
show_config::ShowConfigCmd, snapshots::SnapshotCmd, tag::TagCmd,
36+
init::InitCmd, key::KeyCmd, list::ListCmd, ls::LsCmd, merge::MergeCmd, mount::MountCmd,
37+
prune::PruneCmd, repair::RepairCmd, repoinfo::RepoInfoCmd, restore::RestoreCmd,
38+
self_update::SelfUpdateCmd, show_config::ShowConfigCmd, snapshots::SnapshotCmd,
39+
tag::TagCmd,
3840
},
3941
config::{progress_options::ProgressOptions, RusticConfig},
4042
{Application, RUSTIC_APP},
@@ -92,6 +94,9 @@ enum RusticCmd {
9294
/// Manage keys
9395
Key(KeyCmd),
9496

97+
/// Mount repository
98+
Mount(MountCmd),
99+
95100
/// List repository files
96101
List(ListCmd),
97102

src/commands/mount.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//! `mount` subcommand
2+
mod fs;
3+
use std::{ffi::OsStr, path::PathBuf};
4+
5+
use fs::RusticFS;
6+
7+
use crate::{commands::open_repository, status_err, Application, RUSTIC_APP};
8+
9+
use abscissa_core::{Command, Runnable, Shutdown};
10+
use anyhow::Result;
11+
use fuse_mt::{mount, FuseMT};
12+
13+
/// `dump` subcommand
14+
#[derive(clap::Parser, Command, Debug)]
15+
pub(crate) struct MountCmd {
16+
/// file from snapshot to dump
17+
#[clap(value_name = "SNAPSHOT[:PATH]")]
18+
snap: String,
19+
20+
mountpoint: PathBuf,
21+
}
22+
23+
impl Runnable for MountCmd {
24+
fn run(&self) {
25+
if let Err(err) = self.inner_run() {
26+
status_err!("{}", err);
27+
RUSTIC_APP.shutdown(Shutdown::Crash);
28+
};
29+
}
30+
}
31+
32+
impl MountCmd {
33+
fn inner_run(&self) -> Result<()> {
34+
let config = RUSTIC_APP.config();
35+
36+
let repo = open_repository(&config)?.to_indexed()?;
37+
let node =
38+
repo.node_from_snapshot_path(&self.snap, |sn| config.snapshot_filter.matches(sn))?;
39+
40+
let options = [OsStr::new("-o"), OsStr::new("fsname=rusticfs")];
41+
42+
let fs = FuseMT::new(RusticFS::from_node(repo, node)?, 1);
43+
mount(fs, &self.mountpoint, &options)?;
44+
45+
Ok(())
46+
}
47+
}

src/commands/mount/fs.rs

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

0 commit comments

Comments
 (0)