rustic_rs/commands/
webdav.rs#![allow(clippy::doc_markdown)]
use std::{net::ToSocketAddrs, str::FromStr};
use crate::{repository::CliIndexedRepo, status_err, Application, RusticConfig, RUSTIC_APP};
use abscissa_core::{config::Override, Command, FrameworkError, Runnable, Shutdown};
use anyhow::{anyhow, Result};
use conflate::Merge;
use dav_server::{warp::dav_handler, DavHandler};
use serde::{Deserialize, Serialize};
use rustic_core::vfs::{FilePolicy, IdenticalSnapshot, Latest, Vfs};
#[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: CliIndexedRepo) -> 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 sn_filter = |sn: &_| config.snapshot_filter.matches(sn);
        let vfs = if let Some(snap) = &config.webdav.snapshot_path {
            let node = repo.node_from_snapshot_path(snap, sn_filter)?;
            Vfs::from_dir_node(&node)
        } else {
            let snapshots = repo.get_matching_snapshots(sn_filter)?;
            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| FilePolicy::from_str(s),
        )?;
        let dav_server = DavHandler::builder()
            .filesystem(vfs.into_webdav_fs(repo, file_access))
            .build_handler();
        tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()?
            .block_on(async {
                warp::serve(dav_handler(dav_server)).run(addr).await;
            });
        Ok(())
    }
}