feat: package cli function

- exports all personal data into csv file
 - exports all portfolios into specified directory
 - exports PostgreSQL database if specified
This commit is contained in:
Sebastian Pravda 2022-11-23 18:00:59 +01:00
parent be61af2b05
commit 13d66c719f
No known key found for this signature in database
GPG key ID: F3BC84F08EFA3F57
3 changed files with 192 additions and 38 deletions

View file

@ -14,6 +14,7 @@ portfolio-core = { path = "../core" }
[dependencies.tokio]
version = "^1.21"
features = [
"process",
"macros",
]

View file

@ -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<String, Box<dyn std::error::Error>> {
Ok(match (sub_matches.get_one::<String>("key"), sub_matches.get_one::<String>("password")) {
(Some(key), _) => {
key.to_string()
},
(_, Some(password)) => {
let admin_id = if let Some(s) = sub_matches.get_one::<String>("admin_id") {
s.parse::<i32>().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<DbConn, Box<dyn std::error::Error>> {
let db_url = sub_matches.get_one::<Url>("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<dyn std::error::Error>> {
@ -53,19 +88,95 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
)
.subcommand(
Command::new("portfolio")
.about("Database & Portfolio operations")
.about("Portfolio file operations")
.arg(
arg!(
-p --portfolio <PATH> "Path to the portfolio root"
-f --file <PATH> "Age file path"
)
.required(true)
.value_parser(value_parser!(PathBuf)),
)
.arg(
arg!(
-o --output <PATH> "Output file path"
)
.required(true)
.value_parser(value_parser!(PathBuf)),
)
.arg(
arg!(
-d --database <URL> "URL to the database or sql file with postgres:// or sqlite://"
)
.alias("url")
.required(true)
.value_parser(value_parser!(Url)),
)
.arg(
arg!(
-k --key <KEY> "AGE private key for decryption"
)
.required(true),
.required(false),
)
.arg(
arg!(
-p --password <PASSWORD> "Password for decryption"
)
.required(false),
)
.arg(
arg!(
-a --admin_id <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 <PATH> "Portfolio root directory"
)
.required(true)
.value_parser(value_parser!(PathBuf)),
)
.arg(
arg!(
-o --output <PATH> "Output file path"
)
.required(true)
.value_parser(value_parser!(PathBuf)),
)
.arg(
arg!(
-d --database <URL> "URL to the database or sql file with postgres:// or sqlite://"
)
.alias("url")
.required(true)
.value_parser(value_parser!(Url)),
)
.arg(
arg!(
-k --key <KEY> "AGE private key for decryption"
)
.required(false),
)
.arg(
arg!(
-p --password <PASSWORD> "Password for decryption"
)
.required(false),
)
.arg(
arg!(
-a --admin_id <ADMIN_ID> "Admin ID"
)
.required(false),
)
)
.subcommand(
@ -128,43 +239,63 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
match clap.subcommand() {
Some(("export", sub_matches)) => {
let db_url = sub_matches.get_one::<Url>("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::<String>("key"), sub_matches.get_one::<String>("password")) {
(Some(key), _) => {
key.to_string()
},
(_, Some(password)) => {
let admin_id = if let Some(s) = sub_matches.get_one::<String>("admin_id") {
s.parse::<i32>().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::<PathBuf>("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::<PathBuf>("file").unwrap();
let decrypted = crypto::decrypt_file_with_private_key_as_buffer(age_file_path, &key).await?;
let output = sub_matches.get_one::<PathBuf>("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::<Url>("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::<PathBuf>("root_dir").unwrap();
let output = sub_matches.get_one::<PathBuf>("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<i32> = 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::<bool>("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::<String>("input").unwrap();

View file

@ -40,6 +40,17 @@ pub struct CandidateWithParent { // TODO: use this instead of (Candidate, Parent
pub parent_email: Option<String>,
}
#[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<Vec<ApplicationId>, DbErr> {
Candidate::find()
.order_by(candidate::Column::Application, Order::Asc)
.column(candidate::Column::Application)
.into_model::<ApplicationId>()
.all(db)
.await
}
}