diff --git a/Cargo.lock b/Cargo.lock
index d1031f3..a5cddcd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -464,6 +464,7 @@ dependencies = [
"bichon-core",
"console",
"dialoguer",
+ "indicatif",
"tokio",
]
@@ -5022,6 +5023,8 @@ dependencies = [
"fastdivide",
"fnv",
"fs4",
+ "futures-channel",
+ "futures-util",
"htmlescape",
"itertools",
"levenshtein_automata",
@@ -5045,6 +5048,7 @@ dependencies = [
"tantivy-common",
"tantivy-fst",
"tantivy-query-grammar",
+ "tantivy-sstable",
"tantivy-stacker",
"tantivy-tokenizer-api",
"tempfile",
diff --git a/Cargo.toml b/Cargo.toml
index 44d763f..4c458e0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -84,7 +84,7 @@ uuid = { version = "1.23.1", features = ["v4", "serde"] }
fjall = { version = "3.1.4", features = ["lz4", "metrics", "bytes_1"] }
tracing-log = "0.2.0"
tokio-util = "0.7.18"
-
+indicatif = "0.18.4"
[profile.release]
strip = true
diff --git a/crates/admin/Cargo.toml b/crates/admin/Cargo.toml
index f65744d..b1c91db 100644
--- a/crates/admin/Cargo.toml
+++ b/crates/admin/Cargo.toml
@@ -8,4 +8,5 @@ edition.workspace = true
bichon-core = { path = "../core" }
tokio.workspace = true
dialoguer.workspace = true
-console.workspace = true
\ No newline at end of file
+console.workspace = true
+indicatif.workspace = true
diff --git a/crates/admin/src/main.rs b/crates/admin/src/main.rs
index f94146b..2e15120 100644
--- a/crates/admin/src/main.rs
+++ b/crates/admin/src/main.rs
@@ -16,19 +16,13 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-use std::{
- fs,
- path::{Path, PathBuf},
-};
+use console::style;
+use dialoguer::{theme::ColorfulTheme, Select};
-use bichon_core::{
- admin::meta::{find_admin, init_meta_database, update_admin_password},
- error::BichonError,
- utils::encrypt::internal_decrypt_string,
-};
-use console::{style, Emoji};
-use dialoguer::Confirm;
-use dialoguer::{theme::ColorfulTheme, Input, Password, Select};
+use crate::{migrate::handle_migration, reset::handle_reset_password};
+
+pub mod migrate;
+pub mod reset;
#[tokio::main]
async fn main() {
@@ -38,7 +32,12 @@ async fn main() {
style("BICHON ADMINISTRATIVE TOOL").bold().bright().cyan()
);
- let main_options = vec!["Reset Admin Password", "Exit"];
+ let main_options = vec![
+ "Reset Admin Password",
+ "Migrate Legacy v0.x Storage to v1.0",
+ "Exit",
+ ];
+
let selection = Select::with_theme(&theme)
.with_prompt("Select an operation")
.default(0)
@@ -46,272 +45,11 @@ async fn main() {
.interact()
.unwrap();
- if selection == 1 {
- println!("{}", style("Exiting...").dim());
- return;
- }
-
- let root_dir_str: String = Input::with_theme(&theme)
- .with_prompt("Enter the absolute path for 'bichon_root_dir'")
- .validate_with(|input: &String| -> Result<(), &str> {
- let path = Path::new(input);
- if !path.is_absolute() {
- return Err("Path must be absolute.");
- }
- if !path.exists() {
- return Err("Directory does not exist.");
- }
- let has_metadata = path.join("meta.db").exists();
- if !has_metadata {
- return Err("Invalid directory: 'meta.db' not found.");
- }
- Ok(())
- })
- .interact_text()
- .unwrap();
-
- let root_path = PathBuf::from(&root_dir_str);
-
- let database = match init_meta_database(&root_path.join("meta.db")) {
- Ok(database) => database,
- Err(e) => match e {
- BichonError::Generic {
- message,
- location,
- code,
- } => {
- if message.contains("RedbDatabaseError(DatabaseAlreadyOpen") {
- println!("\n{}", style("ERROR: Database is locked.").red().bold());
- println!(
- "{}",
- style("The Bichon service is likely still running.").yellow()
- );
- println!(
- "Since the database cannot be shared between multiple instances, \n\
- you must {} the Bichon service before proceeding.",
- style("STOP").underlined().bold()
- );
- std::process::exit(1);
- } else {
- eprintln!(
- "\n{} (Code: {:#?})\nLocation: {}\nMessage: {}",
- style("A database error occurred:").red().bold(),
- code,
- location,
- message
- );
- std::process::exit(1);
- }
- }
- },
- };
-
- let admin = find_admin(&database);
-
- match admin {
- Ok(Some(user)) => {
- println!("\n{}", style("Admin user found:").green().bold());
- println!("----------------------------------------");
- println!("{:<12} : {}", "Username", style(&user.username).cyan());
- println!("{:<12} : {}", "Email", style(&user.email).cyan());
- }
- Ok(None) => {
- println!(
- "\n{}",
- style("ERROR: No admin user found in the database.")
- .red()
- .bold()
- );
- println!("Please ensure the system has been initialized correctly.");
- std::process::exit(1);
- }
- Err(e) => {
- eprintln!(
- "\n{} Failed to query admin user.",
- style("ERROR:").red().bold()
- );
- eprintln!("Details: {:?}", e);
- std::process::exit(1);
- }
- }
-
- let encryption_key = loop {
- let auth_methods = vec![
- "Enter encryption password manually",
- "Read from password file",
- ];
- let method = Select::with_theme(&theme)
- .with_prompt("How would you like to provide the database encryption key?")
- .items(&auth_methods)
- .interact()
- .unwrap();
-
- let raw_key = if method == 0 {
- Password::with_theme(&theme)
- .with_prompt("Enter Encryption Password")
- .interact()
- .unwrap()
- } else {
- let file_path: String = Input::with_theme(&theme)
- .with_prompt("Enter path to encryption password file")
- .interact_text()
- .unwrap();
-
- match fs::read_to_string(&file_path) {
- Ok(content) => content.trim().to_string(),
- Err(e) => {
- println!("{}: {}", style("Failed to read file").red(), e);
- continue;
- }
- }
- };
-
- if raw_key.is_empty() {
- println!("{}", style("Key cannot be empty.").red());
- continue;
- }
-
- let prompt_message = format!(
- "Encryption key loaded: [ {} ]\n\n \
- {}: This key must match the database encryption key used by the server.\n \
- It corresponds to these settings in your service:\n \
- - Arguments: {} or {}\n \
- - Envs: {} or {}\n\n \
- Do you want to continue?",
- style(&raw_key).cyan().bold(),
- style("IMPORTANT").yellow().bold(),
- style("--bichon_encrypt_password").italic(),
- style("--bichon_encrypt_password_file").italic(),
- style("BICHON_ENCRYPT_PASSWORD").green(),
- style("BICHON_ENCRYPT_PASSWORD_FILE").green()
- );
-
- if Confirm::with_theme(&theme)
- .with_prompt(prompt_message)
- .default(true)
- .interact()
- .unwrap()
- {
- break raw_key;
- }
- };
-
- let admin = find_admin(&database);
-
- match admin {
- Ok(Some(user)) => {
- println!("----------------------------------------");
- println!("{:<12} : {}", "Username", style(&user.username).cyan());
- println!("{:<12} : {}", "Email", style(&user.email).cyan());
-
- let pwd_display = match &user.password {
- Some(p) => {
- let password = match internal_decrypt_string(&encryption_key, p) {
- Ok(p) => p,
- Err(e) => {
- println!("\n{}", style("ERROR: Decryption Failed").red().bold());
- println!(
- "{}",
- style("The provided encryption key is incorrect or invalid for this database.").yellow()
- );
- println!(
- "{} Please verify your {} or the {} you provided.",
- style("➔").cyan(),
- style("encryption password").bold(),
- style("key file").bold()
- );
- eprintln!("\nTechnical details: {:?}", e);
- std::process::exit(1);
- }
- };
- style(password).yellow().to_string()
- }
- None => style("None (No password set)").dim().italic().to_string(),
- };
-
- println!("{:<12} : {}", "Password", pwd_display);
- println!("----------------------------------------");
-
- if !dialoguer::Confirm::with_theme(&theme)
- .with_prompt(format!(
- "Do you want to reset the password for '{}'?",
- user.username
- ))
- .interact()
- .unwrap()
- {
- println!("Operation cancelled.");
- return;
- }
- }
- Ok(None) => {
- println!(
- "\n{}",
- style("ERROR: No admin user found in the database.")
- .red()
- .bold()
- );
- println!("Please ensure the system has been initialized correctly.");
- std::process::exit(1);
- }
- Err(e) => {
- eprintln!(
- "\n{} Failed to query admin user.",
- style("ERROR:").red().bold()
- );
- eprintln!("Details: {:?}", e);
- std::process::exit(1);
- }
- }
-
- println!(
- "\n{}",
- style("TARGET: Reset password for user 'admin'")
- .yellow()
- .bold()
- );
-
- let new_login_password = Password::with_theme(&theme)
- .with_prompt("Enter new Admin Login Password")
- .with_confirmation("Repeat password to confirm", "Passwords do not match!")
- .interact()
- .unwrap();
-
- if !Confirm::with_theme(&theme)
- .with_prompt("Proceed with database update?")
- .interact()
- .unwrap()
- {
- return;
- }
-
- println!("\n{} {}", style("⌛").yellow(), "Updating database...");
-
- match update_admin_password(&database, new_login_password, &encryption_key) {
- Ok(_) => {
- println!(
- "\n{} {}",
- Emoji("✨", "*"),
- style("Success! Admin password has been updated.")
- .green()
- .bold()
- );
- println!(
- "{}",
- style("You can now log in with the new password.").dim()
- );
- }
- Err(e) => {
- println!(
- "\n{}",
- style("ERROR: Failed to update database").red().bold()
- );
- eprintln!(
- "{} Could not save the new password to the database.",
- style("➔").cyan()
- );
- eprintln!("\nDetails: {:?}", e);
- std::process::exit(1);
+ match selection {
+ 0 => handle_reset_password(&theme),
+ 1 => handle_migration(&theme),
+ _ => {
+ println!("{}", style("Exiting...").dim());
}
}
}
diff --git a/crates/admin/src/migrate.rs b/crates/admin/src/migrate.rs
new file mode 100644
index 0000000..5e9da7c
--- /dev/null
+++ b/crates/admin/src/migrate.rs
@@ -0,0 +1,284 @@
+use std::path::{Path, PathBuf};
+
+use bichon_core::migrate::{
+ do_migrate, is_tantivy_index_dir,
+ store::{LegacyDirs, NewDirs},
+};
+use console::style;
+use dialoguer::{theme::ColorfulTheme, Confirm, Input};
+use indicatif::{ProgressBar, ProgressStyle};
+
+pub fn handle_migration(theme: &ColorfulTheme) {
+ println!(
+ "\n{}",
+ style("MIGRATION: Bichon v0.x Storage Architecture → v1.0")
+ .bold()
+ .yellow()
+ );
+
+ println!(
+ "{}",
+ style(
+ "This tool migrates data from the legacy v0.x Tantivy-based storage \
+ architecture (used in versions 0.0.1 through 0.3.7) to the new v1.0 \
+ separated index and Fjall-backed storage format."
+ )
+ .dim()
+ );
+
+ println!(
+ "{}",
+ style(
+ "Legacy v0.x architecture:\n\
+ • envelope metadata stored in Tantivy\n\
+ • message data stored in Tantivy\n\n\
+ New v1.0 architecture:\n\
+ • mail indexes stored in Tantivy\n\
+ • attachment indexes stored in Tantivy\n\
+ • raw message data stored in Fjall\n\
+ • attachment blobs stored in Fjall"
+ )
+ .dim()
+ );
+
+ println!(
+ "\n{} {}",
+ style("IMPORTANT:").yellow().bold(),
+ style(
+ "The paths below must exactly match what your old bichon server was configured with."
+ )
+ .yellow()
+ );
+
+ // --- bichon-root-dir ---
+ let root_dir_str: String = Input::with_theme(theme)
+ .with_prompt("Enter --bichon-root-dir (same value used by the old server)")
+ .validate_with(|input: &String| -> Result<(), &str> {
+ let path = Path::new(input);
+ if !path.is_absolute() {
+ return Err("Path must be absolute.");
+ }
+ if !path.exists() {
+ return Err("Directory does not exist.");
+ }
+ Ok(())
+ })
+ .interact_text()
+ .unwrap();
+
+ let root_path = PathBuf::from(&root_dir_str);
+
+ // --- bichon-index-dir ---
+ let default_index = root_path.join("envelope");
+ let default_new_index = root_path.join("bichon-indices");
+ let index_dir_str: String = Input::with_theme(theme)
+ .with_prompt(format!(
+ "Enter --bichon-index-dir (leave blank to use default: {})",
+ style(default_index.display()).cyan()
+ ))
+ .allow_empty(true)
+ .validate_with(|input: &String| -> Result<(), &str> {
+ if input.is_empty() {
+ return Ok(());
+ }
+ let path = Path::new(input);
+ if !path.is_absolute() {
+ return Err("Path must be absolute.");
+ }
+
+ if !path.exists() {
+ return Err("Directory does not exist.");
+ }
+ Ok(())
+ })
+ .interact_text()
+ .unwrap();
+
+ let index_path = if index_dir_str.is_empty() {
+ default_index
+ } else {
+ PathBuf::from(&index_dir_str)
+ };
+
+ let new_index_path = if index_dir_str.is_empty() {
+ default_new_index
+ } else {
+ PathBuf::from(&index_dir_str).join("bichon-indices")
+ };
+
+ // --- bichon-data-dir ---
+ let default_data = root_path.join("eml");
+ let default_new_data = root_path.join("bichon-storage");
+ let data_dir_str: String = Input::with_theme(theme)
+ .with_prompt(format!(
+ "Enter --bichon-data-dir (leave blank to use default: {})",
+ style(default_data.display()).cyan()
+ ))
+ .allow_empty(true)
+ .validate_with(|input: &String| -> Result<(), &str> {
+ if input.is_empty() {
+ return Ok(());
+ }
+ let path = Path::new(input);
+ if !path.is_absolute() {
+ return Err("Path must be absolute.");
+ }
+ if !path.exists() {
+ return Err("Directory does not exist.");
+ }
+ Ok(())
+ })
+ .interact_text()
+ .unwrap();
+
+ let data_path = if data_dir_str.is_empty() {
+ default_data
+ } else {
+ PathBuf::from(&data_dir_str)
+ };
+
+ let new_data_path = if data_dir_str.is_empty() {
+ default_new_data
+ } else {
+ PathBuf::from(&data_dir_str).join("bichon-storage")
+ };
+
+ println!("\n{}", style("Paths to be migrated:").bold());
+ println!("----------------------------------------");
+ println!(
+ "{:<20} : {}",
+ "bichon-root-dir",
+ style(root_path.display()).cyan()
+ );
+ println!(
+ "{:<20} : {}",
+ "bichon-index-dir",
+ style(index_path.display()).cyan()
+ );
+ println!(
+ "{:<20} : {}",
+ "bichon-data-dir",
+ style(data_path.display()).cyan()
+ );
+ println!("----------------------------------------");
+
+ println!(
+ "\n{} Checking legacy v0.x storage layout...",
+ style("⌛").yellow()
+ );
+
+ match is_legacy_data_layout_with_paths(&index_path, &data_path) {
+ Ok(true) => {
+ println!(
+ "{} {}",
+ style("✔").green(),
+ style("Legacy v0.x Tantivy-based storage detected. Migration to v1.0 is required.")
+ .yellow()
+ );
+ }
+ Ok(false) => {
+ println!(
+ "{} {}",
+ style("✔").green(),
+ style("No legacy v0.x storage layout was detected at the specified paths.").green()
+ );
+
+ println!(
+ "{}",
+ style(
+ "The selected directories may already be using the v1.0 storage architecture."
+ )
+ .dim()
+ );
+
+ return;
+ }
+ Err(e) => {
+ eprintln!(
+ "{} Failed to verify legacy storage layout: {:?}",
+ style("ERROR:").red().bold(),
+ e
+ );
+
+ std::process::exit(1);
+ }
+ }
+
+ println!(
+ "\n{} {}",
+ style("⚠").yellow(),
+ style(
+ "This migration is non-destructive. Existing v0.x storage files will remain unchanged."
+ )
+ .yellow()
+ );
+
+ if !Confirm::with_theme(theme)
+ .with_prompt("Ready to migrate?")
+ .default(true)
+ .interact()
+ .unwrap()
+ {
+ println!("{}", style("Migration cancelled.").dim());
+ return;
+ }
+
+ println!("\n{} Migrating...", style("⌛").yellow());
+ let pb = ProgressBar::new(0);
+ pb.set_style(ProgressStyle::default_bar()
+ .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) {msg}")
+ .unwrap()
+ .progress_chars("#>-"));
+ let legacy = LegacyDirs::new(index_path, data_path);
+ let new_dirs = NewDirs::new(new_index_path, new_data_path);
+ if let Err(e) = do_migrate(legacy, new_dirs, |msg| {
+ if let Some(data) = msg.strip_prefix("PROGRESS:") {
+ let parts: Vec<&str> = data.split(':').collect();
+ if parts.len() == 2 {
+ let migrated = parts[0].parse::().unwrap_or(0);
+ let skipped = parts[1].parse::().unwrap_or(0);
+
+ pb.set_position(migrated + skipped);
+ pb.set_message(format!(
+ "Migrated: {}, {} {}",
+ style(migrated).green(),
+ style(skipped).red(),
+ style("skipped").dim()
+ ));
+ }
+ } else if let Some(total) = msg.strip_prefix("TOTAL:") {
+ pb.set_length(total.parse().unwrap_or(0));
+ } else if msg.starts_with("WARN:") {
+ pb.println(format!("{} {}", style("⚠").yellow(), &msg[5..]));
+ } else if let Some(done_data) = msg.strip_prefix("DONE:") {
+ let parts: Vec<&str> = done_data.split(':').collect();
+ pb.finish_with_message(format!(
+ "Migration finished. Total: {}, Skipped: {}",
+ parts.get(0).unwrap_or(&"0"),
+ parts.get(1).unwrap_or(&"0")
+ ));
+ }
+ }) {
+ eprintln!(
+ "\n{} Migration failed:\n{:?}",
+ style("✘").red().bold(),
+ style(e).red()
+ );
+ return;
+ }
+ println!(
+ "{} {}",
+ style("✔").green(),
+ style("Migration completed successfully!").bold()
+ );
+}
+
+pub fn is_legacy_data_layout_with_paths(
+ envelope_dir: &PathBuf,
+ eml_dir: &PathBuf,
+) -> std::io::Result {
+ let envelope_result = is_tantivy_index_dir(envelope_dir)?;
+ let eml_result = is_tantivy_index_dir(eml_dir)?;
+
+ Ok(envelope_result || eml_result)
+}
diff --git a/crates/admin/src/reset.rs b/crates/admin/src/reset.rs
new file mode 100644
index 0000000..050d5f5
--- /dev/null
+++ b/crates/admin/src/reset.rs
@@ -0,0 +1,274 @@
+use std::path::{Path, PathBuf};
+
+use bichon_core::{
+ admin::meta::{find_admin, init_meta_database, update_admin_password},
+ error::BichonError,
+ utils::encrypt::internal_decrypt_string,
+};
+use console::{style, Emoji};
+use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select};
+
+pub fn handle_reset_password(theme: &ColorfulTheme) {
+ let root_dir_str: String = Input::with_theme(theme)
+ .with_prompt("Enter the absolute path for 'bichon_root_dir'")
+ .validate_with(|input: &String| -> Result<(), &str> {
+ let path = Path::new(input);
+ if !path.is_absolute() {
+ return Err("Path must be absolute.");
+ }
+ if !path.exists() {
+ return Err("Directory does not exist.");
+ }
+ let has_metadata = path.join("meta.db").exists();
+ if !has_metadata {
+ return Err("Invalid directory: 'meta.db' not found.");
+ }
+ Ok(())
+ })
+ .interact_text()
+ .unwrap();
+
+ let root_path = PathBuf::from(&root_dir_str);
+ let database = match init_meta_database(&root_path.join("meta.db")) {
+ Ok(database) => database,
+ Err(e) => match e {
+ BichonError::Generic {
+ message,
+ location,
+ code,
+ } => {
+ if message.contains("RedbDatabaseError(DatabaseAlreadyOpen") {
+ println!("\n{}", style("ERROR: Database is locked.").red().bold());
+ println!(
+ "{}",
+ style("The Bichon service is likely still running.").yellow()
+ );
+ println!(
+ "Since the database cannot be shared between multiple instances, \n\
+ you must {} the Bichon service before proceeding.",
+ style("STOP").underlined().bold()
+ );
+ std::process::exit(1);
+ } else {
+ eprintln!(
+ "\n{} (Code: {:#?})\nLocation: {}\nMessage: {}",
+ style("A database error occurred:").red().bold(),
+ code,
+ location,
+ message
+ );
+ std::process::exit(1);
+ }
+ }
+ },
+ };
+
+ let admin = find_admin(&database);
+
+ match admin {
+ Ok(Some(user)) => {
+ println!("\n{}", style("Admin user found:").green().bold());
+ println!("----------------------------------------");
+ println!("{:<12} : {}", "Username", style(&user.username).cyan());
+ println!("{:<12} : {}", "Email", style(&user.email).cyan());
+ }
+ Ok(None) => {
+ println!(
+ "\n{}",
+ style("ERROR: No admin user found in the database.")
+ .red()
+ .bold()
+ );
+ println!("Please ensure the system has been initialized correctly.");
+ std::process::exit(1);
+ }
+ Err(e) => {
+ eprintln!(
+ "\n{} Failed to query admin user.",
+ style("ERROR:").red().bold()
+ );
+ eprintln!("Details: {:?}", e);
+ std::process::exit(1);
+ }
+ }
+
+ let encryption_key = loop {
+ let auth_methods = vec![
+ "Enter encryption password manually",
+ "Read from password file",
+ ];
+ let method = Select::with_theme(theme)
+ .with_prompt("How would you like to provide the database encryption key?")
+ .items(&auth_methods)
+ .interact()
+ .unwrap();
+
+ let raw_key = if method == 0 {
+ Password::with_theme(theme)
+ .with_prompt("Enter Encryption Password")
+ .interact()
+ .unwrap()
+ } else {
+ let file_path: String = Input::with_theme(theme)
+ .with_prompt("Enter path to encryption password file")
+ .interact_text()
+ .unwrap();
+
+ match std::fs::read_to_string(&file_path) {
+ Ok(content) => content.trim().to_string(),
+ Err(e) => {
+ println!("{}: {}", style("Failed to read file").red(), e);
+ continue;
+ }
+ }
+ };
+
+ if raw_key.is_empty() {
+ println!("{}", style("Key cannot be empty.").red());
+ continue;
+ }
+
+ let prompt_message = format!(
+ "Encryption key loaded: [ {} ]\n\n \
+ {}: This key must match the database encryption key used by the server.\n \
+ It corresponds to these settings in your service:\n \
+ - Arguments: {} or {}\n \
+ - Envs: {} or {}\n\n \
+ Do you want to continue?",
+ style(&raw_key).cyan().bold(),
+ style("IMPORTANT").yellow().bold(),
+ style("--bichon_encrypt_password").italic(),
+ style("--bichon_encrypt_password_file").italic(),
+ style("BICHON_ENCRYPT_PASSWORD").green(),
+ style("BICHON_ENCRYPT_PASSWORD_FILE").green()
+ );
+
+ if Confirm::with_theme(theme)
+ .with_prompt(prompt_message)
+ .default(true)
+ .interact()
+ .unwrap()
+ {
+ break raw_key;
+ }
+ };
+
+ let admin = find_admin(&database);
+
+ match admin {
+ Ok(Some(user)) => {
+ println!("----------------------------------------");
+ println!("{:<12} : {}", "Username", style(&user.username).cyan());
+ println!("{:<12} : {}", "Email", style(&user.email).cyan());
+
+ let pwd_display = match &user.password {
+ Some(p) => {
+ let password = match internal_decrypt_string(&encryption_key, p) {
+ Ok(p) => p,
+ Err(e) => {
+ println!("\n{}", style("ERROR: Decryption Failed").red().bold());
+ println!(
+ "{}",
+ style("The provided encryption key is incorrect or invalid for this database.").yellow()
+ );
+ println!(
+ "{} Please verify your {} or the {} you provided.",
+ style("➔").cyan(),
+ style("encryption password").bold(),
+ style("key file").bold()
+ );
+ eprintln!("\nTechnical details: {:?}", e);
+ std::process::exit(1);
+ }
+ };
+ style(password).yellow().to_string()
+ }
+ None => style("None (No password set)").dim().italic().to_string(),
+ };
+
+ println!("{:<12} : {}", "Password", pwd_display);
+ println!("----------------------------------------");
+
+ if !dialoguer::Confirm::with_theme(theme)
+ .with_prompt(format!(
+ "Do you want to reset the password for '{}'?",
+ user.username
+ ))
+ .interact()
+ .unwrap()
+ {
+ println!("Operation cancelled.");
+ return;
+ }
+ }
+ Ok(None) => {
+ println!(
+ "\n{}",
+ style("ERROR: No admin user found in the database.")
+ .red()
+ .bold()
+ );
+ println!("Please ensure the system has been initialized correctly.");
+ std::process::exit(1);
+ }
+ Err(e) => {
+ eprintln!(
+ "\n{} Failed to query admin user.",
+ style("ERROR:").red().bold()
+ );
+ eprintln!("Details: {:?}", e);
+ std::process::exit(1);
+ }
+ }
+
+ println!(
+ "\n{}",
+ style("TARGET: Reset password for user 'admin'")
+ .yellow()
+ .bold()
+ );
+
+ let new_login_password = Password::with_theme(theme)
+ .with_prompt("Enter new Admin Login Password")
+ .with_confirmation("Repeat password to confirm", "Passwords do not match!")
+ .interact()
+ .unwrap();
+
+ if !Confirm::with_theme(theme)
+ .with_prompt("Proceed with database update?")
+ .interact()
+ .unwrap()
+ {
+ return;
+ }
+
+ println!("\n{} {}", style("⌛").yellow(), "Updating database...");
+
+ match update_admin_password(&database, new_login_password, &encryption_key) {
+ Ok(_) => {
+ println!(
+ "\n{} {}",
+ Emoji("✨", "*"),
+ style("Success! Admin password has been updated.")
+ .green()
+ .bold()
+ );
+ println!(
+ "{}",
+ style("You can now log in with the new password.").dim()
+ );
+ }
+ Err(e) => {
+ println!(
+ "\n{}",
+ style("ERROR: Failed to update database").red().bold()
+ );
+ eprintln!(
+ "{} Could not save the new password to the database.",
+ style("➔").cyan()
+ );
+ eprintln!("\nDetails: {:?}", e);
+ std::process::exit(1);
+ }
+ }
+}
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
index 92e52de..08de615 100644
--- a/crates/cli/Cargo.toml
+++ b/crates/cli/Cargo.toml
@@ -23,5 +23,5 @@ base64.workspace = true
codepage-strings = "1.0.2"
hex = "0.4.3"
sysinfo.workspace = true
-indicatif = "0.18.4"
+indicatif.workspace = true
serde_json.workspace = true
\ No newline at end of file
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index 65cbcab..220090c 100644
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -48,7 +48,7 @@ async-imap = { git = "https://github.com/rustmailer/async-imap.git", branch = "m
"runtime-tokio",
"compress",
] }
-tantivy = { version = "0.26.1", features = ["zstd-compression"] }
+tantivy = { version = "0.26.1", features = ["zstd-compression", "quickwit"] }
webpki-roots.workspace = true
rustls.workspace = true
rustls-pki-types.workspace = true
diff --git a/crates/core/src/admin/meta.rs b/crates/core/src/admin/meta.rs
index 7ec75e8..10c8d6b 100644
--- a/crates/core/src/admin/meta.rs
+++ b/crates/core/src/admin/meta.rs
@@ -16,21 +16,18 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-
-
use std::{path::Path, rc::Rc};
use native_db::{Builder, Database};
use crate::{
- {
- database::META_MODELS,
- error::{code::ErrorCode, BichonResult},
- token::{AccessTokenModel, AccessTokenModelKey, TokenType},
- users::{UserModel, DEFAULT_ADMIN_USER_ID},
- utils::encrypt::internal_encrypt_string,
- },
+ account::migration::AccountV3,
+ database::META_MODELS,
+ error::{code::ErrorCode, BichonResult},
raise_error,
+ token::{AccessTokenModel, AccessTokenModelKey, TokenType},
+ users::{UserModel, DEFAULT_ADMIN_USER_ID},
+ utils::encrypt::internal_encrypt_string,
};
use itertools::Itertools;
@@ -48,6 +45,21 @@ pub fn init_meta_database(path: impl AsRef) -> BichonResult>) -> BichonResult> {
+ let r_transaction = database
+ .r_transaction()
+ .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
+ let entities: Vec = r_transaction
+ .scan()
+ .primary()
+ .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
+ .all()
+ .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
+ .try_collect()
+ .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
+ Ok(entities)
+}
+
pub fn find_admin(database: &Rc>) -> BichonResult