diff --git a/Cargo.lock b/Cargo.lock index 5bfcfdcb5..16cb19239 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -534,8 +534,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c64043d6c7b7a4c58e39e7efccfdea7b93d885a795d0c054a69dbbf4dd52686" dependencies = [ "crossterm", - "strum", - "strum_macros", + "strum 0.25.0", + "strum_macros 0.25.3", "unicode-width", ] @@ -830,6 +830,42 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "dav-server" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0b91592a7b705ba0a9487b7475fca78bce97473e220f8cc3948381b1c81b793" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers", + "htmlescape", + "http", + "http-body", + "hyper", + "lazy_static", + "log", + "mime_guess", + "percent-encoding", + "pin-project", + "pin-utils", + "regex", + "time", + "tokio", + "url", + "uuid", + "warp", + "xml-rs", + "xmltree", +] + [[package]] name = "der" version = "0.7.8" @@ -1188,6 +1224,7 @@ checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1210,6 +1247,17 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.30" @@ -1346,6 +1394,30 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -1385,6 +1457,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + [[package]] name = "http" version = "0.2.11" @@ -1797,6 +1875,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1817,6 +1905,24 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin 0.9.8", + "version_check", +] + [[package]] name = "nix" version = "0.27.1" @@ -2367,6 +2473,18 @@ dependencies = [ "serde", ] +[[package]] +name = "quick_cache" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c20af3800cee5134b79a3bd4a3d4b583c16ccfa5f53338f46400851a5b3819" +dependencies = [ + "ahash", + "equivalent", + "hashbrown 0.14.3", + "parking_lot", +] + [[package]] name = "quote" version = "1.0.35" @@ -2627,6 +2745,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "runtime-format" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09958d5b38bca768ede7928c767c89a08ba568144a7b61992aecae79b03c8c94" +dependencies = [ + "tinyvec", +] + [[package]] name = "rust-ini" version = "0.20.0" @@ -2664,6 +2791,7 @@ dependencies = [ "clap", "clap_complete", "comfy-table", + "dav-server", "dialoguer", "dircmp", "directories", @@ -2690,13 +2818,15 @@ dependencies = [ "simplelog", "tempfile", "thiserror", + "tokio", "toml 0.8.8", + "warp", ] [[package]] name = "rustic_backend" version = "0.1.0" -source = "git+https://github.com/rustic-rs/rustic_core.git#27499dbf4c2381f4075a2acb3b16cccdde9d65e5" +source = "git+https://github.com/rustic-rs/rustic_core.git#14d1533ca98d4807d35b1bd11499dd7cca00e102" dependencies = [ "aho-corasick", "anyhow", @@ -2717,8 +2847,8 @@ dependencies = [ "rustic_core", "serde", "shell-words", - "strum", - "strum_macros", + "strum 0.25.0", + "strum_macros 0.25.3", "thiserror", "tokio", "url", @@ -2728,7 +2858,7 @@ dependencies = [ [[package]] name = "rustic_core" version = "0.1.2" -source = "git+https://github.com/rustic-rs/rustic_core.git#27499dbf4c2381f4075a2acb3b16cccdde9d65e5" +source = "git+https://github.com/rustic-rs/rustic_core.git#14d1533ca98d4807d35b1bd11499dd7cca00e102" dependencies = [ "aes256ctr_poly1305aes", "anyhow", @@ -2740,6 +2870,7 @@ dependencies = [ "chrono", "clap", "crossbeam-channel", + "dav-server", "derivative", "derive_more", "derive_setters", @@ -2749,6 +2880,7 @@ dependencies = [ "enum-map", "enum-map-derive", "filetime", + "futures", "gethostname", "hex", "humantime", @@ -2760,8 +2892,10 @@ dependencies = [ "nix", "pariter", "path-dedot", + "quick_cache", "rand", "rayon", + "runtime-format", "scrypt", "serde", "serde-aux", @@ -2770,6 +2904,7 @@ dependencies = [ "serde_with", "sha2", "shell-words", + "strum 0.26.1", "thiserror", "walkdir", "xattr", @@ -2880,6 +3015,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -3286,6 +3427,15 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +[[package]] +name = "strum" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" +dependencies = [ + "strum_macros 0.26.1", +] + [[package]] name = "strum_macros" version = "0.25.3" @@ -3299,6 +3449,19 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "strum_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.48", +] + [[package]] name = "subtle" version = "2.5.0" @@ -3553,6 +3716,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.10" @@ -3622,6 +3808,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3705,12 +3892,40 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -3777,6 +3992,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8parse" version = "0.2.1" @@ -3843,6 +4064,37 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warp" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e92e22e03ff1230c03a1a8ee37d2f89cd489e2e541b7550d6afad96faed169" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers", + "http", + "hyper", + "log", + "mime", + "mime_guess", + "multer", + "percent-encoding", + "pin-project", + "rustls-pemfile", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tokio-util", + "tower-service", + "tracing", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -4146,6 +4398,21 @@ dependencies = [ "rustix", ] +[[package]] +name = "xml-rs" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + [[package]] name = "xtask" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 19364cd0b..d9f1c68f4 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" @@ -79,6 +80,7 @@ merge = { workspace = true } bytesize = { workspace = true } comfy-table = { workspace = true } +dav-server = { version = "0.5.8", default-features = false, features = ["warp-compat"], optional = true } dialoguer = { workspace = true } directories = { workspace = true } gethostname = { workspace = true } @@ -89,6 +91,8 @@ jemallocator-global = { version = "0.3.2", optional = true } mimalloc = { version = "0.1.39", default_features = false, optional = true } rhai = { workspace = true } simplelog = { workspace = true } +tokio = { version = "1", optional = true } +warp = { version = "0.3.6", optional = true } [dev-dependencies] abscissa_core = { workspace = true, features = ["testing"] } @@ -102,6 +106,7 @@ toml = { workspace = true } [target.'cfg(not(windows))'.dependencies] libc = "0.2.152" + [workspace.dependencies] abscissa_core = { version = "0.7.0", default-features = false, features = ["application"] } rustic_backend = { git = "https://github.com/rustic-rs/rustic_core.git", features = ["cli"] } diff --git a/config/README.md b/config/README.md index 7790f4c1f..68c51082d 100644 --- a/config/README.md +++ b/config/README.md @@ -174,3 +174,21 @@ the repository section. | warm-up | If true, warms up the target repository by file access. | Not set | | | warm-up-command | Command to warm up the target repository. | Not set | | | warm-up-wait | The wait time for warming up the target repository. | Not set | | + +### WebDAV Options + +`rustic` supports mounting snapshots via WebDAV. This is useful if you want to +access your snapshots via a file manager. + +**Note**: `https://` and Authentication are not supported yet. + +The following options are available to be used in your configuration file: + +| Attribute | Description | Default Value | Example Value | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------- | +| address | Address of the WebDAV server. | localhost:8000 | | +| path-template | The path template to use for snapshots. {id}, {id_long}, {time}, {username}, {hostname}, {label}, {tags}, {backup_start}, {backup_end} are replaced. | `[{hostname}]/[{label}]/{time}` | | +| time-template | The time template to use to display times in the path template. See for format options. | `%Y-%m-%d_%H-%M-%S` | | +| symlinks | If true, follows symlinks. | false | | +| file-access | How to handle access to files. | "forbidden" for hot/cold repositories, else "read" | | +| snapshot-path | Specify directly which snapshot/path to serve | Not set, this will generate a virtual tree with all snapshots using path-template | | diff --git a/config/full.toml b/config/full.toml index 2e519cf77..bf0ad9635 100644 --- a/config/full.toml +++ b/config/full.toml @@ -169,3 +169,11 @@ warm-up-wait = "10min" # Default: not set [[copy.targets]] repository = "/repo/rustic2" # Must be set # ... + +[webdav] +address = "localhost:8000" +path-template = "[{hostname}]/[{label}]/{time}" # 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}"]. Only relevant if no snapshot-path is given. +time-template = "%Y-%m-%d_%H-%M-%S" # only relevant if no snapshot-path is given +symlinks = false +file-access = "read" # Default: "forbidden" for hot/cold repos, else "read" +snapshot-path = "latest:/dir" # Default: not set - this will generate a virtual tree with all snapshots using path-template diff --git a/src/commands.rs b/src/commands.rs index 2481187de..a74736a6e 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 { @@ -227,6 +235,8 @@ impl Configurable for EntryPoint { match &self.commands { RusticCmd::Forget(cmd) => cmd.override_config(config), + #[cfg(feature = "webdav")] + RusticCmd::Webdav(cmd) => cmd.override_config(config), // subcommands that don't need special overrides use a catch all _ => Ok(config), diff --git a/src/commands/webdav.rs b/src/commands/webdav.rs new file mode 100644 index 000000000..9fc97cfca --- /dev/null +++ b/src/commands/webdav.rs @@ -0,0 +1,125 @@ +//! `webdav` subcommand +use std::{net::ToSocketAddrs, str::FromStr}; + +use crate::{commands::open_repository, status_err, Application, RusticConfig, RUSTIC_APP}; +use abscissa_core::{config::Override, Command, FrameworkError, Runnable, Shutdown}; +use anyhow::{anyhow, Result}; +use dav_server::{warp::dav_handler, DavHandler}; +use merge::Merge; +use serde::Deserialize; + +use rustic_core::vfs::{FilePolicy, IdenticalSnapshot, Latest, Vfs}; + +#[derive(Clone, Command, Default, Debug, clap::Parser, Deserialize, Merge)] +#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] +pub struct WebDavCmd { + /// Address to bind the webdav server to. [default: "localhost:8000"] + #[clap(long, value_name = "ADDRESS")] + address: Option, + + /// 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)] + #[merge(strategy = merge::bool::overwrite_false)] + symlinks: bool, + + /// How to handle access to files. [default: "forbidden" for hot/cold repositories, else "read"] + #[clap(long)] + file_access: Option, + + /// Specify directly which snapshot/path to serve + #[clap(value_name = "SNAPSHOT[:PATH]")] + snapshot_path: Option, +} + +impl Override for WebDavCmd { + // Process the given command line options, overriding settings from + // a configuration file using explicit flags taken from command-line + // arguments. + fn override_config(&self, mut config: RusticConfig) -> Result { + let mut self_config = self.clone(); + // merge "webdav" section from config file, if given + self_config.merge(config.webdav); + config.webdav = self_config; + Ok(config) + } +} + +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 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.snapshot_path { + let node = repo.node_from_snapshot_path(snap, sn_filter)?; + Vfs::from_dirnode(node) + } 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)? + }; + + let addr = self + .address + .clone() + .unwrap_or_else(|| "localhost:8000".to_string()) + .to_socket_addrs()? + .next() + .ok_or_else(|| anyhow!("no address given"))?; + + let file_access = self.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(()) + } +} diff --git a/src/config.rs b/src/config.rs index b31ad919c..3a6d3757c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -22,6 +22,8 @@ use rustic_backend::BackendOptions; use rustic_core::RepositoryOptions; use serde::{Deserialize, Serialize}; +#[cfg(feature = "webdav")] +use crate::commands::webdav::WebDavCmd; use crate::{ commands::{backup::BackupCmd, copy::Targets, forget::ForgetOptions}, config::progress_options::ProgressOptions, @@ -60,20 +62,25 @@ pub struct RusticConfig { /// Forget options #[clap(skip)] pub forget: ForgetOptions, + + #[cfg(feature = "webdav")] + /// webdav options + #[clap(skip)] + pub webdav: WebDavCmd, } #[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 {