rustic_rs/commands/
webdav.rs1#![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 #[clap(long, value_name = "ADDRESS")]
29 #[merge(strategy=conflate::option::overwrite_none)]
30 address: Option<String>,
31
32 #[clap(long)]
34 #[merge(strategy=conflate::option::overwrite_none)]
35 path_template: Option<String>,
36
37 #[clap(long)]
39 #[merge(strategy=conflate::option::overwrite_none)]
40 time_template: Option<String>,
41
42 #[clap(long)]
44 #[merge(strategy=conflate::bool::overwrite_false)]
45 symlinks: bool,
46
47 #[clap(long)]
49 #[merge(strategy=conflate::option::overwrite_none)]
50 file_access: Option<String>,
51
52 #[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 fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
65 let mut self_config = self.clone();
66 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 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}