From 111485e7c42669823b0e11f2d8225f45e07e2405 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 27 Feb 2025 16:53:29 -0700 Subject: [PATCH] feat: Support for loading Servarr API tokens from a file --- README.md | 6 ++--- src/app/app_tests.rs | 10 ++++---- src/app/mod.rs | 44 +++++++++++++++++++++++++++++++----- src/main.rs | 3 ++- src/network/mod.rs | 3 ++- src/network/network_tests.rs | 12 +++++----- 6 files changed, 57 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ddd26ac..37e63a3 100644 --- a/README.md +++ b/README.md @@ -319,13 +319,13 @@ sonarr: uri: http://htpc.local/sonarr # Example of using the 'uri' key instead of 'host' and 'port' api_token: someApiToken1234567890 readarr: - host: 192.168.0.87 + host: 192.168.0.87 port: 8787 - api_token: someApiToken1234567890 + api_token_file: /root/.config/readarr_api_token # Example of loading the API token from a file instead of hardcoding it in the configuration file lidarr: host: 192.168.0.86 port: 8686 - api_token: someApiToken1234567890 + api_token: ${MY_LIDARR_API_TOKEN} # Example of configuring using environment variables whisparr: host: 192.168.0.69 port: 6969 diff --git a/src/app/app_tests.rs b/src/app/app_tests.rs index 6be41e6..d7cb166 100644 --- a/src/app/app_tests.rs +++ b/src/app/app_tests.rs @@ -348,7 +348,7 @@ mod tests { assert_eq!(servarr_config.host, Some("localhost".to_string())); assert_eq!(servarr_config.port, None); assert_eq!(servarr_config.uri, None); - assert!(servarr_config.api_token.is_empty()); + assert_eq!(servarr_config.api_token, Some(String::new())); assert_eq!(servarr_config.ssl_cert_path, None); } @@ -507,14 +507,16 @@ mod tests { let port = 1234; let uri = "http://localhost:1234".to_owned(); let api_token = "thisisatest".to_owned(); + let api_token_file = "/root/.config/api_token".to_owned(); let ssl_cert_path = "/some/path".to_owned(); - let expected_str = format!("ServarrConfig {{ host: Some(\"{}\"), port: Some({}), uri: Some(\"{}\"), api_token: \"***********\", ssl_cert_path: Some(\"{}\") }}", - host, port, uri, ssl_cert_path); + let expected_str = format!("ServarrConfig {{ host: Some(\"{}\"), port: Some({}), uri: Some(\"{}\"), api_token: Some(\"***********\"), api_token_file: Some(\"{}\"), ssl_cert_path: Some(\"{}\") }}", + host, port, uri, api_token_file, ssl_cert_path); let servarr_config = ServarrConfig { host: Some(host), port: Some(port), uri: Some(uri), - api_token, + api_token: Some(api_token), + api_token_file: Some(api_token_file), ssl_cert_path: Some(ssl_cert_path), }; diff --git a/src/app/mod.rs b/src/app/mod.rs index 744b5be..5eeb1dc 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,6 +1,6 @@ -use std::process; - -use anyhow::{anyhow, Error}; +use std::{fs, process}; +use std::path::PathBuf; +use anyhow::{anyhow, Error, Result}; use colored::Colorize; use log::{debug, error}; use regex::Regex; @@ -258,6 +258,28 @@ impl AppConfig { _ => (), } } + + pub fn post_process_initialization(&mut self) { + let fetch_token = |config: &mut ServarrConfig, name: &'static str| { + if let Some(api_token_file) = config.api_token_file.as_ref() { + if !PathBuf::from(api_token_file).exists() { + log_and_print_error(format!("The specified {} API token file", name)); + process::exit(1); + } + + let api_token = fs::read_to_string(api_token_file).map_err(|e| anyhow!(e)).unwrap(); + config.api_token = Some(api_token.trim().to_owned()); + } + }; + + if let Some(radarr_config) = self.radarr.as_mut() { + fetch_token(radarr_config, "Radarr"); + } + + if let Some(sonarr_config) = self.sonarr.as_mut() { + fetch_token(sonarr_config, "Sonarr"); + } + } } #[derive(Redact, Deserialize, Serialize, Clone)] @@ -268,9 +290,11 @@ pub struct ServarrConfig { pub port: Option, #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub uri: Option, - #[serde(default, deserialize_with = "deserialize_env_var")] + #[serde(default, deserialize_with = "deserialize_optional_env_var")] #[redact] - pub api_token: String, + pub api_token: Option, + #[serde(default, deserialize_with = "deserialize_optional_env_var")] + pub api_token_file: Option, #[serde(default, deserialize_with = "deserialize_optional_env_var")] pub ssl_cert_path: Option, } @@ -281,6 +305,13 @@ impl ServarrConfig { log_and_print_error("'host' or 'uri' is required for configuration".to_owned()); process::exit(1); } + + if self.api_token_file.is_none() && self.api_token.is_none() { + log_and_print_error( + "'api_token' or 'api_token_path' is required for configuration".to_owned(), + ); + process::exit(1); + } } } @@ -290,7 +321,8 @@ impl Default for ServarrConfig { host: Some("localhost".to_string()), port: None, uri: None, - api_token: "".to_string(), + api_token: Some(String::new()), + api_token_file: None, ssl_cert_path: None, } } diff --git a/src/main.rs b/src/main.rs index 5bb3a07..aa9238b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -87,7 +87,7 @@ async fn main() -> Result<()> { let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); let args = Cli::parse(); - let config = if let Some(ref config_file) = args.config { + let mut config = if let Some(ref config_file) = args.config { load_config(config_file.to_str().expect("Invalid config file specified"))? } else { confy::load("managarr", "config")? @@ -95,6 +95,7 @@ async fn main() -> Result<()> { let spinner_disabled = args.disable_spinner; debug!("Managarr loaded using config: {config:?}"); config.validate(); + config.post_process_initialization(); let reqwest_client = build_network_client(&config); let (sync_network_tx, sync_network_rx) = mpsc::channel(500); let cancellation_token = CancellationToken::new(); diff --git a/src/network/mod.rs b/src/network/mod.rs index 2571ba7..32cfa4e 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -213,6 +213,7 @@ impl<'a, 'b> Network<'a, 'b> { uri, api_token, ssl_cert_path, + .. }, default_port, ) = match network_event.into() { @@ -252,7 +253,7 @@ impl<'a, 'b> Network<'a, 'b> { uri, method, body, - api_token: api_token.to_owned(), + api_token: api_token.as_ref().expect("API token not found").clone(), ignore_status_code: false, } } diff --git a/src/network/network_tests.rs b/src/network/network_tests.rs index 1613ca0..6c2927c 100644 --- a/src/network/network_tests.rs +++ b/src/network/network_tests.rs @@ -38,7 +38,7 @@ mod tests { app.is_loading = true; let radarr_config = ServarrConfig { host, - api_token: String::new(), + api_token: Some(String::new()), port, ssl_cert_path: None, ..ServarrConfig::default() @@ -472,7 +472,7 @@ mod tests { let servarr_config = ServarrConfig { host: Some("192.168.0.123".to_owned()), port: Some(8080), - api_token: api_token.clone(), + api_token: Some(api_token.clone()), ssl_cert_path: Some("/test/cert.crt".to_owned()), ..ServarrConfig::default() }; @@ -507,7 +507,7 @@ mod tests { let resource = network_event.resource(); let servarr_config = ServarrConfig { uri: Some("https://192.168.0.123:8080".to_owned()), - api_token: api_token.clone(), + api_token: Some(api_token.clone()), ..ServarrConfig::default() }; { @@ -577,7 +577,7 @@ mod tests { let servarr_config = ServarrConfig { host: Some("192.168.0.123".to_owned()), port: Some(8080), - api_token: api_token.clone(), + api_token: Some(api_token.clone()), ssl_cert_path: Some("/test/cert.crt".to_owned()), ..ServarrConfig::default() }; @@ -618,7 +618,7 @@ mod tests { let resource = network_event.resource(); let servarr_config = ServarrConfig { uri: Some("https://192.168.0.123:8080".to_owned()), - api_token: api_token.clone(), + api_token: Some(api_token.clone()), ..ServarrConfig::default() }; { @@ -738,7 +738,7 @@ pub(in crate::network) mod test_utils { let servarr_config = ServarrConfig { host, port, - api_token: "test1234".to_owned(), + api_token: Some("test1234".to_owned()), ..ServarrConfig::default() };