Skip to main content

rustic_rs/commands/
mount.rs

1//! `mount` subcommand
2
3// ignore markdown clippy lints as we use doc-comments to generate clap help texts
4#![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    /// The path template to use for snapshots. {id}, {id_long}, {time}, {username}, {hostname}, {label}, {tags}, {backup_start}, {backup_end} are replaced. [default: "[{hostname}]/[{label}]/{time}"]
30    #[clap(long)]
31    #[merge(strategy=conflate::option::overwrite_none)]
32    path_template: Option<String>,
33
34    /// The time template to use to display times in the path template. See https://pubs.opengroup.org/onlinepubs/009695399/functions/strftime.html for format options. [default: "%Y-%m-%d_%H-%M-%S"]
35    #[clap(long)]
36    #[merge(strategy=conflate::option::overwrite_none)]
37    time_template: Option<String>,
38
39    /// Don't allow other users to access the mount point (else options allow_other,default_permissions are set)
40    #[clap(short, long)]
41    #[merge(strategy=conflate::bool::overwrite_false)]
42    exclusive: bool,
43
44    /// How to handle access to files. [default: "forbidden" for hot/cold repositories, else "read"]
45    #[clap(long)]
46    #[merge(strategy=conflate::option::overwrite_none)]
47    file_access: Option<FilePolicy>,
48
49    /// The mount point to use
50    #[clap(value_name = "PATH")]
51    #[merge(strategy=conflate::option::overwrite_none)]
52    mount_point: Option<PathBuf>,
53
54    /// Specify directly which snapshot/path to mount
55    ///
56    /// Snapshot can be identified the following ways: "01a2b3c4" or "latest" or "latest~N" (N >= 0)
57    #[clap(value_name = "SNAPSHOT[:PATH]")]
58    #[merge(strategy=conflate::option::overwrite_none)]
59    snapshot_path: Option<String>,
60
61    /// Other options to use for mount
62    #[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    // Process the given command line options, overriding settings from
69    // a configuration file using explicit flags taken from command-line
70    // arguments.
71    fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
72        // Merge by precedence, cli <- config <- default
73        let self_config = self
74            .clone()
75            .merge_from(config.mount)
76            .merge_from(Self::with_default_config());
77
78        // Other values
79        if self_config.mount_point.is_none() {
80            return Err(ParseError
81                .context("Please specify a valid mount point!")
82                .into());
83        }
84
85        // rewrite the "mount" section in the config file
86        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        // We have merged the config file, the command line options, and the
119        // default values into a single struct. Now we can use the values.
120        // If a value is missing, we can return an error.
121        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        // Prepare the mount options
149        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        // Sort and deduplicate options
172        mount_options.sort_unstable();
173        mount_options.dedup();
174
175        // join options into a single comma-delimited string and prepent "-o "
176        // this should be parsed just fine by fuser, here
177        // https://github.com/cberner/fuser/blob/9f6ced73a36f1d99846e28be9c5e4903939ee9d5/src/mnt/mount_options.rs#L157
178        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}