korrosync 0.4.0

A KOReader Sync Server
Documentation
use std::fs;

use clap::Parser;
use color_eyre::eyre::{self, Context};
use korrosync::cli::{Cli, Commands, DbCommands, UserCommands};
use korrosync::config::Config;
use korrosync::model::User;
use korrosync::service::db::{KorrosyncService, KorrosyncServiceRedb};

#[tokio::main]
async fn main() -> eyre::Result<()> {
    color_eyre::install()?;

    let cli = Cli::parse();

    match cli.command {
        Commands::Serve => {
            let mut cfg = Config::from_env();
            if let Some(db_path) = cli.db_path {
                cfg.db.path = db_path;
            }
            korrosync::run_server(cfg).await
        }
        Commands::User(cmd) => {
            let db_path = resolve_db_path(cli.db_path);
            let service = KorrosyncServiceRedb::new(&db_path).context("Failed to open database")?;

            match cmd {
                UserCommands::Create { username, password } => {
                    let password = resolve_password(password)?;
                    let user = User::new(&username, &password)
                        .map_err(|e| eyre::eyre!("Failed to create user: {}", e))?;
                    service
                        .create_or_update_user(user)
                        .context("Failed to save user")?;
                    println!("User '{}' created successfully", username);
                }
                UserCommands::List => {
                    let users = service.list_users().context("Failed to list users")?;
                    if users.is_empty() {
                        println!("No users found");
                    } else {
                        println!("{:<20} LAST ACTIVITY", "USERNAME");
                        println!("{}", "-".repeat(40));
                        for user in &users {
                            let activity = user
                                .last_activity()
                                .map(|ts| {
                                    chrono::DateTime::from_timestamp_millis(ts)
                                        .map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string())
                                        .unwrap_or_else(|| ts.to_string())
                                })
                                .unwrap_or_else(|| "never".to_string());
                            println!("{:<20} {}", user.username(), activity);
                        }
                        println!("\nTotal: {} user(s)", users.len());
                    }
                }
                UserCommands::Remove { username } => {
                    let deleted = service
                        .delete_user(username.clone())
                        .context("Failed to delete user")?;
                    if deleted {
                        println!("User '{}' removed successfully", username);
                    } else {
                        println!("User '{}' not found", username);
                    }
                }
                UserCommands::ResetPassword { username, password } => {
                    let password = resolve_password(password)?;
                    let existing = service
                        .get_user(username.clone())
                        .context("Failed to query user")?;
                    if existing.is_none() {
                        eyre::bail!("User '{}' not found", username);
                    }
                    let user = User::new(&username, &password)
                        .map_err(|e| eyre::eyre!("Failed to hash password: {}", e))?;
                    service
                        .create_or_update_user(user)
                        .context("Failed to update user")?;
                    println!("Password for user '{}' reset successfully", username);
                }
            }
            Ok(())
        }
        Commands::Db(cmd) => {
            let db_path = resolve_db_path(cli.db_path);

            match cmd {
                DbCommands::Info => {
                    let metadata = fs::metadata(&db_path);
                    println!("Database path: {}", db_path);
                    match metadata {
                        Ok(meta) => {
                            println!("Database size: {} bytes", meta.len());
                        }
                        Err(_) => {
                            println!("Database file does not exist yet");
                        }
                    }
                    if let Ok(service) = KorrosyncServiceRedb::new(&db_path) {
                        let users = service.list_users().unwrap_or_default();
                        println!("Users: {}", users.len());
                    }
                }
                DbCommands::Backup { output } => {
                    fs::copy(&db_path, &output).context("Failed to backup database")?;
                    println!("Database backed up to '{}'", output);
                }
            }
            Ok(())
        }
    }
}

fn resolve_db_path(cli_override: Option<String>) -> String {
    cli_override.unwrap_or_else(|| Config::from_env().db.path)
}

fn resolve_password(password: String) -> eyre::Result<String> {
    if password != "-" {
        return Ok(password);
    }
    let mut buf = String::new();
    std::io::stdin()
        .read_line(&mut buf)
        .context("Failed to read password from stdin")?;
    let password = buf.trim_end_matches('\n').trim_end_matches('\r');
    if password.is_empty() {
        eyre::bail!("Password cannot be empty");
    }
    Ok(password.to_string())
}