#![allow(clippy::doc_markdown)]
use std::net::ToSocketAddrs;
use crate::{
Application, RUSTIC_APP, RusticConfig,
repository::{IndexedRepo, get_filtered_snapshots},
status_err,
};
use rustic_core::vfs::{FilePolicy, IdenticalSnapshot, Latest, Vfs};
use abscissa_core::{Command, FrameworkError, Runnable, Shutdown, config::Override};
use anyhow::{Result, anyhow};
use axum::{
Router,
extract::{Request, State},
response::IntoResponse,
routing::any,
};
use conflate::Merge;
use dav_server::DavHandler;
use log::info;
use serde::{Deserialize, Serialize};
mod webdavfs;
use webdavfs::WebDavFS;
#[derive(Clone, Command, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct WebDavCmd {
#[clap(long, value_name = "ADDRESS")]
#[merge(strategy=conflate::option::overwrite_none)]
address: Option<String>,
#[clap(long)]
#[merge(strategy=conflate::option::overwrite_none)]
path_template: Option<String>,
#[clap(long)]
#[merge(strategy=conflate::option::overwrite_none)]
time_template: Option<String>,
#[clap(long)]
#[merge(strategy=conflate::bool::overwrite_false)]
symlinks: bool,
#[clap(long)]
#[merge(strategy=conflate::option::overwrite_none)]
file_access: Option<String>,
#[clap(value_name = "SNAPSHOT[:PATH]")]
#[merge(strategy=conflate::option::overwrite_none)]
snapshot_path: Option<String>,
}
impl Override<RusticConfig> for WebDavCmd {
fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
let mut self_config = self.clone();
self_config.merge(config.webdav);
config.webdav = self_config;
Ok(config)
}
}
impl Runnable for WebDavCmd {
fn run(&self) {
if let Err(err) = RUSTIC_APP
.config()
.repository
.run_indexed(|repo| self.inner_run(repo))
{
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
};
}
}
impl WebDavCmd {
fn inner_run(&self, repo: IndexedRepo) -> Result<()> {
let config = RUSTIC_APP.config();
let path_template = config
.webdav
.path_template
.clone()
.unwrap_or_else(|| "[{hostname}]/[{label}]/{time}".to_string());
let time_template = config
.webdav
.time_template
.clone()
.unwrap_or_else(|| "%Y-%m-%d_%H-%M-%S".to_string());
let vfs = if let Some(snap) = &config.webdav.snapshot_path {
let node =
repo.node_from_snapshot_path(snap, |sn| config.snapshot_filter.matches(sn))?;
Vfs::from_dir_node(&node)
} else {
let snapshots = get_filtered_snapshots(&repo)?;
let (latest, identical) = if config.webdav.symlinks {
(Latest::AsLink, IdenticalSnapshot::AsLink)
} else {
(Latest::AsDir, IdenticalSnapshot::AsDir)
};
Vfs::from_snapshots(snapshots, &path_template, &time_template, latest, identical)?
};
let addr = config
.webdav
.address
.clone()
.unwrap_or_else(|| "localhost:8000".to_string())
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow!("no address given"))?;
let file_access = config.webdav.file_access.as_ref().map_or_else(
|| {
if repo.config().is_hot == Some(true) {
Ok(FilePolicy::Forbidden)
} else {
Ok(FilePolicy::Read)
}
},
|s| s.parse(),
)?;
let webdavfs = WebDavFS::new(repo, vfs, file_access);
let dav_server = DavHandler::builder()
.filesystem(Box::new(webdavfs))
.build_handler();
let app = Router::new()
.route("/", any(handle_dav))
.route("/{*path}", any(handle_dav))
.with_state(dav_server);
info!("serving webdav on {addr}");
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
axum::serve(tokio::net::TcpListener::bind(addr).await?, app).await
})?;
Ok(())
}
}
async fn handle_dav(State(dav): State<DavHandler>, req: Request) -> impl IntoResponse {
dav.handle(req).await
}