use clap::{Parser, Subcommand}; use std::path::Path; use axum::{extract::Request, response::IntoResponse, routing::any, Extension, Router}; #[derive(Parser)] #[command(name = "markbase", about = "Momentry Display Engine")] pub struct Cli { #[command(subcommand)] pub command: Commands, } #[derive(Subcommand)] pub enum Commands { #[command(flatten)] Framework(FrameworkCommands), #[command(flatten)] App(crate::cli::apps::AppCommands), } #[derive(Subcommand)] pub enum FrameworkCommands { Display { #[arg(short, long, default_value = "11438")] port: u16, #[arg(short, long)] file: Option, }, Render { file: String, #[arg(short, long)] output: Option, }, Config { #[command(subcommand)] action: ConfigCommands, }, Scan { #[arg(short, long)] user: String, #[arg(short, long)] dir: String, #[arg(short, long, default_value = "100")] batch: usize, #[arg(short, long, default_value = "true")] skip_hash: bool, #[arg(short, long, default_value = "4")] threads: usize, }, Hash { #[arg(short, long)] user: String, #[arg(short, long, default_value = "4")] threads: usize, }, BcryptTest { #[arg(short, long, default_value = "demo123")] password: String, #[arg(long)] verify_hash: Option, }, WebDAV { #[command(subcommand)] action: WebDAVCommands, }, Iscsi { #[command(subcommand)] action: IscsiCommands, }, } #[derive(Subcommand)] pub enum WebDAVCommands { Start { #[arg(short, long, default_value = "8002")] port: u16, #[arg(short, long)] user: String, }, } #[derive(Subcommand)] pub enum IscsiCommands { Start { #[arg(short, long)] user: String, #[arg(short, long, default_value = "3260")] port: u16, #[arg(short, long, default_value = "5GB")] lun_size: String, #[arg(short, long)] force: bool, #[arg(long)] device: Option, }, Stop, Status, } #[derive(Subcommand)] pub enum ConfigCommands { Init { #[arg(short, long)] force: bool, }, Show { #[arg(short, long)] section: Option, }, Edit { #[arg(short, long)] key: String, #[arg(short, long)] value: String, }, Validate, } pub async fn handle_framework_command(cmd: FrameworkCommands) -> anyhow::Result<()> { match cmd { FrameworkCommands::Display { port, file } => { crate::server::run(port, file).await?; } FrameworkCommands::Render { file, output } => { let md = std::fs::read_to_string(&file)?; let html = crate::render::md_to_html(&md); if let Some(path) = &output { std::fs::write(path, html)?; } else { println!("{html}"); } } FrameworkCommands::Config { action } => { handle_config_command(action)?; } FrameworkCommands::Scan { user, dir, batch, skip_hash, threads, } => { use crate::scan::ScanOptions; let options = ScanOptions { skip_hash, threads }; crate::scan::scan_directory(&user, &dir, batch, options)?; } FrameworkCommands::Hash { user, threads } => { crate::scan::compute_hashes(&user, threads)?; } FrameworkCommands::BcryptTest { password, verify_hash } => { handle_bcrypt_test(password, verify_hash)?; } FrameworkCommands::WebDAV { action } => { handle_webdav_command(action).await?; } FrameworkCommands::Iscsi { action } => { handle_iscsi_command(action)?; } } Ok(()) } fn handle_config_command(action: ConfigCommands) -> anyhow::Result<()> { match action { ConfigCommands::Init { force } => { let config_path = Path::new("config/markbase.toml"); if config_path.exists() && !force { println!("Configuration file already exists at config/markbase.toml"); println!("Use --force to overwrite"); return Ok(()); } let config = crate::config::MarkBaseConfig::default_config(); config.save(config_path)?; println!("✓ Configuration file created: config/markbase.toml"); println!("Default values:"); println!(" Server port: {}", config.server.port); println!(" PostgreSQL host: {}", config.postgresql.host); println!(" Test users: {}", config.test.users.join(", ")); } ConfigCommands::Show { section } => { let config_path = Path::new("config/markbase.toml"); if !config_path.exists() { println!("Configuration file not found. Run 'markbase config init' first."); return Ok(()); } let config = crate::config::MarkBaseConfig::load(config_path)?; if let Some(s) = section { show_section(&config, &s); } else { println!("{}", toml::to_string_pretty(&config)?); } } ConfigCommands::Edit { key, value } => { let config_path = Path::new("config/markbase.toml"); if !config_path.exists() { println!("Configuration file not found. Run 'markbase config init' first."); return Ok(()); } let mut config = crate::config::MarkBaseConfig::load(config_path)?; match config.get(&key) { Some(old_value) => { config.set(&key, &value)?; config.validate()?; config.save(config_path)?; println!("✓ Updated {}: {} → {}", key, old_value, value); } None => { println!("Invalid config key: {}", key); println!( "Valid keys: server.*, postgresql.*, authentication.*, test.*, logging.*" ); } } } ConfigCommands::Validate => { let config_path = Path::new("config/markbase.toml"); if !config_path.exists() { println!("Configuration file not found. Run 'markbase config init' first."); return Ok(()); } let config = crate::config::MarkBaseConfig::load(config_path)?; match config.validate() { Ok(_) => { println!("✓ Configuration is valid"); } Err(e) => { println!("✗ Configuration validation failed: {}", e); } } } } Ok(()) } fn show_section(config: &crate::config::MarkBaseConfig, section: &str) { match section { "server" => println!("{}", toml::to_string_pretty(&config.server).unwrap()), "postgresql" => println!("{}", toml::to_string_pretty(&config.postgresql).unwrap()), "authentication" => println!("{}", toml::to_string_pretty(&config.authentication).unwrap()), "test" => println!("{}", toml::to_string_pretty(&config.test).unwrap()), "logging" => println!("{}", toml::to_string_pretty(&config.logging).unwrap()), _ => println!("Invalid section: {}. Valid sections: server, postgresql, authentication, test, logging", section), } } fn handle_bcrypt_test(password: String, verify_hash: Option) -> anyhow::Result<()> { use bcrypt::{hash, verify, DEFAULT_COST}; println!("=== bcrypt Hash Test ==="); println!("Password: {}", password); println!(""); let new_hash = hash(&password, DEFAULT_COST)?; println!("Generated hash:"); println!("{}", new_hash); println!(""); if let Some(hash_to_verify) = verify_hash { println!("Verifying hash: {}", hash_to_verify); let valid = verify(&password, &hash_to_verify)?; println!("Valid: {}", valid); println!(""); } let db_hash = "$2b$10$ha5wU.mOi8fHLJCfun860u2cfVopa04jwe/q82IKOwqp5uG70qsH6"; println!("Database hash: {}", db_hash); let valid = verify(&password, db_hash)?; println!("Database hash valid for '{}': {}", password, valid); println!(""); if !valid { println!("❌ Database hash is incorrect!"); println!("Update SQL:"); println!("UPDATE sftpgo_users SET password_hash = '{}' WHERE username IN ('testuser', 'demo', 'warren', 'momentry');", new_hash); println!(""); println!("Execute:"); println!("sqlite3 data/auth.sqlite \"UPDATE sftpgo_users SET password_hash = '{}' WHERE username IN ('testuser', 'demo', 'warren', 'momentry');\"", new_hash); } else { println!("✅ Database hash is correct!"); } Ok(()) } async fn handle_webdav_command(action: WebDAVCommands) -> anyhow::Result<()> { match action { WebDAVCommands::Start { port, user } => { let db_path = std::path::PathBuf::from(crate::FileTree::user_db_path(&user)); if !db_path.exists() { return Err(anyhow::anyhow!( "User database not found: {}", db_path.display() )); } println!("=== MarkBase WebDAV Server ==="); println!("User: {}", user); println!("Port: {}", port); println!("Database: {}", db_path.display()); println!(""); run_webdav_server(port, user, db_path).await?; } } Ok(()) } async fn run_webdav_server( port: u16, user: String, db_path: std::path::PathBuf, ) -> anyhow::Result<()> { use tokio::net::TcpListener; let webdav = markbase_webdav::webdav::MarkBaseWebDAV::new(user, db_path); let dav_handler = webdav.create_handler(); let app = Router::new() .route("/webdav", any(handle_dav)) .route("/webdav/", any(handle_dav)) .route("/webdav/*path", any(handle_dav)) .layer(Extension(dav_handler)); let addr = format!("127.0.0.1:{}", port); let listener = TcpListener::bind(&addr).await?; println!("WebDAV server listening on http://{}", addr); println!("Mount point: /webdav"); println!(""); println!("Press Ctrl+C to stop"); axum::serve(listener, app).await?; Ok(()) } async fn handle_dav( Extension(dav): Extension, req: Request, ) -> impl IntoResponse { dav.handle(req).await } fn handle_iscsi_command(action: IscsiCommands) -> anyhow::Result<()> { let binary = find_binary("markbase-iscsi"); let mut cmd = std::process::Command::new(&binary); cmd.arg("iscsi"); match action { IscsiCommands::Start { user, port, lun_size, force, device, } => { cmd.arg("start") .args(["--user", &user]) .args(["--port", &port.to_string()]) .args(["--lun-size", &lun_size]); if force { cmd.arg("--force"); } if let Some(d) = device { cmd.args(["--device", &d]); } } IscsiCommands::Stop => { cmd.arg("stop"); } IscsiCommands::Status => { cmd.arg("status"); } } let status = cmd.status()?; std::process::exit(status.code().unwrap_or(1)); } fn find_binary(name: &str) -> std::path::PathBuf { let exe = std::env::current_exe().unwrap(); let dir = exe.parent().unwrap(); dir.join(name) }