feat: detect legacy tantivy data layout and abort startup with migration hint

This commit is contained in:
rustmailer
2026-05-08 16:28:11 +08:00
parent 174d56e7b4
commit 66b595908c
26 changed files with 189 additions and 29 deletions
+2 -1
View File
@@ -46,6 +46,7 @@ use crate::{
pub struct DashboardStats {
pub account_count: usize, // Number of accounts
pub email_count: u64, // Total number of emails
pub attachment_count: u64, // Total number of attachments
pub total_size_bytes: u64, // Total size of all emails (in bytes)
pub storage_usage_bytes: u64, // Actual storage used (in bytes)
pub index_usage_bytes: u64, // Index storage size (in bytes)
@@ -89,7 +90,7 @@ impl DashboardStats {
};
stat.email_count = ENVELOPE_MANAGER.total_emails(&authorized_ids)?;
stat.attachment_count = ATTACHMENT_MANAGER.total_attachments(&authorized_ids)?;
if has_all_accounts {
stat.storage_usage_bytes = get_total_size(&DATA_DIR_MANAGER.storage_dir)
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
+1
View File
@@ -13,6 +13,7 @@ pub mod import;
pub mod logger;
pub mod mailbox;
pub mod message;
pub mod migrate;
pub mod oauth2;
pub mod settings;
pub mod store;
+62
View File
@@ -0,0 +1,62 @@
use std::path::PathBuf;
use crate::settings::cli::SETTINGS;
pub fn is_tantivy_index_dir(dir: &PathBuf) -> std::io::Result<bool> {
if !dir.exists() || !dir.is_dir() {
return Ok(false);
}
let tantivy_extensions = [".store", ".term", ".idx", ".fieldnorm", ".pos"];
let mut match_count = 0;
let mut has_meta_json = false;
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let file_name = entry.file_name();
let name = file_name.to_string_lossy();
if name == "meta.json" {
has_meta_json = true;
continue;
}
if tantivy_extensions.iter().any(|ext| name.ends_with(ext)) {
match_count += 1;
}
}
Ok(has_meta_json && match_count >= 3)
}
pub fn is_legacy_data_layout() -> std::io::Result<bool> {
let root_dir = PathBuf::from(&SETTINGS.bichon_root_dir);
let envelope_dir = if let Some(ref index_dir) = SETTINGS.bichon_index_dir {
PathBuf::from(index_dir)
} else {
root_dir.join("envelope")
};
let eml_dir = if let Some(ref data_dir) = SETTINGS.bichon_data_dir {
PathBuf::from(data_dir)
} else {
root_dir.join("eml")
};
let envelope_result = is_tantivy_index_dir(&envelope_dir)?;
let eml_result = is_tantivy_index_dir(&eml_dir)?;
Ok(envelope_result || eml_result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_real_tantivy_dir() {
let path = PathBuf::from(r"D:\test-data\envelope");
let result = is_tantivy_index_dir(&path).unwrap();
println!("is tantivy index dir: {}", result);
assert!(result);
}
}
+25 -1
View File
@@ -266,6 +266,30 @@ impl IndexManager {
Box::new(boolean_query)
}
pub fn total_attachments(&self, accounts: &Option<HashSet<u64>>) -> BichonResult<u64> {
let searcher = self.create_searcher()?;
match accounts {
Some(ref ids) if !ids.is_empty() => {
let mut subqueries = Vec::new();
for &id in ids {
let term = Term::from_field_u64(SchemaTools::email_fields().f_account_id, id);
subqueries.push((
Occur::Should,
Box::new(TermQuery::new(term, IndexRecordOption::Basic)) as Box<dyn Query>,
));
}
let query = Box::new(BooleanQuery::new(subqueries)) as Box<dyn Query>;
let count = searcher
.search(&query, &Count)
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
Ok(count as u64)
}
Some(_) => Ok(0),
None => Ok(searcher.num_docs()),
}
}
fn filter_query(
&self,
accounts: Option<HashSet<u64>>,
@@ -358,7 +382,7 @@ impl IndexManager {
if let Some(ref name) = filter.attachment_name {
let query_parser =
QueryParser::for_index(&self.index, vec![f.f_name_text, f.f_name_exact]);
let q = query_parser
.parse_query(name)
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InvalidParameter))?;