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 rustic_core::vfs::{FilePolicy, IdenticalSnapshot, Latest, Vfs};
14
15use abscissa_core::{Command, FrameworkError, Runnable, Shutdown, config::Override};
16use anyhow::{Result, anyhow};
17use axum::{
18 Router,
19 extract::{Request, State},
20 response::IntoResponse,
21 routing::any,
22};
23use conflate::Merge;
24use dav_server::DavHandler;
25use log::info;
26use serde::{Deserialize, Serialize};
27
28mod webdavfs;
29use webdavfs::WebDavFS;
30
31#[derive(Clone, Command, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)]
32#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
33pub struct WebDavCmd {
34 #[clap(long, value_name = "ADDRESS")]
36 #[merge(strategy=conflate::option::overwrite_none)]
37 address: Option<String>,
38
39 #[clap(long)]
41 #[merge(strategy=conflate::option::overwrite_none)]
42 path_template: Option<String>,
43
44 #[clap(long)]
46 #[merge(strategy=conflate::option::overwrite_none)]
47 time_template: Option<String>,
48
49 #[clap(long)]
51 #[merge(strategy=conflate::bool::overwrite_false)]
52 symlinks: bool,
53
54 #[clap(long)]
56 #[merge(strategy=conflate::option::overwrite_none)]
57 file_access: Option<String>,
58
59 #[clap(value_name = "SNAPSHOT[:PATH]")]
63 #[merge(strategy=conflate::option::overwrite_none)]
64 snapshot_path: Option<String>,
65}
66
67impl Override<RusticConfig> for WebDavCmd {
68 fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
72 let mut self_config = self.clone();
73 self_config.merge(config.webdav);
75 config.webdav = self_config;
76 Ok(config)
77 }
78}
79
80impl Runnable for WebDavCmd {
81 fn run(&self) {
82 if let Err(err) = RUSTIC_APP
83 .config()
84 .repository
85 .run_indexed(|repo| self.inner_run(repo))
86 {
87 status_err!("{}", err);
88 RUSTIC_APP.shutdown(Shutdown::Crash);
89 };
90 }
91}
92
93impl WebDavCmd {
94 fn inner_run(&self, repo: IndexedRepo) -> Result<()> {
98 let config = RUSTIC_APP.config();
99
100 let path_template = config
101 .webdav
102 .path_template
103 .clone()
104 .unwrap_or_else(|| "[{hostname}]/[{label}]/{time}".to_string());
105 let time_template = config
106 .webdav
107 .time_template
108 .clone()
109 .unwrap_or_else(|| "%Y-%m-%d_%H-%M-%S".to_string());
110
111 let vfs = if let Some(snap) = &config.webdav.snapshot_path {
112 let node =
113 repo.node_from_snapshot_path(snap, |sn| config.snapshot_filter.matches(sn))?;
114 Vfs::from_dir_node(&node)
115 } else {
116 let snapshots = get_filtered_snapshots(&repo)?;
117 let (latest, identical) = if config.webdav.symlinks {
118 (Latest::AsLink, IdenticalSnapshot::AsLink)
119 } else {
120 (Latest::AsDir, IdenticalSnapshot::AsDir)
121 };
122 Vfs::from_snapshots(snapshots, &path_template, &time_template, latest, identical)?
123 };
124
125 let addr = config
126 .webdav
127 .address
128 .clone()
129 .unwrap_or_else(|| "localhost:8000".to_string())
130 .to_socket_addrs()?
131 .next()
132 .ok_or_else(|| anyhow!("no address given"))?;
133
134 let file_access = config.webdav.file_access.as_ref().map_or_else(
135 || {
136 if repo.config().is_hot == Some(true) {
137 Ok(FilePolicy::Forbidden)
138 } else {
139 Ok(FilePolicy::Read)
140 }
141 },
142 |s| s.parse(),
143 )?;
144
145 let webdavfs = WebDavFS::new(repo, vfs, file_access);
146 let dav_server = DavHandler::builder()
147 .filesystem(Box::new(webdavfs))
148 .build_handler();
149
150 let app = Router::new()
151 .route("/", any(handle_dav))
152 .route("/{*path}", any(handle_dav))
153 .with_state(dav_server);
154
155 info!("serving webdav on {addr}");
156 tokio::runtime::Builder::new_current_thread()
157 .enable_all()
158 .build()?
159 .block_on(async {
160 axum::serve(tokio::net::TcpListener::bind(addr).await?, app).await
161 })?;
162
163 Ok(())
164 }
165}
166
167async fn handle_dav(State(dav): State<DavHandler>, req: Request) -> impl IntoResponse {
168 dav.handle(req).await
169}