mirror of
https://github.com/danbulant/Portfolio
synced 2026-06-15 12:31:23 +00:00
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:
parent
be61af2b05
commit
13d66c719f
3 changed files with 192 additions and 38 deletions
|
|
@ -14,6 +14,7 @@ portfolio-core = { path = "../core" }
|
|||
[dependencies.tokio]
|
||||
version = "^1.21"
|
||||
features = [
|
||||
"process",
|
||||
"macros",
|
||||
]
|
||||
|
||||
|
|
|
|||
207
cli/src/main.rs
207
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<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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue