rustic_rs/commands/
webdav.rs

1//! `webdav` subcommand
2
3// ignore markdown clippy lints as we use doc-comments to generate clap help texts
4#![allow(clippy::doc_markdown)]
5
6use std::net::ToSocketAddrs;
7
8use crate::{
9    Application, RUSTIC_APP, RusticConfig,
10    repository::{CliIndexedRepo, get_filtered_snapshots},
11    status_err,
12};
13use abscissa_core::{Command, FrameworkError, Runnable, Shutdown, config::Override};
14use anyhow::{Result, anyhow};
15use conflate::Merge;
16use dav_server::{DavHandler, warp::dav_handler};
17use serde::{Deserialize, Serialize};
18
19use rustic_core::vfs::{FilePolicy, IdenticalSnapshot, Latest, Vfs};
20use webdavfs::WebDavFS;
21
22mod webdavfs;
23
24#[derive(Clone, Command, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)]
25#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
26pub struct WebDavCmd {
27    /// Address to bind the webdav server to. [default: "localhost:8000"]
28    #[clap(long, value_name = "ADDRESS")]
29    #[merge(strategy=conflate::option::overwrite_none)]
30    address: Option<String>,
31
32    /// 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}"]
33    #[clap(long)]
34    #[merge(strategy=conflate::option::overwrite_none)]
35    path_template: Option<String>,
36
37    /// The time template to use to display times in the path template. See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for format options. [default: "%Y-%m-%d_%H-%M-%S"]
38    #[clap(long)]
39    #[merge(strategy=conflate::option::overwrite_none)]
40    time_template: Option<String>,
41
42    /// Use symlinks. This may not be supported by all WebDAV clients
43    #[clap(long)]
44    #[merge(strategy=conflate::bool::overwrite_false)]
45    symlinks: bool,
46
47    /// How to handle access to files. [default: "forbidden" for hot/cold repositories, else "read"]
48    #[clap(long)]
49    #[merge(strategy=conflate::option::overwrite_none)]
50    file_access: Option<String>,
51
52    /// Specify directly which snapshot/path to serve
53    #[clap(value_name = "SNAPSHOT[:PATH]")]
54    #[merge(strategy=conflate::option::overwrite_none)]
55    snapshot_path: Option<String>,
56}
57
58impl Override<RusticConfig> for WebDavCmd {
59    // Process the given command line options, overriding settings from
60    // a configuration file using explicit flags taken from command-line
61    // arguments.
62    fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
63        let mut self_config = self.clone();
64        // merge "webdav" section from config file, if given
65        self_config.merge(config.webdav);
66        config.webdav = self_config;
67        Ok(config)
68    }
69}
70
71impl Runnable for WebDavCmd {
72    fn run(&self) {
73        if let Err(err) = RUSTIC_APP
74            .config()
75            .repository
76            .run_indexed(|repo| self.inner_run(repo))
77        {
78            status_err!("{}", err);
79            RUSTIC_APP.shutdown(Shutdown::Crash);
80        };
81    }
82}
83
84impl WebDavCmd {
85    /// be careful about self VS RUSTIC_APP.config() usage
86    /// only the RUSTIC_APP.config() involves the TOML and ENV merged configurations
87    /// see https://github.com/rustic-rs/rustic/issues/1242
88    fn inner_run(&self, repo: CliIndexedRepo) -> Result<()> {
89        let config = RUSTIC_APP.config();
90
91        let path_template = config
92            .webdav
93            .path_template
94            .clone()
95            .unwrap_or_else(|| "[{hostname}]/[{label}]/{time}".to_string());
96        let time_template = config
97            .webdav
98            .time_template
99            .clone()
100            .unwrap_or_else(|| "%Y-%m-%d_%H-%M-%S".to_string());
101
102        let vfs = if let Some(snap) = &config.webdav.snapshot_path {
103            let node =
104                repo.node_from_snapshot_path(snap, |sn| config.snapshot_filter.matches(sn))?;
105            Vfs::from_dir_node(&node)
106        } else {
107            let snapshots = get_filtered_snapshots(&repo)?;
108            let (latest, identical) = if config.webdav.symlinks {
109                (Latest::AsLink, IdenticalSnapshot::AsLink)
110            } else {
111                (Latest::AsDir, IdenticalSnapshot::AsDir)
112            };
113            Vfs::from_snapshots(snapshots, &path_template, &time_template, latest, identical)?
114        };
115
116        let addr = config
117            .webdav
118            .address
119            .clone()
120            .unwrap_or_else(|| "localhost:8000".to_string())
121            .to_socket_addrs()?
122            .next()
123            .ok_or_else(|| anyhow!("no address given"))?;
124
125        let file_access = config.webdav.file_access.as_ref().map_or_else(
126            || {
127                if repo.config().is_hot == Some(true) {
128                    Ok(FilePolicy::Forbidden)
129                } else {
130                    Ok(FilePolicy::Read)
131                }
132            },
133            |s| s.parse(),
134        )?;
135
136        let webdavfs = WebDavFS::new(repo, vfs, file_access);
137        let dav_server = DavHandler::builder()
138            .filesystem(Box::new(webdavfs))
139            .build_handler();
140
141        tokio::runtime::Builder::new_current_thread()
142            .enable_all()
143            .build()?
144            .block_on(async {
145                warp::serve(dav_handler(dav_server)).run(addr).await;
146            });
147
148        Ok(())
149    }
150}