Skip to main content

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::{IndexedRepo, 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://pubs.opengroup.org/onlinepubs/009695399/functions/strftime.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    ///
54    /// Snapshot can be identified the following ways: "01a2b3c4" or "latest" or "latest~N" (N >= 0)
55    #[clap(value_name = "SNAPSHOT[:PATH]")]
56    #[merge(strategy=conflate::option::overwrite_none)]
57    snapshot_path: Option<String>,
58}
59
60impl Override<RusticConfig> for WebDavCmd {
61    // Process the given command line options, overriding settings from
62    // a configuration file using explicit flags taken from command-line
63    // arguments.
64    fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
65        let mut self_config = self.clone();
66        // merge "webdav" section from config file, if given
67        self_config.merge(config.webdav);
68        config.webdav = self_config;
69        Ok(config)
70    }
71}
72
73impl Runnable for WebDavCmd {
74    fn run(&self) {
75        if let Err(err) = RUSTIC_APP
76            .config()
77            .repository
78            .run_indexed(|repo| self.inner_run(repo))
79        {
80            status_err!("{}", err);
81            RUSTIC_APP.shutdown(Shutdown::Crash);
82        };
83    }
84}
85
86impl WebDavCmd {
87    /// be careful about self VS RUSTIC_APP.config() usage
88    /// only the RUSTIC_APP.config() involves the TOML and ENV merged configurations
89    /// see https://github.com/rustic-rs/rustic/issues/1242
90    fn inner_run(&self, repo: IndexedRepo) -> Result<()> {
91        let config = RUSTIC_APP.config();
92
93        let path_template = config
94            .webdav
95            .path_template
96            .clone()
97            .unwrap_or_else(|| "[{hostname}]/[{label}]/{time}".to_string());
98        let time_template = config
99            .webdav
100            .time_template
101            .clone()
102            .unwrap_or_else(|| "%Y-%m-%d_%H-%M-%S".to_string());
103
104        let vfs = if let Some(snap) = &config.webdav.snapshot_path {
105            let node =
106                repo.node_from_snapshot_path(snap, |sn| config.snapshot_filter.matches(sn))?;
107            Vfs::from_dir_node(&node)
108        } else {
109            let snapshots = get_filtered_snapshots(&repo)?;
110            let (latest, identical) = if config.webdav.symlinks {
111                (Latest::AsLink, IdenticalSnapshot::AsLink)
112            } else {
113                (Latest::AsDir, IdenticalSnapshot::AsDir)
114            };
115            Vfs::from_snapshots(snapshots, &path_template, &time_template, latest, identical)?
116        };
117
118        let addr = config
119            .webdav
120            .address
121            .clone()
122            .unwrap_or_else(|| "localhost:8000".to_string())
123            .to_socket_addrs()?
124            .next()
125            .ok_or_else(|| anyhow!("no address given"))?;
126
127        let file_access = config.webdav.file_access.as_ref().map_or_else(
128            || {
129                if repo.config().is_hot == Some(true) {
130                    Ok(FilePolicy::Forbidden)
131                } else {
132                    Ok(FilePolicy::Read)
133                }
134            },
135            |s| s.parse(),
136        )?;
137
138        let webdavfs = WebDavFS::new(repo, vfs, file_access);
139        let dav_server = DavHandler::builder()
140            .filesystem(Box::new(webdavfs))
141            .build_handler();
142
143        tokio::runtime::Builder::new_current_thread()
144            .enable_all()
145            .build()?
146            .block_on(async {
147                warp::serve(dav_handler(dav_server)).run(addr).await;
148            });
149
150        Ok(())
151    }
152}