rustic_rs/commands/
webdav.rs1#![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 #[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]")]
54 #[merge(strategy=conflate::option::overwrite_none)]
55 snapshot_path: Option<String>,
56}
57
58impl Override<RusticConfig> for WebDavCmd {
59 fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
63 let mut self_config = self.clone();
64 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 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}