Add temporary CSV parsing import command

This commit is contained in:
Matteo Settenvini 2022-07-28 10:28:02 +02:00
parent 4c76734032
commit f201329441
Signed by: matteo
GPG Key ID: 8576CC1AD97D42DF
5 changed files with 224 additions and 246 deletions

217
Cargo.lock generated
View File

@ -2,12 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.58" version = "1.0.58"
@ -43,6 +37,18 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bstr"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"lazy_static",
"memchr",
"regex-automata",
"serde",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.10.0" version = "3.10.0"
@ -87,12 +93,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "chunked_transfer"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]] [[package]]
name = "ci_info" name = "ci_info"
version = "0.10.2" version = "0.10.2"
@ -145,10 +145,12 @@ dependencies = [
"base64", "base64",
"chrono", "chrono",
"clap", "clap",
"csv",
"directories", "directories",
"minicaldav", "ics",
"reqwest", "reqwest",
"rusty-hook", "rusty-hook",
"rustydav",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
@ -173,12 +175,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]] [[package]]
name = "crc32fast" name = "csv"
version = "1.3.2" version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
dependencies = [ dependencies = [
"cfg-if", "bstr",
"csv-core",
"itoa 0.4.8",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
dependencies = [
"memchr",
] ]
[[package]] [[package]]
@ -264,16 +279,6 @@ dependencies = [
"instant", "instant",
] ]
[[package]]
name = "flate2"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -421,7 +426,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
"itoa", "itoa 1.0.2",
] ]
[[package]] [[package]]
@ -462,7 +467,7 @@ dependencies = [
"http-body", "http-body",
"httparse", "httparse",
"httpdate", "httpdate",
"itoa", "itoa 1.0.2",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio", "tokio",
@ -484,6 +489,12 @@ dependencies = [
"tokio-native-tls", "tokio-native-tls",
] ]
[[package]]
name = "ics"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b891481ef6353e3b97118d4650469e379a39e4373a66908c12f99763182826b1"
[[package]] [[package]]
name = "ident_case" name = "ident_case"
version = "1.0.1" version = "1.0.1"
@ -526,6 +537,12 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.2" version = "1.0.2"
@ -600,28 +617,6 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "minicaldav"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb263a7d12c40d5f200dda93b3665b9ae714d4fe64a6467938c92d974a579edb"
dependencies = [
"base64",
"log",
"ureq",
"url",
"xmltree",
]
[[package]]
name = "miniz_oxide"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
dependencies = [
"adler",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.4" version = "0.8.4"
@ -893,6 +888,12 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]] [[package]]
name = "remove_dir_all" name = "remove_dir_all"
version = "0.5.3" version = "0.5.3"
@ -939,33 +940,6 @@ dependencies = [
"winreg", "winreg",
] ]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi",
]
[[package]]
name = "rustls"
version = "0.20.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
dependencies = [
"log",
"ring",
"sct",
"webpki",
]
[[package]] [[package]]
name = "rusty-hook" name = "rusty-hook"
version = "0.11.2" version = "0.11.2"
@ -978,6 +952,15 @@ dependencies = [
"toml", "toml",
] ]
[[package]]
name = "rustydav"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc4c86c47126ac8bfc573084610e93f4ca8726f3ae7bf6c64bd60476731b6e42"
dependencies = [
"reqwest",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.10" version = "1.0.10"
@ -1003,16 +986,6 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "sct"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
"ring",
"untrusted",
]
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.6.1" version = "2.6.1"
@ -1062,7 +1035,7 @@ version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7"
dependencies = [ dependencies = [
"itoa", "itoa 1.0.2",
"ryu", "ryu",
"serde", "serde",
] ]
@ -1074,7 +1047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"itoa", "itoa 1.0.2",
"ryu", "ryu",
"serde", "serde",
] ]
@ -1095,12 +1068,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
@ -1314,30 +1281,6 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9399fa2f927a3d327187cbd201480cee55bee6ac5d3c77dd27f0c6814cff16d5"
dependencies = [
"base64",
"chunked_transfer",
"encoding_rs",
"flate2",
"log",
"once_cell",
"rustls",
"url",
"webpki",
"webpki-roots",
]
[[package]] [[package]]
name = "url" name = "url"
version = "2.2.2" version = "2.2.2"
@ -1479,25 +1422,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "webpki"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf"
dependencies = [
"webpki",
]
[[package]] [[package]]
name = "widestring" name = "widestring"
version = "0.5.1" version = "0.5.1"
@ -1586,18 +1510,3 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "xml-rs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
[[package]]
name = "xmltree"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb"
dependencies = [
"xml-rs",
]

