From 6e804520a453be59ac68df2cd81f7dac9ab626f8 Mon Sep 17 00:00:00 2001 From: Alexander Weiss Date: Sun, 28 Jan 2024 23:12:47 +0100 Subject: [PATCH] add webdav command --- Cargo.toml | 6 ++- src/commands.rs | 8 +++ src/commands/webdav.rs | 107 +++++++++++++++++++++++++++++++++++++++++ src/config.rs | 8 +-- 4 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 src/commands/webdav.rs diff --git a/Cargo.toml b/Cargo.toml index 19364cd0b..3fd245e3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,10 +31,11 @@ description = { workspace = true } members = ["crates/rustic_testing", "xtask"] [features] -default = ["self-update"] +default = ["self-update", "webdav"] mimalloc = ["dep:mimalloc"] jemallocator = ["dep:jemallocator-global"] self-update = ["dep:self_update", "dep:semver"] +webdav = ["dep:dav-server", "dep:warp", "dep:tokio", "rustic_core/webdav"] [[bin]] name = "rustic" @@ -89,6 +90,9 @@ jemallocator-global = { version = "0.3.2", optional = true } mimalloc = { version = "0.1.39", default_features = false, optional = true } rhai = { workspace = true } simplelog = { workspace = true } +dav-server = { version = "0.5.8", default-features = false, features = ["warp-compat"], optional = true } +warp = { version = "0.3.6", optional = true } +tokio = {version = "1", optional = true } [dev-dependencies] abscissa_core = { workspace = true, features = ["testing"] } diff --git a/src/commands.rs b/src/commands.rs index 2481187de..11985f29e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -22,11 +22,15 @@ pub(crate) mod self_update; pub(crate) mod show_config; pub(crate) mod snapshots; pub(crate) mod tag; +#[cfg(feature = "webdav")] +pub(crate) mod webdav; use std::fs::File; use std::path::PathBuf; use std::str::FromStr; +#[cfg(feature = "webdav")] +use crate::commands::webdav::WebDavCmd; use crate::{ commands::{ backup::BackupCmd, cat::CatCmd, check::CheckCmd, completions::CompletionsCmd, @@ -128,6 +132,10 @@ enum RusticCmd { /// Change tags of snapshots Tag(TagCmd), + + /// Start a webdav server which allows to access the repository + #[cfg(feature = "webdav")] + Webdav(WebDavCmd), } fn styles() -> Styles { diff --git a/src/commands/webdav.rs b/src/commands/webdav.rs new file mode 100644 index 000000000..f26d9f433 --- /dev/null +++ b/src/commands/webdav.rs @@ -0,0 +1,107 @@ +//! `mount` subcommand +use std::net::ToSocketAddrs; + +use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; +use abscissa_core::{Command, Runnable, Shutdown}; +use anyhow::{anyhow, Result}; +use dav_server::{warp::dav_handler, DavHandler}; +use rustic_core::vfs::{FilePolicy, IdenticalSnapshot, Latest, Vfs}; + +#[derive(clap::Parser, Command, Debug)] +pub(crate) struct WebDavCmd { + /// Address to bind the webdav server to + #[clap(long, value_name = "ADDRESS", default_value = "localhost:8000")] + addr: String, + + /// The path template to use for snapshots. {id}, {id_long}, {time}, {username}, {hostname}, {label}, {tags}, {backup_start}, {backup_end} are replaced. [default: "[{hostname}]/[{label}]/{time}"] + #[clap(long)] + path_template: Option, + + /// The time template to use to display times in the path template. See https://docs.rs/chrono/latest/chrono/format/strftime/index.html for format options. [default: "%Y-%m-%d_%H-%M-%S"] + #[clap(long)] + time_template: Option, + + /// Use symlinks. This may not be supported by all WebDAV clients + #[clap(long)] + symlinks: bool, + + /// How to handle access to files. Default: "forbidden" for hot/cold repositories, else "read" + #[clap(long)] + file_access: Option, + + /// Specify directly which path to mount + #[clap(value_name = "SNAPSHOT[:PATH]")] + snap: Option, +} + +impl Runnable for WebDavCmd { + fn run(&self) { + if let Err(err) = self.inner_run() { + status_err!("{}", err); + RUSTIC_APP.shutdown(Shutdown::Crash); + }; + } +} + +impl WebDavCmd { + fn inner_run(&self) -> Result<()> { + let config = RUSTIC_APP.config(); + let repo = open_repository(&config.repository)?.to_indexed()?; + + let file_access = self.file_access.unwrap_or_else(|| { + if repo.config().is_hot == Some(true) { + FilePolicy::Forbidden + } else { + FilePolicy::Read + } + }); + + let path_template = self + .path_template + .clone() + .unwrap_or_else(|| "[{hostname}]/[{label}]/{time}".to_string()); + let time_template = self + .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) = &self.snap { + let node = repo.node_from_snapshot_path(snap, sn_filter)?; + Vfs::from_dirnode(node, file_access) + } else { + let snapshots = repo.get_matching_snapshots(sn_filter)?; + let (latest, identical) = if self.symlinks { + (Latest::AsLink, IdenticalSnapshot::AsLink) + } else { + (Latest::AsDir, IdenticalSnapshot::AsDir) + }; + Vfs::from_snapshots( + snapshots, + path_template, + time_template, + latest, + identical, + file_access, + )? + }; + let addr = self + .addr + .to_socket_addrs()? + .next() + .ok_or_else(|| anyhow!("no address given"))?; + let dav_server = DavHandler::builder() + .filesystem(vfs.into_webdav_fs(repo)) + .build_handler(); + + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()? + .block_on(async { + warp::serve(dav_handler(dav_server)).run(addr).await; + }); + + Ok(()) + } +} diff --git a/src/config.rs b/src/config.rs index b31ad919c..3ab0630d7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -65,15 +65,15 @@ pub struct RusticConfig { #[derive(Clone, Default, Debug, Parser, Deserialize, Merge)] #[serde(default, rename_all = "kebab-case")] pub struct AllRepositoryOptions { - /// Repository options + /// Backend options #[clap(flatten)] #[serde(flatten)] - pub repo: RepositoryOptions, + pub be: BackendOptions, - /// Backend options + /// Repository options #[clap(flatten)] #[serde(flatten)] - pub be: BackendOptions, + pub repo: RepositoryOptions, } impl RusticConfig {