From b2cd902e987436b81318210fcb725f5864dc762a Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Wed, 23 Nov 2022 11:46:28 +0100 Subject: [PATCH 1/4] feat: cli csv export --- Cargo.lock | 51 +++++++++++-- cli/Cargo.toml | 1 + cli/src/main.rs | 103 ++++++++++++++++++++------- core/src/candidate_details.rs | 32 ++++++++- core/src/database/query/candidate.rs | 47 ++++++++++-- 5 files changed, 197 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index baa4654..6397fad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,6 +354,18 @@ dependencies = [ "generic-array", ] +[[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]] name = "bumpalo" version = "3.11.1" @@ -645,6 +657,28 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +dependencies = [ + "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]] name = "ctr" version = "0.9.2" @@ -1226,7 +1260,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa", + "itoa 1.0.4", ] [[package]] @@ -1267,7 +1301,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa", + "itoa 1.0.4", "pin-project-lite", "socket2", "tokio", @@ -1456,6 +1490,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "itoa" version = "1.0.4" @@ -2000,6 +2040,7 @@ name = "portfolio-cli" version = "0.1.0" dependencies = [ "clap 4.0.23", + "csv", "portfolio-core", "portfolio-entity", "sea-orm", @@ -2706,7 +2747,7 @@ version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" dependencies = [ - "itoa", + "itoa 1.0.4", "ryu", "serde", ] @@ -2862,7 +2903,7 @@ dependencies = [ "hkdf", "hmac", "indexmap", - "itoa", + "itoa 1.0.4", "libc", "libsqlite3-sys", "log", @@ -3060,7 +3101,7 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ - "itoa", + "itoa 1.0.4", "serde", "time-core", "time-macros", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2a327db..7094fb6 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -6,6 +6,7 @@ publish = false [dependencies] url = "^2.3" +csv = "^1.1" clap = { version = "^4.0", features = ["cargo"] } portfolio-entity = { path = "../entity" } diff --git a/cli/src/main.rs b/cli/src/main.rs index 9c69d96..8255e91 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,12 +1,11 @@ use std::path::PathBuf; + +use clap::{arg, ArgAction, command, Command, value_parser}; +use sea_orm::{Database, DatabaseConnection}; use url::Url; -use clap::{arg, command, value_parser, ArgAction, Command}; -use sea_orm::{Database, DatabaseConnection}; - -use ::entity::candidate::Entity as Candidate; -use ::entity::parent::Entity as Parent; -use sea_orm::*; +use portfolio_core::{crypto, Query}; +use portfolio_core::candidate_details::{ApplicationDetails, EncryptedApplicationDetails}; #[tokio::main] async fn main() -> Result<(), Box> { @@ -15,8 +14,15 @@ async fn main() -> Result<(), Box> { .subcommand_required(true) .arg_required_else_help(true) .subcommand( - Command::new("portfolio") - .about("Database & Portfolio operations") + Command::new("export") + .about("Export all candidate data to a CSV file") + .arg( + arg!( + -o --output "Output file path" + ) + .required(true) + .value_parser(value_parser!(PathBuf)), + ) .arg( arg!( -d --database "URL to the database or sql file with postgres:// or sqlite://" @@ -25,6 +31,28 @@ async fn main() -> Result<(), Box> { .required(true) .value_parser(value_parser!(Url)), ) + .arg( + arg!( + -k --key "AGE private key for decryption" + ) + .required(false), + ) + .arg( + arg!( + -p --password "Password for decryption" + ) + .required(false), + ) + .arg( + arg!( + -a --admin_id "Admin ID" + ) + .required(false), + ) + ) + .subcommand( + Command::new("portfolio") + .about("Database & Portfolio operations") .arg( arg!( -p --portfolio "Path to the portfolio root" @@ -98,29 +126,52 @@ async fn main() -> Result<(), Box> { .get_matches(); match clap.subcommand() { - Some(("portfolio", sub_matches)) => { - let sqlite_url = sub_matches.get_one::("database").unwrap(); - - println!("Connecting to {:?}", sqlite_url); - - if sqlite_url.scheme() != "sqlite" && sqlite_url.scheme() != "postgres" { + Some(("export", sub_matches)) => { + let db_url = sub_matches.get_one::("database").unwrap(); + if db_url.scheme() != "sqlite" && db_url.scheme() != "postgres" { return Err("URL scheme postgres:// or sqlite:// required")?; } + let db: DatabaseConnection = Database::connect(db_url.as_str()).await?; - let db: DatabaseConnection = Database::connect(sqlite_url.as_str()).await?; + let key = match (sub_matches.get_one::("key"), sub_matches.get_one::("password")) { + (Some(key), _) => { + key.to_string() + }, + (_, Some(password)) => { + let admin_id = sub_matches.get_one::("id").unwrap().parse::().unwrap(); + let admin = Query::find_admin_by_id(&db, admin_id) + .await + .map_err(|e| format!("Admin {} not found", admin_id))? + .ok_or("Admin not found")?; + crypto::decrypt_password( + admin.private_key, + password.to_string() + ).await? - let entries = Candidate::find() - .join_rev( - JoinType::InnerJoin, - Parent::belongs_to(Candidate) - .from(::entity::parent::Column::Application) - .to(::entity::candidate::Column::Application) - .into(), - ) - .all(&db) - .await?; + }, + _ => { + unreachable!("Either key or password must be provided"); + } + }; + + let output = sub_matches.get_one::("output").unwrap(); + let mut csv = csv::Writer::from_path(output)?; - println!("Found {} entries", entries.len()); + let candidates_with_parents = Query::list_all_candidates_with_parents(&db).await?; + for candidate in candidates_with_parents { + let application = candidate.application; + + if let Ok(enc_details) = EncryptedApplicationDetails::try_from(candidate) { + let details = enc_details.decrypt(key.to_string()).await?; + csv.serialize(details)?; + } else { + println!("Failed to decrypt candidate {} (Candidate data not set)", application); + } + } + csv.flush()?; + }, + Some(("portfolio", sub_matches)) => { + todo!() } Some(("hash", sub_matches)) => { let input = sub_matches.get_one::("input").unwrap(); diff --git a/core/src/candidate_details.rs b/core/src/candidate_details.rs index ea4616a..29c40e6 100644 --- a/core/src/candidate_details.rs +++ b/core/src/candidate_details.rs @@ -1,8 +1,9 @@ use chrono::NaiveDate; -use entity::{candidate, parent}; use serde::{Deserialize, Serialize}; -use crate::{crypto, error::ServiceError}; +use entity::{candidate, parent}; + +use crate::{crypto, database::query::candidate::CandidateWithParent, error::ServiceError}; pub const NAIVE_DATE_FMT: &str = "%Y-%m-%d"; @@ -184,6 +185,33 @@ impl TryFrom<(candidate::Model, parent::Model)> for EncryptedApplicationDetails } } +impl TryFrom for EncryptedApplicationDetails { + type Error = ServiceError; + + fn try_from( + cp: CandidateWithParent, + ) -> Result { + Ok(EncryptedApplicationDetails { + name: EncryptedString::try_from(cp.name)?, + surname: EncryptedString::try_from(cp.surname)?, + birthplace: EncryptedString::try_from(cp.birthplace)?, + birthdate: EncryptedString::try_from(cp.birthdate)?, + address: EncryptedString::try_from(cp.address)?, + telephone: EncryptedString::try_from(cp.telephone)?, + citizenship: EncryptedString::try_from(cp.citizenship)?, + email: EncryptedString::try_from(cp.email)?, + sex: EncryptedString::try_from(cp.sex)?, + study: cp.study.ok_or(ServiceError::CandidateDetailsNotSet)?, + + parent_name: EncryptedString::try_from(cp.parent_name)?, + parent_surname: EncryptedString::try_from(cp.parent_surname)?, + parent_telephone: EncryptedString::try_from(cp.parent_telephone)?, + parent_email: EncryptedString::try_from(cp.parent_email)?, + }) + } +} + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct ApplicationDetails { diff --git a/core/src/database/query/candidate.rs b/core/src/database/query/candidate.rs index 64b8559..fd60071 100644 --- a/core/src/database/query/candidate.rs +++ b/core/src/database/query/candidate.rs @@ -1,9 +1,9 @@ -use crate::{Query}; - -use ::entity::{candidate, candidate::Entity as Candidate, parent}; use sea_orm::*; use serde::Serialize; +use ::entity::{candidate, candidate::Entity as Candidate, parent}; + +use crate::Query; pub const PAGE_SIZE: u64 = 20; @@ -19,6 +19,28 @@ pub struct CandidateParentResult { pub parent_surname: Option, } +#[derive(FromQueryResult, Serialize)] +pub struct CandidateWithParent { // TODO: use this instead of (Candidate, Parent)??? + pub application: i32, + pub name: Option, + pub surname: Option, + pub birth_surname: Option, + pub birthplace: Option, + pub birthdate: Option, + pub address: Option, + pub telephone: Option, + pub citizenship: Option, + pub email: Option, + pub sex: Option, + pub study: Option, + pub personal_identification_number: Option, + + pub parent_name: Option, + pub parent_surname: Option, + pub parent_telephone: Option, + pub parent_email: Option, +} + impl Query { pub async fn find_candidate_by_id( db: &DbConn, @@ -53,14 +75,31 @@ impl Query { query.fetch().await } } + + + pub async fn list_all_candidates_with_parents( + db: &DbConn, + ) -> Result, DbErr> { + Candidate::find() + .order_by(candidate::Column::Application, Order::Asc) + .join(JoinType::InnerJoin, candidate::Relation::Parent.def()) + .column_as(parent::Column::Name, "parent_name") + .column_as(parent::Column::Surname, "parent_surname") + .column_as(parent::Column::Telephone, "parent_telephone") + .column_as(parent::Column::Email, "parent_email") + .into_model::() + .all(db) + .await + } } #[cfg(test)] mod tests { - use entity::candidate; use sea_orm::{ActiveModelTrait, Set}; + use entity::candidate; + use crate::Query; use crate::util::get_memory_sqlite_connection; From be61af2b05b66ce75157217f10a3f5e79324d91b Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Wed, 23 Nov 2022 16:07:55 +0100 Subject: [PATCH 2/4] refactor: export csv in core/ - feat: export candidate's application id --- Cargo.lock | 2 +- cli/Cargo.toml | 1 - cli/src/main.rs | 29 +++++++++++----------------- core/Cargo.toml | 3 +++ core/src/database/query/candidate.rs | 3 +-- core/src/error.rs | 8 +++++++- core/src/lib.rs | 10 ++++++---- 7 files changed, 29 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6397fad..17eda9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2040,7 +2040,6 @@ name = "portfolio-cli" version = "0.1.0" dependencies = [ "clap 4.0.23", - "csv", "portfolio-core", "portfolio-entity", "sea-orm", @@ -2060,6 +2059,7 @@ dependencies = [ "async_zip", "base64", "chrono", + "csv", "dotenv", "futures", "infer", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 7094fb6..2a327db 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -6,7 +6,6 @@ publish = false [dependencies] url = "^2.3" -csv = "^1.1" clap = { version = "^4.0", features = ["cargo"] } portfolio-entity = { path = "../entity" } diff --git a/cli/src/main.rs b/cli/src/main.rs index 8255e91..867a206 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,3 +1,4 @@ +use std::error::Error; use std::path::PathBuf; use clap::{arg, ArgAction, command, Command, value_parser}; @@ -138,10 +139,14 @@ async fn main() -> Result<(), Box> { key.to_string() }, (_, Some(password)) => { - let admin_id = sub_matches.get_one::("id").unwrap().parse::().unwrap(); + let admin_id = if let Some(s) = sub_matches.get_one::("admin_id") { + s.parse::().unwrap() + } else { + return Err("Admin ID required")?; + }; let admin = Query::find_admin_by_id(&db, admin_id) .await - .map_err(|e| format!("Admin {} not found", admin_id))? + .map_err(|e| format!("Admin {} not found: {}", admin_id, e))? .ok_or("Admin not found")?; crypto::decrypt_password( admin.private_key, @@ -150,25 +155,13 @@ async fn main() -> Result<(), Box> { }, _ => { - unreachable!("Either key or password must be provided"); + return Err("Either key or password must be provided")?; } }; - + let output = sub_matches.get_one::("output").unwrap(); - let mut csv = csv::Writer::from_path(output)?; - - let candidates_with_parents = Query::list_all_candidates_with_parents(&db).await?; - for candidate in candidates_with_parents { - let application = candidate.application; - - if let Ok(enc_details) = EncryptedApplicationDetails::try_from(candidate) { - let details = enc_details.decrypt(key.to_string()).await?; - csv.serialize(details)?; - } else { - println!("Failed to decrypt candidate {} (Candidate data not set)", application); - } - } - csv.flush()?; + let csv = portfolio_core::utils::csv::export(&db, key).await?; + tokio::fs::write(output, csv).await?; }, Some(("portfolio", sub_matches)) => { todo!() diff --git a/core/Cargo.toml b/core/Cargo.toml index b2f28bc..84a8895 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -12,6 +12,9 @@ portfolio-entity = { path = "../entity" } # serde serde = { version = "^1.0", features = ["derive"] } +# csv +csv = "1.1" + # error thiserror = "^1.0" diff --git a/core/src/database/query/candidate.rs b/core/src/database/query/candidate.rs index fd60071..ff5427f 100644 --- a/core/src/database/query/candidate.rs +++ b/core/src/database/query/candidate.rs @@ -19,12 +19,11 @@ pub struct CandidateParentResult { pub parent_surname: Option, } -#[derive(FromQueryResult, Serialize)] +#[derive(FromQueryResult, Serialize, Default)] pub struct CandidateWithParent { // TODO: use this instead of (Candidate, Parent)??? pub application: i32, pub name: Option, pub surname: Option, - pub birth_surname: Option, pub birthplace: Option, pub birthdate: Option, pub address: Option, diff --git a/core/src/error.rs b/core/src/error.rs index 8c34271..be230b0 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -59,7 +59,11 @@ pub enum ServiceError { #[error("Portfolio is incomplete")] IncompletePortfolio, #[error("Zip error")] - ZipError(#[from] async_zip::error::ZipError) + ZipError(#[from] async_zip::error::ZipError), + #[error("Csv error")] + CsvError(#[from] csv::Error), + #[error("Csv into inner error")] + CsvIntoInnerError, } impl ServiceError { @@ -94,6 +98,8 @@ impl ServiceError { //TODO: Correct code ServiceError::IncompletePortfolio => 406, ServiceError::ZipError(_) => 500, + ServiceError::CsvError(_) => 500, + ServiceError::CsvIntoInnerError => 500, } } } diff --git a/core/src/lib.rs b/core/src/lib.rs index 579ddcd..dd3e5c8 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,3 +1,8 @@ +pub use sea_orm; + +pub use database::mutation::*; +pub use database::query::*; + pub mod database; pub mod crypto; pub mod filetype; @@ -6,8 +11,5 @@ pub mod error; pub mod candidate_details; pub mod util; pub mod responses; +pub mod utils; -pub use database::mutation::*; -pub use database::query::*; - -pub use sea_orm; From 13d66c719f451ca0954463cba02fa4bf2067265b Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Wed, 23 Nov 2022 18:00:59 +0100 Subject: [PATCH 3/4] feat: package cli function - exports all personal data into csv file - exports all portfolios into specified directory - exports PostgreSQL database if specified --- cli/Cargo.toml | 1 + cli/src/main.rs | 207 ++++++++++++++++++++++----- core/src/database/query/candidate.rs | 22 +++ 3 files changed, 192 insertions(+), 38 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2a327db..8d5a64e 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -14,6 +14,7 @@ portfolio-core = { path = "../core" } [dependencies.tokio] version = "^1.21" features = [ + "process", "macros", ] diff --git a/cli/src/main.rs b/cli/src/main.rs index 867a206..34ae161 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,12 +1,47 @@ -use std::error::Error; use std::path::PathBuf; -use clap::{arg, ArgAction, command, Command, value_parser}; -use sea_orm::{Database, DatabaseConnection}; +use clap::{arg, ArgAction, ArgMatches, command, Command, value_parser}; +use sea_orm::{Database, DatabaseConnection, DbConn}; use url::Url; use portfolio_core::{crypto, Query}; -use portfolio_core::candidate_details::{ApplicationDetails, EncryptedApplicationDetails}; +use portfolio_core::services::portfolio_service::{FileType}; + +async fn get_admin_private_key(db: &DbConn, sub_matches: &ArgMatches) -> Result> { + Ok(match (sub_matches.get_one::("key"), sub_matches.get_one::("password")) { + (Some(key), _) => { + key.to_string() + }, + (_, Some(password)) => { + let admin_id = if let Some(s) = sub_matches.get_one::("admin_id") { + s.parse::().unwrap() + } else { + return Err("Admin ID required")?; + }; + let admin = Query::find_admin_by_id(&db, admin_id) + .await + .map_err(|e| format!("Admin {} not found: {}", admin_id, e))? + .ok_or("Admin not found")?; + crypto::decrypt_password( + admin.private_key, + password.to_string() + ).await? + + }, + _ => { + return Err("Either key or password must be provided")?; + } + }) +} + +async fn get_db_conn(sub_matches: &ArgMatches) -> Result> { + let db_url = sub_matches.get_one::("database").unwrap(); + if db_url.scheme() != "sqlite" && db_url.scheme() != "postgres" { + return Err("URL scheme postgres:// or sqlite:// required")?; + } + let db: DatabaseConnection = Database::connect(db_url.as_str()).await?; + Ok(db) +} #[tokio::main] async fn main() -> Result<(), Box> { @@ -53,19 +88,95 @@ async fn main() -> Result<(), Box> { ) .subcommand( Command::new("portfolio") - .about("Database & Portfolio operations") + .about("Portfolio file operations") .arg( arg!( - -p --portfolio "Path to the portfolio root" + -f --file "Age file path" ) .required(true) .value_parser(value_parser!(PathBuf)), ) + .arg( + arg!( + -o --output "Output file path" + ) + .required(true) + .value_parser(value_parser!(PathBuf)), + ) + .arg( + arg!( + -d --database "URL to the database or sql file with postgres:// or sqlite://" + ) + .alias("url") + .required(true) + .value_parser(value_parser!(Url)), + ) .arg( arg!( -k --key "AGE private key for decryption" ) - .required(true), + .required(false), + ) + .arg( + arg!( + -p --password "Password for decryption" + ) + .required(false), + ) + .arg( + arg!( + -a --admin_id "Admin ID" + ) + .required(false), + ) + ) + .subcommand( + Command::new("package") + .about("Package all data into one zip") + .arg( + arg!( + -s --pg_dump "Backup SQL database with pg_dump (PostgreSQL only)" + ) + ) + .arg( + arg!( + -r --root_dir "Portfolio root directory" + ) + .required(true) + .value_parser(value_parser!(PathBuf)), + ) + .arg( + arg!( + -o --output "Output file path" + ) + .required(true) + .value_parser(value_parser!(PathBuf)), + ) + .arg( + arg!( + -d --database "URL to the database or sql file with postgres:// or sqlite://" + ) + .alias("url") + .required(true) + .value_parser(value_parser!(Url)), + ) + .arg( + arg!( + -k --key "AGE private key for decryption" + ) + .required(false), + ) + .arg( + arg!( + -p --password "Password for decryption" + ) + .required(false), + ) + .arg( + arg!( + -a --admin_id "Admin ID" + ) + .required(false), ) ) .subcommand( @@ -128,43 +239,63 @@ async fn main() -> Result<(), Box> { match clap.subcommand() { Some(("export", sub_matches)) => { - let db_url = sub_matches.get_one::("database").unwrap(); - if db_url.scheme() != "sqlite" && db_url.scheme() != "postgres" { - return Err("URL scheme postgres:// or sqlite:// required")?; - } - let db: DatabaseConnection = Database::connect(db_url.as_str()).await?; - - let key = match (sub_matches.get_one::("key"), sub_matches.get_one::("password")) { - (Some(key), _) => { - key.to_string() - }, - (_, Some(password)) => { - let admin_id = if let Some(s) = sub_matches.get_one::("admin_id") { - s.parse::().unwrap() - } else { - return Err("Admin ID required")?; - }; - let admin = Query::find_admin_by_id(&db, admin_id) - .await - .map_err(|e| format!("Admin {} not found: {}", admin_id, e))? - .ok_or("Admin not found")?; - crypto::decrypt_password( - admin.private_key, - password.to_string() - ).await? - - }, - _ => { - return Err("Either key or password must be provided")?; - } - }; + let db = get_db_conn(sub_matches).await?; + let key = get_admin_private_key(&db, sub_matches).await?; let output = sub_matches.get_one::("output").unwrap(); let csv = portfolio_core::utils::csv::export(&db, key).await?; tokio::fs::write(output, csv).await?; }, Some(("portfolio", sub_matches)) => { - todo!() + let db = get_db_conn(sub_matches).await?; + let key = get_admin_private_key(&db, sub_matches).await?; + + let age_file_path = sub_matches.get_one::("file").unwrap(); + + let decrypted = crypto::decrypt_file_with_private_key_as_buffer(age_file_path, &key).await?; + + let output = sub_matches.get_one::("output").unwrap(); + tokio::fs::write(output, decrypted).await?; + }, + Some(("package", sub_matches)) => { // TODO: compress the output directory into one file??? + let db_url = sub_matches.get_one::("database").unwrap(); + let db = get_db_conn(sub_matches).await?; + let key = get_admin_private_key(&db, sub_matches).await?; + + let portfolio_root_dir = sub_matches.get_one::("root_dir").unwrap(); + let output = sub_matches.get_one::("output").unwrap(); + tokio::fs::create_dir_all(&output).await?; + + let csv = portfolio_core::utils::csv::export(&db, key.to_string()).await?; + tokio::fs::write(output.join("personal_data.csv"), csv).await?; + println!("Exported personal data to personal_data.csv"); + + let ids: Vec = Query::list_all_candidate_ids(&db) + .await? + .iter() + .map(|application_id| application_id.to_i32()) + .collect(); + for id in ids { + let file_path = portfolio_root_dir.join(&id.to_string()).join(FileType::Age.as_str()); + println!("{}", file_path.display()); + let output_path = output.join(&id.to_string()); + if let Ok(portfolio) = crypto::decrypt_file_with_private_key_as_buffer(file_path, &key).await { + tokio::fs::create_dir_all(&output_path).await?; + tokio::fs::write(&output_path.join(FileType::PortfolioZip.as_str()), portfolio).await?; + }; + } + println!("Exported all portfolios"); + + if *sub_matches.get_one::("pg_dump").unwrap_or(&false) { + let file = std::fs::File::create(&output.join("pg_dump.sql"))?; + tokio::process::Command::new("pg_dump") + .args(&[db_url.as_str()]) + .stdout(file) + .spawn() + .expect("failed to start pg_dump"); + } + println!("Exported database"); + } Some(("hash", sub_matches)) => { let input = sub_matches.get_one::("input").unwrap(); diff --git a/core/src/database/query/candidate.rs b/core/src/database/query/candidate.rs index ff5427f..4a696dd 100644 --- a/core/src/database/query/candidate.rs +++ b/core/src/database/query/candidate.rs @@ -40,6 +40,17 @@ pub struct CandidateWithParent { // TODO: use this instead of (Candidate, Parent pub parent_email: Option, } +#[derive(FromQueryResult)] +pub struct ApplicationId { + application: i32, +} + +impl ApplicationId { + pub fn to_i32(&self) -> i32 { + self.application + } +} + impl Query { pub async fn find_candidate_by_id( db: &DbConn, @@ -90,6 +101,17 @@ impl Query { .all(db) .await } + + pub async fn list_all_candidate_ids( + db: &DbConn, + ) -> Result, DbErr> { + Candidate::find() + .order_by(candidate::Column::Application, Order::Asc) + .column(candidate::Column::Application) + .into_model::() + .all(db) + .await + } } From 2b3fc2e274829e18b40713c1f509bfbb3bd9ee1d Mon Sep 17 00:00:00 2001 From: Sebastian Pravda Date: Wed, 23 Nov 2022 18:14:23 +0100 Subject: [PATCH 4/4] fix: add missing files --- core/src/utils/csv.rs | 59 +++++++++++++++++++++++++++++++++++++++++++ core/src/utils/mod.rs | 1 + 2 files changed, 60 insertions(+) create mode 100644 core/src/utils/csv.rs create mode 100644 core/src/utils/mod.rs diff --git a/core/src/utils/csv.rs b/core/src/utils/csv.rs new file mode 100644 index 0000000..3ee2e25 --- /dev/null +++ b/core/src/utils/csv.rs @@ -0,0 +1,59 @@ +use sea_orm::{DbConn}; +use crate::{error::ServiceError, candidate_details::{EncryptedApplicationDetails, ApplicationDetails}, Query, database::query::candidate::CandidateWithParent}; + + +type Row = CandidateWithParent; + +impl From<(i32, ApplicationDetails)> for Row { + fn from((application, d): (i32, ApplicationDetails)) -> Self { + Self { + application, + name: Some(d.name), + surname: Some(d.surname), + birthplace: Some(d.birthplace), + birthdate: Some(d.birthdate.to_string()), + address: Some(d.address), + telephone: Some(d.telephone), + citizenship: Some(d.citizenship), + email: Some(d.email), + sex: Some(d.sex), + study: Some(d.study), + personal_identification_number: None, // TODO + + parent_name: Some(d.parent_name), + parent_surname: Some(d.parent_surname), + parent_telephone: Some(d.parent_telephone), + parent_email: Some(d.parent_email), + } + } +} + +pub async fn export( + db: &DbConn, + private_key: String, +) -> Result, ServiceError> { + let mut wtr = csv::Writer::from_writer(vec![]); + + let candidates_with_parents = Query::list_all_candidates_with_parents(&db).await?; + for candidate in candidates_with_parents { + let application = candidate.application; + + let row: Row = match EncryptedApplicationDetails::try_from(candidate) { + Ok(d) => Row::from( + d + .decrypt(private_key.to_string()) + .await + .map(|d| (application, d))? + ), + + Err(_) => Row { + application, + ..Default::default() + } + }; + wtr.serialize(row)?; + } + wtr + .into_inner() + .map_err(|_| ServiceError::CsvIntoInnerError) +} \ No newline at end of file diff --git a/core/src/utils/mod.rs b/core/src/utils/mod.rs new file mode 100644 index 0000000..a89587e --- /dev/null +++ b/core/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod csv; \ No newline at end of file