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 rustic_core::vfs::{FilePolicy, IdenticalSnapshot, Latest, Vfs};
17use std::{ffi::OsStr, path::PathBuf};
18
19use crate::{
20    Application, RUSTIC_APP, RusticConfig,
21    repository::{IndexedRepo, get_filtered_snapshots},
22    status_err,
23};
24
25#[derive(Clone, Debug, Default, Command, Parser, Merge, serde::Serialize, serde::Deserialize)]
26#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
27pub struct MountCmd {
28    /// 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}"]
29    #[clap(long)]
30    #[merge(strategy=conflate::option::overwrite_none)]
31    path_template: Option<String>,
32
33    /// 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"]
34    #[clap(long)]
35    #[merge(strategy=conflate::option::overwrite_none)]
36    time_template: Option<String>,
37
38    /// Don't allow other users to access the mount point
39    #[clap(short, long)]
40    #[merge(strategy=conflate::bool::overwrite_false)]
41    exclusive: bool,
42
43    /// How to handle access to files. [default: "forbidden" for hot/cold repositories, else "read"]
44    #[clap(long)]
45    #[merge(strategy=conflate::option::overwrite_none)]
46    file_access: Option<FilePolicy>,
47
48    /// The mount point to use
49    #[clap(value_name = "PATH")]
50    #[merge(strategy=conflate::option::overwrite_none)]
51    mount_point: Option<PathBuf>,
52
53    /// Specify directly which snapshot/path to mount
54    ///
55    /// Snapshot can be identified the following ways: "01a2b3c4" or "latest" or "latest~N" (N >= 0)
56    #[clap(value_name = "SNAPSHOT[:PATH]")]
57    #[merge(strategy=conflate::option::overwrite_none)]
58    snapshot_path: Option<String>,
59
60    /// Other options to use for mount
61    #[clap(short, long = "option", value_name = "OPTION")]
62    #[merge(strategy = conflate::vec::overwrite_empty)]
63    options: Vec<String>,
64}
65
66impl Override<RusticConfig> for MountCmd {
67    // Process the given command line options, overriding settings from
68    // a configuration file using explicit flags taken from command-line
69    // arguments.
70    fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
71        // Merge by precedence, cli <- config <- default
72        let self_config = self
73            .clone()
74            .merge_from(config.mount)
75            .merge_from(Self::with_default_config());
76
77        // Other values
78        if self_config.mount_point.is_none() {
79            return Err(ParseError
80                .context("Please specify a valid mount point!")
81                .into());
82        }
83
84        // rewrite the "mount" section in the config file
85        config.mount = self_config;
86
87        Ok(config)
88    }
89}
90
91impl Runnable for MountCmd {
92    fn run(&self) {
93        if let Err(err) = RUSTIC_APP
94            .config()
95            .repository
96            .run_indexed(|repo| self.inner_run(repo))
97        {
98            status_err!("{}", err);
99            RUSTIC_APP.shutdown(Shutdown::Crash);
100        };
101    }
102}
103
104impl MountCmd {
105    fn with_default_config() -> Self {
106        Self {
107            path_template: Some(String::from("[{hostname}]/[{label}]/{time}")),
108            time_template: Some(String::from("%Y-%m-%d_%H-%M-%S")),
109            options: vec![String::from("kernel_cache")],
110            ..Default::default()
111        }
112    }
113
114    fn inner_run(&self, repo: IndexedRepo) -> Result<()> {
115        let config = RUSTIC_APP.config();
116
117        // We have merged the config file, the command line options, and the
118        // default values into a single struct. Now we can use the values.
119        // If a value is missing, we can return an error.
120        let Some(path_template) = config.mount.path_template.clone() else {
121            bail!("Please specify a path template!");
122        };
123
124        let Some(time_template) = config.mount.time_template.clone() else {
125            bail!("Please specify a time template!");
126        };
127
128        let Some(mount_point) = config.mount.mount_point.clone() else {
129            bail!("Please specify a mount point!");
130        };
131
132        let vfs = if let Some(snap) = &config.mount.snapshot_path {
133            let node =
134                repo.node_from_snapshot_path(snap, |sn| config.snapshot_filter.matches(sn))?;
135            Vfs::from_dir_node(&node)
136        } else {
137            let snapshots = get_filtered_snapshots(&repo)?;
138            Vfs::from_snapshots(
139                snapshots,
140                &path_template,
141                &time_template,
142                Latest::AsLink,
143                IdenticalSnapshot::AsLink,
144            )?
145        };
146
147        // Prepare the mount options
148        let mut mount_options = config.mount.options.clone();
149
150        mount_options.push(format!("fsname=rusticfs:{}", repo.config().id));
151
152        if !config.mount.exclusive {
153            mount_options
154                .extend_from_slice(&["allow_other".to_string(), "default_permissions".to_string()]);
155        }
156
157        let file_access = config.mount.file_access.as_ref().map_or_else(
158            || {
159                if repo.config().is_hot == Some(true) {
160                    FilePolicy::Forbidden
161                } else {
162                    FilePolicy::Read
163                }
164            },
165            |s| *s,
166        );
167
168        let fs = FuseMT::new(FuseFS::new(repo, vfs, file_access), 1);
169
170        // Sort and deduplicate options
171        mount_options.sort_unstable();
172        mount_options.dedup();
173
174        // join options into a single comma-delimited string and prepent "-o "
175        // this should be parsed just fine by fuser, here
176        // https://github.com/cberner/fuser/blob/9f6ced73a36f1d99846e28be9c5e4903939ee9d5/src/mnt/mount_options.rs#L157
177        let opt_string = format!("-o {}", mount_options.join(","));
178
179        mount(fs, mount_point, &[OsStr::new(&opt_string)])?;
180
181        Ok(())
182    }
183}