rustic_rs/commands/
mount.rs1#![allow(clippy::doc_markdown)]
5
6mod fusefs;
7use fusefs::FuseFS;
8
9use abscissa_core::{
10 Command, FrameworkError, FrameworkErrorKind::ParseError, Runnable, Shutdown, config::Override,
11};
12use anyhow::{Result, bail};
13use clap::Parser;
14use conflate::{Merge, MergeFrom};
15use fuse_mt::{FuseMT, mount};
16use log::info;
17use rustic_core::vfs::{FilePolicy, IdenticalSnapshot, Latest, Vfs};
18use std::{ffi::OsStr, path::PathBuf};
19
20use crate::{
21 Application, RUSTIC_APP, RusticConfig,
22 repository::{IndexedRepo, get_filtered_snapshots},
23 status_err,
24};
25
26#[derive(Clone, Debug, Default, Command, Parser, Merge, serde::Serialize, serde::Deserialize)]
27#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
28pub struct MountCmd {
29 #[clap(long)]
31 #[merge(strategy=conflate::option::overwrite_none)]
32 path_template: Option<String>,
33
34 #[clap(long)]
36 #[merge(strategy=conflate::option::overwrite_none)]
37 time_template: Option<String>,
38
39 #[clap(short, long)]
41 #[merge(strategy=conflate::bool::overwrite_false)]
42 exclusive: bool,
43
44 #[clap(long)]
46 #[merge(strategy=conflate::option::overwrite_none)]
47 file_access: Option<FilePolicy>,
48
49 #[clap(value_name = "PATH")]
51 #[merge(strategy=conflate::option::overwrite_none)]
52 mount_point: Option<PathBuf>,
53
54 #[clap(value_name = "SNAPSHOT[:PATH]")]
58 #[merge(strategy=conflate::option::overwrite_none)]
59 snapshot_path: Option<String>,
60
61 #[clap(short, long = "option", value_name = "OPTION")]
63 #[merge(strategy = conflate::vec::overwrite_empty)]
64 options: Vec<String>,
65}
66
67impl Override<RusticConfig> for MountCmd {
68 fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
72 let self_config = self
74 .clone()
75 .merge_from(config.mount)
76 .merge_from(Self::with_default_config());
77
78 if self_config.mount_point.is_none() {
80 return Err(ParseError
81 .context("Please specify a valid mount point!")
82 .into());
83 }
84
85 config.mount = self_config;
87
88 Ok(config)
89 }
90}
91
92impl Runnable for MountCmd {
93 fn run(&self) {
94 if let Err(err) = RUSTIC_APP
95 .config()
96 .repository
97 .run_indexed(|repo| self.inner_run(repo))
98 {
99 status_err!("{}", err);
100 RUSTIC_APP.shutdown(Shutdown::Crash);
101 };
102 }
103}
104
105impl MountCmd {
106 fn with_default_config() -> Self {
107 Self {
108 path_template: Some(String::from("[{hostname}]/[{label}]/{time}")),
109 time_template: Some(String::from("%Y-%m-%d_%H-%M-%S")),
110 options: Vec::new(),
111 ..Default::default()
112 }
113 }
114
115 fn inner_run(&self, repo: IndexedRepo) -> Result<()> {
116 let config = RUSTIC_APP.config();
117
118 let Some(path_template) = config.mount.path_template.clone() else {
122 bail!("Please specify a path template!");
123 };
124
125 let Some(time_template) = config.mount.time_template.clone() else {
126 bail!("Please specify a time template!");
127 };
128
129 let Some(mount_point) = config.mount.mount_point.clone() else {
130 bail!("Please specify a mount point!");
131 };
132
133 let vfs = if let Some(snap) = &config.mount.snapshot_path {
134 let node =
135 repo.node_from_snapshot_path(snap, |sn| config.snapshot_filter.matches(sn))?;
136 Vfs::from_dir_node(&node)
137 } else {
138 let snapshots = get_filtered_snapshots(&repo)?;
139 Vfs::from_snapshots(
140 snapshots,
141 &path_template,
142 &time_template,
143 Latest::AsLink,
144 IdenticalSnapshot::AsLink,
145 )?
146 };
147
148 let mut mount_options = config.mount.options.clone();
150
151 mount_options.push(format!("fsname=rusticfs:{}", repo.config().id));
152
153 if !config.mount.exclusive {
154 mount_options
155 .extend_from_slice(&["allow_other".to_string(), "default_permissions".to_string()]);
156 }
157
158 let file_access = config.mount.file_access.as_ref().map_or_else(
159 || {
160 if repo.config().is_hot == Some(true) {
161 FilePolicy::Forbidden
162 } else {
163 FilePolicy::Read
164 }
165 },
166 |s| *s,
167 );
168
169 let fs = FuseMT::new(FuseFS::new(repo, vfs, file_access), 1);
170
171 mount_options.sort_unstable();
173 mount_options.dedup();
174
175 let opt_string = format!("-o{}", mount_options.join(","));
179
180 info!(
181 "mounting {}, press Ctrl-C to cancel...",
182 mount_point.display()
183 );
184 mount(fs, mount_point, &[OsStr::new(&opt_string)])?;
185
186 Ok(())
187 }
188}