diff --git a/.gitignore b/.gitignore index c22a45f..fc1cdb1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ # Test data created may accidentally be included /tests/test_data/test_repos/repo_remove_me /tests/test_data/test_repos/repo_remove_me_2 +tests/fixtures/test_data/rustic_server.toml #often updated with content that should not be archived /tmp_test_data \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 5ced312..33253f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,6 +294,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "block-buffer" version = "0.9.0" @@ -434,6 +440,31 @@ dependencies = [ "libc", ] +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -484,6 +515,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -615,6 +652,24 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "gcc" version = "0.3.55" @@ -845,6 +900,23 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.5.0", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "itoa" version = "1.0.10" @@ -933,6 +1005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -955,6 +1028,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1185,7 +1267,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1335,6 +1417,7 @@ dependencies = [ "htpasswd-verify", "http-body-util", "http-range", + "inquire", "pin-project", "rand 0.8.5", "rstest", @@ -1554,6 +1637,27 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1602,18 +1706,18 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" -version = "0.25.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ "heck", "proc-macro2", @@ -1876,6 +1980,18 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 4f42775..6447de1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ authors = ["rustic-rs Maintainers"] categories = ["command-line-utilities"] edition = "2021" homepage = "https://rustic.cli.rs/" -keywords = ["backup", "restic", "deduplication", "encryption", "cli", "server"] +keywords = ["backup", "restic", "deduplication", "encryption", "server"] license = "AGPL-3.0-or-later" repository = "https://github.com/rustic-rs/rustic_server" rust-version = "1.70.0" @@ -28,28 +28,28 @@ anyhow = "1.0.79" async-trait = "0.1" # FIXME: Add "headers" feature to Axum? axum = { version = "0.7.4", features = ["tracing", "multipart", "http2"] } -axum-auth = "0.7.0" -axum-extra = { version = "0.9.2", features = ["typed-header", "query", "async-read-body", "typed-routing"] } -axum-macros = "0.4.1" +axum-auth = "0.7" +axum-extra = { version = "0.9", features = ["typed-header", "query", "async-read-body", "typed-routing"] } +axum-macros = "0.4" axum-range = "0.4" -axum-server = { version = "0.6.0", features = ["tls-rustls"] } +axum-server = { version = "0.6", features = ["tls-rustls"] } clap = { version = "4.4", features = ["derive"] } -displaydoc = "0.2.4" -# enum_dispatch = "0.3.12" +displaydoc = "0.2" futures = "0.3" futures-util = "0.3" htpasswd-verify = "0.3" -http-body-util = "0.1.0" +http-body-util = "0.1" http-range = "0.1" +inquire = "0.7" pin-project = "1.1" -rand = "0.8.5" -serde = { version = "1", default-features = false, features = ["derive"] } -serde_derive = "1" -strum = { version = "0.25.0", features = ["derive"] } -thiserror = "1.0.56" +rand = "0.8" +serde = { version = "1.0", default-features = false, features = ["derive"] } +serde_derive = "1.0" +strum = { version = "0.26", features = ["derive"] } +thiserror = "1.0" tokio = { version = "1", features = ["full"] } -tokio-util = { version = "0.7.10", features = ["io", "io-util"] } -toml = "0.8.8" +tokio-util = { version = "0.7", features = ["io", "io-util"] } +toml = "0.8" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } walkdir = "2" diff --git a/config/README.md b/config/README.md index da77a3c..c43bde8 100644 --- a/config/README.md +++ b/config/README.md @@ -11,18 +11,56 @@ This folder contains a few configuration files as an example. See also the rustic configuration, described in: https://github.com/rustic-rs/rustic/tree/main/config -## `acl.toml` +# Server config file `rustic_server.toml` -This file may have any name, but requires valid toml formatting. +This file may have any name, but requires valid toml formatting, as shown below. +A path to this file can be entered on the command line when starting the server. -Format: +File format: + +``` +[server] +host_dns_name = | +port = +common_root_path = + +[repos] +# Absolute file path will be: /common_root_path>/ +# if is empty, an absolute path to a folder is expected here +storage_path = + +[authorization] +# Absolute file path will be: /common_root_path>/ +# if is empty, an absolute path to a file is expected here +auth_path = +use_auth = + +[access_control] +# Absolute file will be: /common_root_path>/ +# if is empty, an absolute path to a file is expected here +acl_path = +private_repo = +append_only = +``` + +# Access control list file `acl.toml` + +Using the server configuration file, this file may have any name, but requires +valid toml formatting, as shown below. + +A **path** to this file can be entered on the command line when starting the +server. + +File format: ``` [] ... more users -... more repositories +[] + +... more users ``` The `access_type` can have values: @@ -33,41 +71,23 @@ The `access_type` can have values: Todo: Describe "default" tag in the file. -## `rustic_server.toml` - -This file may have any name, but requires valid toml formatting. - -File format: - -``` -[server] -host_dns_name = | -port = - -[repos] -storage_path = - -[authorization] -auth_path = -use_auth = +# user credential file `.htaccess` -[access_control] -acl_path = -private_repo = -append_only = -``` +This file is formatted as a vanilla `Apache` `.htacces` file. -On top of some additional configuration items, the content of this file points -to the `acl.toml`, and `.htaccess` files. +Using the server configuration file, this file may have any name, but requires +valid formatting. -## `.htaccess` +A **path** to this file can be entered on the command line when starting the +server. In that case the file name has to be `.htaccess`. -This is a vanilla `Apache` `.htacces` file. +The server binary allows this file to be created from the command line. Execute +`rustic_server --help` for details. # Configure `rustic_server` from the command line It is also possible to configure the server from the command line, and skip the -configuration file. +server configuration file. To see all options, use: diff --git a/src/auth.rs b/src/auth.rs index 98665f8..a6e94b1 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -112,7 +112,6 @@ impl FromRequestParts for AuthFromRequest { #[cfg(test)] mod test { use super::*; - use crate::auth::Auth; use crate::test_helpers::{basic_auth_header_value, init_test_environment}; use anyhow::Result; use axum::body::Body; @@ -121,7 +120,6 @@ mod test { use axum::Router; use http_body_util::BodyExt; use std::env; - use std::path::PathBuf; use tower::ServiceExt; #[test] diff --git a/src/bin/rustic-server.rs b/src/bin/rustic-server.rs index afeda55..d7c6e7c 100644 --- a/src/bin/rustic-server.rs +++ b/src/bin/rustic-server.rs @@ -1,5 +1,6 @@ use anyhow::Result; use clap::{Parser, Subcommand}; +use rustic_server::commands::auth::HtAccessCmd; use rustic_server::commands::serve::{serve, Opts}; @@ -23,8 +24,8 @@ struct RusticServer { enum Commands { /// Start the REST web-server. Serve(Opts), - // Modify credentials in the .htaccess file. - //Auth(HtAccessCmd), + /// Modify credentials in the credential access file. + Auth(HtAccessCmd), // Create a configuration from scratch. //Config, } @@ -37,9 +38,9 @@ enum Commands { impl RusticServer { pub async fn exec(self) -> Result<()> { match self.command { - // Commands::Auth(cmd) => { - // cmd.exec()?; - // } + Commands::Auth(cmd) => { + cmd.exec()?; + } // Commands::Config => { // rustic_server_configuration()?; // } diff --git a/src/commands.rs b/src/commands.rs index ac2b204..27d5bb7 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1 +1,2 @@ +pub mod auth; pub mod serve; diff --git a/src/commands/auth.rs b/src/commands/auth.rs new file mode 100644 index 0000000..47d6138 --- /dev/null +++ b/src/commands/auth.rs @@ -0,0 +1,201 @@ +use crate::config::auth_file::HtAccess; +use anyhow::Result; +use clap::{Args, Parser, Subcommand}; +use std::fs; +use std::path::PathBuf; +use std::process::exit; + +#[derive(Parser)] +#[command()] +pub struct HtAccessCmd { + #[command(subcommand)] + command: Commands, +} + +/// The server configuration file should point us to the `.htaccess` file. +/// If not we complain to the user. +/// +/// To be nice, if the `.htaccess` file pointed to does not exist, then we create it. +/// We do so, even if it is not called `.htaccess`. +impl HtAccessCmd { + pub fn exec(&self) -> Result<()> { + match &self.command { + Commands::Add(arg) => { + add(arg)?; + } + Commands::Update(arg) => { + update(arg)?; + } + Commands::Delete(arg) => { + delete(arg)?; + } + Commands::List(arg) => { + print(arg)?; + } + }; + Ok(()) + } + + fn check(path: &PathBuf) { + //Check + if path.exists() { + if !path.is_file() { + println!( + "Error: Given path leads to a folder, not a file: \n\t{}", + path.to_string_lossy() + ); + exit(0); + } + match fs::OpenOptions::new() + //Test: "open for writing" (fail fast) + .create(false) + .truncate(false) + .append(true) + .open(path) + { + Ok(_) => {} + Err(e) => { + println!( + "No write access to the htaccess file.{}", + path.to_string_lossy() + ); + println!("Got error: {}.", e); + exit(0); + } + } + } else { + //"touch server_config file" (fail fast) + match fs::OpenOptions::new() + .create(true) + .truncate(false) + .write(true) + .open(path) + { + Ok(_) => {} + Err(e) => { + println!( + "Failed to create empty server configuration file.{}", + &path.to_string_lossy() + ); + println!("Got error: {}.", e); + exit(0); + } + } + } + } +} + +#[derive(Subcommand)] +enum Commands { + /// Add a new credential to the .htaccess file. + /// If the username already exists it will update the password only. + Add(AddArg), + /// Change the password for an existing user. + Update(AddArg), + /// Delete an existing credential from the .htaccess file. + Delete(DelArg), + /// List all users known in the .htaccess file. + List(PrintArg), +} + +#[derive(Args)] +struct AddArg { + ///Path to authorization file + #[arg(short = 'f')] + pub config_path: PathBuf, + /// Name of the user to be added. + #[arg(short = 'u')] + user: String, + /// Password. + #[arg(short = 'p')] + password: String, +} + +#[derive(Args)] +struct DelArg { + ///Path to authorization file + #[arg(short = 'f')] + pub config_path: PathBuf, + /// Name of the user to be removed. + #[arg(short = 'u')] + user: String, +} + +#[derive(Args)] +struct PrintArg { + ///Path to authorization file + #[arg(short = 'f')] + pub config_path: PathBuf, +} + +fn add(arg: &AddArg) -> Result<()> { + let ht_access_path = PathBuf::from(&arg.config_path); + HtAccessCmd::check(&ht_access_path); + let mut ht_access = HtAccess::from_file(&ht_access_path)?; + + if ht_access.users().contains(&arg.user.to_string()) { + println!( + "User '{}' exists; use update to change password. No changes were made.", + arg.user.as_str() + ); + exit(0); + } + + ht_access.update(arg.user.as_str(), arg.password.as_str()); + + ht_access.to_file()?; + Ok(()) +} + +fn update(arg: &AddArg) -> Result<()> { + let ht_access_path = PathBuf::from(&arg.config_path); + HtAccessCmd::check(&ht_access_path); + let mut ht_access = HtAccess::from_file(&ht_access_path)?; + + if !ht_access.credentials.contains_key(arg.user.as_str()) { + println!( + "I can not find a user with name {}. Use add command?", + arg.user.as_str() + ); + exit(0); + } + ht_access.update(arg.user.as_str(), arg.password.as_str()); + ht_access.to_file()?; + Ok(()) +} + +fn delete(arg: &DelArg) -> Result<()> { + let ht_access_path = PathBuf::from(&arg.config_path); + HtAccessCmd::check(&ht_access_path); + let mut ht_access = HtAccess::from_file(&ht_access_path)?; + + if ht_access.users().contains(&arg.user.to_string()) { + println!("Deleting user with name {}.", arg.user.as_str()); + ht_access.delete(arg.user.as_str()); + ht_access.to_file()?; + } else { + println!( + "Could not find a user with name {}. No changes were made.", + arg.user.as_str() + ) + }; + Ok(()) +} + +fn print(arg: &PrintArg) -> Result<()> { + let ht_access_path = PathBuf::from(&arg.config_path); + HtAccessCmd::check(&ht_access_path); + let ht_access = HtAccess::from_file(&ht_access_path)?; + + println!("Listing users in the access file for a rustic_server."); + println!( + "\tConfiguration file used: {} ", + ht_access_path.to_string_lossy() + ); + println!("List:"); + for u in ht_access.users() { + println!("\t{}", u); + } + println!("Done."); + Ok(()) +} diff --git a/src/commands/serve.rs b/src/commands/serve.rs index 03c05c5..419fc3a 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -19,7 +19,7 @@ use crate::{ pub async fn serve(opts: Opts) -> Result<()> { match &opts.config { Some(config) => { - let config_path = PathBuf::new().join(config); + let config_path = PathBuf::from(config); let server_config = ServerConfiguration::from_file(&config_path)?; if let Some(level) = server_config.log_level { @@ -28,29 +28,54 @@ pub async fn serve(opts: Opts) -> Result<()> { init_tracing(); } + let root = server_config.server.common_root_path.clone(); + // Repository storage - let storage_path = PathBuf::new().join(server_config.repos.storage_path); + //----------------------------------- + let storage_path = if root.is_empty() { + PathBuf::from(server_config.repos.storage_path) + } else { + assert!(!server_config.repos.storage_path.starts_with('/')); + PathBuf::from(root.clone()).join(server_config.repos.storage_path) + }; let storage = LocalStorage::try_new(&storage_path).map_err(|err| { ErrorKind::GeneralStorageError(format!("Could not create storage: {}", err)) })?; // Authorization user/password + //----------------------------------- let auth_config = server_config.authorization; let no_auth = !auth_config.use_auth; let path = match auth_config.auth_path { None => PathBuf::new(), - Some(p) => PathBuf::new().join(p), + Some(p) => { + if root.is_empty() { + PathBuf::from(p) + } else { + assert!(!p.starts_with('/')); + PathBuf::from(root.clone()).join(p) + } + } }; let auth = Auth::from_file(no_auth, &path).map_err(|err| { ErrorKind::InternalError(format!("Could not read file: {} at {:?}", err, path)) })?; // Access control to the repositories + //----------------------------------- let acl_config = server_config.access_control; - let path = acl_config.acl_path.map(|p| PathBuf::new().join(p)); + let path = acl_config.acl_path.map(|p| { + if root.is_empty() { + PathBuf::from(p) + } else { + assert!(!p.starts_with('/')); + PathBuf::from(root.clone()).join(p) + } + }); let acl = Acl::from_file(acl_config.append_only, acl_config.private_repo, path)?; // Server definition + //----------------------------------- let s_addr = server_config.server; let s_str = format!("{}:{}", s_addr.host_dns_name, s_addr.port); tracing::info!("[serve] Listening on: {}", &s_str); @@ -71,7 +96,7 @@ pub async fn serve(opts: Opts) -> Result<()> { err, opts.path )) })?; - let acl = Acl::from_file(opts.append_only, opts.private_repo, None)?; + let acl = Acl::from_file(opts.append_only, opts.private_repo, opts.acl)?; start_web_server( acl, @@ -94,7 +119,7 @@ pub async fn serve(opts: Opts) -> Result<()> { #[command(name = "rustic-server")] #[command(bin_name = "rustic-server")] pub struct Opts { - /// Server configuration file + /// Server configuration file; Overrides all other options. #[arg(short, long)] pub config: Option, /// listen address @@ -106,7 +131,7 @@ pub struct Opts { /// disable .htpasswd authentication #[arg(long)] pub no_auth: bool, - /// file to read per-repo ACLs from + /// Full path including file name to read from. Governs per-repo ACLs #[arg(long)] pub acl: Option, /// set standard acl to append only mode diff --git a/src/config/auth_file.rs b/src/config/auth_file.rs index 22d4290..d3b7e3a 100644 --- a/src/config/auth_file.rs +++ b/src/config/auth_file.rs @@ -61,7 +61,7 @@ impl HtAccess { self.insert(cred); } - /// Removes one credential by user name + /// Removes one credential by username pub fn delete(&mut self, name: &str) { self.credentials.remove(name); } diff --git a/src/config/server.rs b/src/config/server.rs index f8902ee..84e9539 100644 --- a/src/config/server.rs +++ b/src/config/server.rs @@ -43,6 +43,7 @@ pub struct Authorization { pub struct Server { pub host_dns_name: String, pub port: usize, + pub common_root_path: String, } #[derive(Clone, Serialize, Deserialize, Debug)] @@ -105,7 +106,10 @@ mod test { let config = ServerConfiguration::from_file(&config_path)?; assert_eq!(config.server.host_dns_name, "127.0.0.1"); - assert_eq!(config.repos.storage_path, "./test_data/test_repos/"); + assert_eq!( + config.repos.storage_path, + "rustic_server/tests/fixtures/test_data/test_repos/" + ); Ok(()) } @@ -117,6 +121,7 @@ mod test { let server = Server { host_dns_name: "127.0.0.1".to_string(), port: 2222, + common_root_path: "".into(), }; let tls: Option = Some(TLS { diff --git a/src/typed_path.rs b/src/typed_path.rs index 3ee61b0..6f3e69e 100644 --- a/src/typed_path.rs +++ b/src/typed_path.rs @@ -1,6 +1,6 @@ use axum_extra::routing::TypedPath; use serde_derive::{Deserialize, Serialize}; -use strum::{AsRefStr, Display, EnumString, EnumVariantNames, IntoStaticStr}; +use strum::{AsRefStr, Display, EnumString, IntoStaticStr, VariantNames}; pub trait PathParts { fn parts(&self) -> (Option, Option, Option) { @@ -32,7 +32,7 @@ pub trait PathParts { Deserialize, IntoStaticStr, AsRefStr, - EnumVariantNames, + VariantNames, EnumString, )] #[serde(rename_all = "lowercase")] diff --git a/src/web.rs b/src/web.rs index f0bfc0b..c181edb 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,9 +1,7 @@ use std::net::SocketAddr; +use axum::routing::{delete, get, head, post}; use axum::{middleware, Router}; -use axum_extra::routing::{ - RouterExt, // for `Router::typed_*` -}; use axum_server::tls_rustls::RustlsConfig; use tokio::net::TcpListener; use tracing::level_filters::LevelFilter; @@ -25,6 +23,16 @@ use crate::{ typed_path::{RepositoryTpeNamePath, RepositoryTpePath, TpeNamePath, TpePath}, }; +// TPE_LOCKS is defined, but outside the types[] array. +// This allows us to loop over the types[] when generating "routes" +pub(crate) const TPE_DATA: &str = "data"; +pub(crate) const TPE_KEYS: &str = "keys"; +pub(crate) const TPE_LOCKS: &str = "locks"; +pub(crate) const TPE_SNAPSHOTS: &str = "snapshots"; +pub(crate) const TPE_INDEX: &str = "index"; +pub(crate) const _TPE_CONFIG: &str = "config"; +pub(crate) const TYPES: [&str; 5] = [TPE_DATA, TPE_KEYS, TPE_LOCKS, TPE_SNAPSHOTS, TPE_INDEX]; + pub async fn start_web_server( acl: Acl, auth: Auth, @@ -45,35 +53,52 @@ pub async fn start_web_server( // /:repo/config app = app - .typed_head(has_config) - .typed_post(add_config::) - .typed_get(get_config::) - .typed_delete(delete_config::); + .route("/:repo/config", head(has_config)) + .route("/:repo/config", post(add_config::)) + .route("/:repo/config", get(get_config::)) + .route( + "/:repo/config", + delete(delete_config::), + ); - // /:repo/ - app = app - .typed_post(create_repository::) - .typed_delete(delete_repository::); + // /:tpe --> note: NO trailing slash + // we loop here over explicit types, to prevent the conflict with paths "/:repo/" + for tpe in TYPES.into_iter() { + let path = format!("/{}", &tpe); + app = app.route(path.as_str(), get(list_files::)); + } - // /:tpe - app = app.typed_get(list_files::); + // /:repo/ --> note: trailing slash + app = app + .route("/:repo/", post(create_repository::)) + .route("/:repo/", delete(delete_repository::)); // /:tpe/:name - app = app - .typed_head(file_length::) - .typed_get(get_file::) - .typed_post(add_file::) - .typed_delete(delete_file::); + // we loop here over explicit types, to prevent conflict with paths "/:repo/:tpe" + for tpe in TYPES.into_iter() { + let path = format!("/{}:name", &tpe); + app = app + .route(path.as_str(), head(file_length::)) + .route(path.as_str(), get(get_file::)) + .route(path.as_str(), post(add_file::)) + .route(path.as_str(), delete(delete_file::)); + } // /:repo/:tpe - app = app.typed_get(list_files::); + app = app.route("/:repo/:tpe", get(list_files::)); // /:repo/:tpe/:name app = app - .typed_head(file_length::) - .typed_get(get_file::) - .typed_post(add_file::) - .typed_delete(delete_file::); + .route( + "/:repo/:tpe/:name", + head(file_length::), + ) + .route("/:repo/:tpe/:name", get(get_file::)) + .route("/:repo/:tpe/:name", post(add_file::)) + .route( + "/:repo/:tpe/:name", + delete(delete_file::), + ); // ----------------------------------------------- // Extra logging requested. Handlers will log too diff --git a/tests/fixtures/test_data/rustic_server.toml b/tests/fixtures/test_data/rustic_server.toml index ba9f591..e3525dc 100644 --- a/tests/fixtures/test_data/rustic_server.toml +++ b/tests/fixtures/test_data/rustic_server.toml @@ -2,15 +2,17 @@ host_dns_name = "127.0.0.1" port = 8000 protocol = "http" +# Adapt to your path to the rustic_server git source repository eg. "/home/rvisser/Software/" +common_root_path = "" [repos] -storage_path = "./test_data/test_repos/" +storage_path = "rustic_server/tests/fixtures/test_data/test_repos/" [authorization] -auth_path = "/test_data/test_repo/htaccess" +auth_path = "rustic_server/tests/fixtures/test_data/htaccess" use_auth = true [access_control] -acl_path = "/test_data/test_repo/acl.toml" +acl_path = "rustic_server/tests/fixtures/test_data/acl.toml" private_repo = true append_only = false