View File

@ -26,6 +26,9 @@ version = "0.13"
version = "0.4" version = "0.4"
features = ["serde"] features = ["serde"]
[dependencies.csv]
version = "1.1"
[dependencies.clap] [dependencies.clap]
version = "3.2" version = "3.2"
features = ["cargo"] features = ["cargo"]
@ -33,13 +36,16 @@ features = ["cargo"]
[dependencies.directories] [dependencies.directories]
version = "4.0" version = "4.0"
[dependencies.minicaldav] [dependencies.ics]
version = "0.2" version = "0.5"
[dependencies.reqwest] [dependencies.reqwest]
version = "0.11" version = "0.11"
features = ["json", "blocking"] features = ["json", "blocking"]
[dependencies.rustydav]
version = "0.1"
[dependencies.serde] [dependencies.serde]
version = "1.0" version = "1.0"
features = ["derive"] features = ["derive"]

50
src/api_client.rs Normal file
View File

@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
use {
crate::config::Config, crate::constants, anyhow::anyhow,
base64::write::EncoderWriter as Base64Encoder, reqwest::Url, std::io::Write,
};
pub struct ApiClient {
pub base_url: Url,
pub client: reqwest::Client,
}
impl ApiClient {
pub fn new(server_name: &str, configuration: &Config) -> anyhow::Result<Self> {
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::<Vec<_>>()
)
})?;
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,
})
}
}

View File

