From cde6c7fe9f985356642732d4b45542397a409a65 Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Sun, 3 Jul 2022 01:02:12 +0200 Subject: [PATCH] Allow importing of recipes to NextCloud Closes #1 --- Cargo.lock | 1 + Cargo.toml | 3 ++ src/config/credentials.rs | 9 +++-- src/main.rs | 82 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 197a209..c20e145 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,6 +116,7 @@ name = "cooking-schedule" version = "0.0.0" dependencies = [ "anyhow", + "base64", "clap", "directories", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 6dc6fbe..27288c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,9 @@ version = "0.11" [dependencies.anyhow] version = "1.0" +[dependencies.base64] +version = "0.13" + [dependencies.clap] version = "3.2" features = ["cargo"] diff --git a/src/config/credentials.rs b/src/config/credentials.rs index 31d79e3..1185fbe 100644 --- a/src/config/credentials.rs +++ b/src/config/credentials.rs @@ -17,14 +17,14 @@ pub struct Credentials { #[serde(skip)] config_file: PathBuf, - servers: HashMap, + pub servers: HashMap, } #[derive(Deserialize, Serialize)] pub struct Server { - url: String, - login_name: String, - password: String, + pub url: String, + pub login_name: String, + pub password: String, } impl Credentials { @@ -48,6 +48,7 @@ impl Credentials { pub fn add(&mut self, server: Url) -> Result<()> { let http_client = reqwest::blocking::Client::builder() + .https_only(true) .user_agent(USER_AGENT) .build()?; diff --git a/src/main.rs b/src/main.rs index 5aee557..1f81074 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,11 +6,16 @@ mod constants; use { self::config::Config, + anyhow::anyhow, + base64::write::EncoderWriter as Base64Encoder, clap::{arg, command, ArgMatches, Command}, reqwest::Url, + std::io::Write, }; fn parse_args() -> ArgMatches { + let server_arg = arg!(-s --server "NextCloud server to connect to").required(false); + command!() .propagate_version(true) .subcommand_required(true) @@ -20,6 +25,12 @@ fn parse_args() -> ArgMatches { .about("Authenticate against the provided NextCloud server") .arg(arg!( "NextCloud server to connect to")), ) + .subcommand( + Command::new("import") + .about("Import the given URLs into NextCloud's cookbook") + .arg(server_arg) + .arg(arg!( ... "One or more URLs each pointing to page with a recipe to import in NextCloud")), + ) .get_matches() } @@ -41,8 +52,79 @@ async fn main() -> anyhow::Result<()> { Ok(()) })?; } + Some(("import", sub_matches)) => { + let server_name = sub_matches + .get_one::("server") + .unwrap_or_else(|| { + let servers = &configuration.credentials.servers; + match servers.len() { + 0 => panic!("No NextCloud server set up yet, use '{} init' first", env!("CARGO_BIN_NAME")), + 1 => servers.iter().next().unwrap().0, + _ => panic!("More than one NextCloud server set up, use the '--server' option to specify which one to use. Known servers: {:#?}", servers.keys().collect::>()), + } + }); + + let api_client = ApiClient::new(&server_name, &configuration)?; + for url in sub_matches + .get_many::("url") + .expect("At least one url is required") + { + let response = api_client + .client + .post(api_client.base_url.join("apps/cookbook/import")?) + .json(&serde_json::json!({ + "url": url, + })) + .send() + .await?; + println!("{:#?}", response); // TODO + } + } _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), }; Ok(()) } + +struct ApiClient { + pub base_url: Url, + pub client: reqwest::Client, +} + +impl ApiClient { + pub fn new(server_name: &str, configuration: &Config) -> anyhow::Result { + let server = configuration + .credentials + .servers + .get(server_name) + .ok_or_else(|| { + anyhow!( + "Unknown server {}. Did you use '{} init' first? Known servers: {:#?}", + server_name, + env!("CARGO_BIN_NAME"), + configuration.credentials.servers.keys().collect::>() + ) + })?; + + use reqwest::header; + let mut default_headers = header::HeaderMap::new(); + let mut auth_header = b"Basic ".to_vec(); + { + let mut encoder = Base64Encoder::new(&mut auth_header, base64::STANDARD); + write!(encoder, "{}:{}", server.login_name, server.password).unwrap(); + } + let mut auth_header = header::HeaderValue::from_bytes(&auth_header)?; + auth_header.set_sensitive(true); + default_headers.insert(header::AUTHORIZATION, auth_header); + + let client = reqwest::Client::builder() + .user_agent(constants::USER_AGENT) + .default_headers(default_headers) + .build()?; + + Ok(ApiClient { + base_url: Url::parse(&server.url)?, + client: client, + }) + } +}