@ -1,51 +1,20 @@
use serde::Deserialize;
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu> // SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
mod api_client;
mod config; mod config;
mod constants; mod constants;
mod recipe;
use { use {
self::config::Config, crate::api_client::ApiClient,
anyhow::anyhow, crate::config::Config,
base64::write::EncoderWriter as Base64Encoder, crate::recipe::Recipe,
chrono::{DateTime, Utc},
clap::{arg, command, ArgMatches, Command}, clap::{arg, command, ArgMatches, Command},
reqwest::Url, reqwest::{StatusCode, Url},
std::io::Write, std::path::PathBuf,
}; };
fn parse_args() -> ArgMatches {
let server_arg = arg!(-s --server <server> "NextCloud server to connect to").required(false);
command!()
.propagate_version(true)
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new("init")
.about("Authenticate against the provided NextCloud server")
.arg(arg!(<server> "NextCloud server to connect to")),
)
.subcommand(
Command::new("import")
.about("Import the given URLs into NextCloud's cookbook")
.arg(server_arg.clone())
.arg(arg!(<url> ... "One or more URLs each pointing to page with a recipe to import in NextCloud")),
)
.subcommand(
Command::new("schedule")
.about("")
.arg(server_arg.clone())
.arg(arg!(-d --days <days> "")
.value_parser(clap::builder::RangedU64ValueParser::<u32>::new().range(1..))
.required(false)
.default_value("7"))
)
.get_matches()
}
#[tokio::main(flavor = "multi_thread")] #[tokio::main(flavor = "multi_thread")]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let args = parse_args(); let args = parse_args();
@ -79,6 +48,7 @@ async fn main() -> anyhow::Result<()> {
.send() .send()
.await?; .await?;
println!("{:#?}", response); // TODO println!("{:#?}", response); // TODO
assert!([StatusCode::OK, StatusCode::CONFLICT].contains(&response.status()));
} }
} }
Some(("schedule", sub_matches)) => { Some(("schedule", sub_matches)) => {
@ -90,21 +60,89 @@ async fn main() -> anyhow::Result<()> {
.await?; .await?;
println!("{:#?}", recipes.json::<Vec<Recipe>>().await?); // TODO println!("{:#?}", recipes.json::<Vec<Recipe>>().await?); // TODO
} }
Some(("schedule-csv", sub_matches)) => {
let csv_file = sub_matches
.get_one::<PathBuf>("csv_file")
.expect("<csv_file> is a mandatory parameter, it cannot be missing");
let calendar_name = sub_matches
.get_one::<String>("calendar_name")
.expect("<calendar_name> is a mandatory parameter, it cannot be missing");
let mut csv = csv::Reader::from_path(csv_file)?;
#[derive(serde::Deserialize)]
struct CsvRecord {
day: chrono::naive::NaiveDate,
lunch: String,
dinner: String,
}
let recipe_urls = csv.deserialize::<CsvRecord>().fold(
std::collections::HashSet::new(),
|mut set, r| {
if let Ok(r) = r {
set.insert(r.lunch);
set.insert(r.dinner);
}
set
},
);
let api_client = get_api_client(&sub_matches, &configuration)?;
for url in recipe_urls {
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
assert!([StatusCode::OK, StatusCode::CONFLICT].contains(&response.status()));
}
}
_ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"),
}; };
Ok(()) Ok(())
} }
#[derive(Deserialize, Debug)] fn parse_args() -> ArgMatches {
#[serde(rename_all = "camelCase")] let server_arg = arg!(-s --server <server> "NextCloud server to connect to").required(false);
struct Recipe {
#[serde(rename = "recipe_id")] command!()
recipe_id: u32, .propagate_version(true)
name: String, .subcommand_required(true)
keywords: String, .arg_required_else_help(true)
date_created: DateTime<Utc>, .subcommand(
date_modified: DateTime<Utc>, Command::new("init")
.about("Authenticate against the provided NextCloud server")
.arg(arg!(<server> "NextCloud server to connect to")),
)
.subcommand(
Command::new("import")
.about("Import the given URLs into NextCloud's cookbook")
.arg(server_arg.clone())
.arg(arg!(<url> ... "One or more URLs each pointing to page with a recipe to import in NextCloud")),
)
.subcommand(
Command::new("schedule")
.about("")
.arg(server_arg.clone())
.arg(arg!(-d --days <days> "")
.value_parser(clap::builder::RangedU64ValueParser::<u32>::new().range(1..))
.required(false)
.default_value("7"))
)
.subcommand(
Command::new("schedule-csv")
.about("TEMPORARY WIP FUNCTION, UNSTABLE")
.arg(server_arg.clone())
.arg(arg!(<calendar_name> ""))
.arg(arg!(<csv_file> "").value_parser(clap::value_parser!(PathBuf)))
)
.get_matches()
} }
fn get_api_client(sub_matches: &ArgMatches, configuration: &Config) -> anyhow::Result<ApiClient> { fn get_api_client(sub_matches: &ArgMatches, configuration: &Config) -> anyhow::Result<ApiClient> {
@ -121,46 +159,3 @@ fn get_api_client(sub_matches: &ArgMatches, configuration: &Config) -> anyhow::R
ApiClient::new(&server_name, &configuration) ApiClient::new(&server_name, &configuration)
} }
struct ApiClient {
pub base_url: Url,
pub client: reqwest::Client,
}
impl ApiClient {
pub fn new(server_name: &str, configuration: &Config) -> anyhow::Result<Self> {
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::<Vec<_>>()
)
})?;
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,
})
}
}

18
src/recipe.rs Normal file
View File

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
use {
chrono::{DateTime, Utc},
serde::Deserialize,
};
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Recipe {
#[serde(rename = "recipe_id")]
recipe_id: u32,
name: String,
keywords: String,
date_created: DateTime<Utc>,
date_modified: DateTime<Utc>,
}