mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
feat: use fjall to store detached emails and attachments
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use duckdb::types::Value;
|
||||
use poem_openapi::Object;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::modules::{account::migration::AccountModel, cache::imap::mailbox::MailBox};
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
|
||||
pub struct Envelope {
|
||||
pub id: String,
|
||||
pub message_id: String,
|
||||
pub account_id: u64,
|
||||
pub account_email: Option<String>,
|
||||
pub mailbox_id: u64,
|
||||
pub mailbox_name: Option<String>,
|
||||
pub uid: u32,
|
||||
pub subject: String,
|
||||
pub text: String,
|
||||
pub from: String,
|
||||
pub to: Vec<String>,
|
||||
pub cc: Vec<String>,
|
||||
pub bcc: Vec<String>,
|
||||
pub date: i64,
|
||||
pub internal_date: i64,
|
||||
pub size: u32,
|
||||
pub thread_id: String,
|
||||
pub attachment_count: usize,
|
||||
pub regular_attachment_count: usize,
|
||||
pub tags: Option<Vec<String>>,
|
||||
/// Hash of the content.
|
||||
pub content_hash: String,
|
||||
}
|
||||
|
||||
impl Envelope {
|
||||
pub fn has_any_attachments(&self) -> bool {
|
||||
self.attachment_count > 0
|
||||
}
|
||||
|
||||
pub fn from_row(row: &duckdb::Row) -> duckdb::Result<Self> {
|
||||
let get_list = |col_name: &str| -> Vec<String> {
|
||||
row.get::<_, Value>(col_name)
|
||||
.map(|v| {
|
||||
if let Value::List(inner_list) = v {
|
||||
inner_list
|
||||
.into_iter()
|
||||
.filter_map(|item| {
|
||||
if let Value::Text(s) = item {
|
||||
Some(s)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let account_id = row.get("account_id")?;
|
||||
let mailbox_id = row.get("mailbox_id")?;
|
||||
let email = match AccountModel::get(account_id) {
|
||||
Ok(account) => account.email,
|
||||
Err(_) => "unknown".to_string(),
|
||||
};
|
||||
|
||||
let mailbox_name = MailBox::find_mailbox(account_id, mailbox_id)
|
||||
.ok()
|
||||
.and_then(|m| m)
|
||||
.map(|m| m.name)
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
Ok(Self {
|
||||
id: row.get("id")?,
|
||||
message_id: row.get("message_id").unwrap_or_default(),
|
||||
account_id,
|
||||
account_email: Some(email),
|
||||
mailbox_id,
|
||||
mailbox_name: Some(mailbox_name),
|
||||
uid: row.get::<_, u64>("uid")? as u32,
|
||||
subject: row.get("subject").unwrap_or_default(),
|
||||
text: row.get("body").unwrap_or_default(),
|
||||
from: row.get("sender").unwrap_or_default(),
|
||||
to: get_list("recipients"),
|
||||
cc: get_list("cc"),
|
||||
bcc: get_list("bcc"),
|
||||
date: row.get("sent_at").unwrap_or(0),
|
||||
internal_date: row.get("received_at").unwrap_or(0),
|
||||
size: row.get::<_, u64>("size_bytes")? as u32,
|
||||
thread_id: row.get("thread_id")?,
|
||||
attachment_count: row.get::<_, i32>("attachment_count")? as usize,
|
||||
regular_attachment_count: row.get::<_, i32>("regular_attachment_count")? as usize,
|
||||
tags: {
|
||||
let t = get_list("tags");
|
||||
if t.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(t)
|
||||
}
|
||||
},
|
||||
content_hash: row.get("content_hash")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::LazyLock,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::modules::{
|
||||
duckdb::init::duckdb,
|
||||
message::{
|
||||
attachment::AttachmentMetadata,
|
||||
content::{AttachmentDetail, AttachmentInfo},
|
||||
search::SortBy,
|
||||
tags::{TagCount, TagsRequest},
|
||||
},
|
||||
};
|
||||
use crate::{
|
||||
modules::{
|
||||
blob::envelope::Envelope,
|
||||
common::signal::SIGNAL_MANAGER,
|
||||
dashboard::{DashboardStats, LargestEmail},
|
||||
error::{code::ErrorCode, BichonResult},
|
||||
message::search::SearchFilter,
|
||||
rest::response::DataPage,
|
||||
},
|
||||
raise_error,
|
||||
};
|
||||
|
||||
use tokio::{
|
||||
sync::{mpsc, Mutex},
|
||||
task::{self, JoinHandle},
|
||||
};
|
||||
|
||||
pub static ENVELOPE_INDEX_MANAGER: LazyLock<EnvelopeIndexManager> =
|
||||
LazyLock::new(EnvelopeIndexManager::new);
|
||||
|
||||
pub const ENVELOPE_BATCH_SIZE: usize = 100;
|
||||
|
||||
const MAX_BUFFER_DURATION: Duration = Duration::from_secs(10);
|
||||
|
||||
pub struct EnvelopeIndexManager {
|
||||
sender: mpsc::Sender<(Envelope, Vec<AttachmentInfo>)>,
|
||||
handle: Mutex<Option<JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
impl EnvelopeIndexManager {
|
||||
pub async fn shutdown(&self) {
|
||||
let mut guard = self.handle.lock().await;
|
||||
if let Some(handle) = guard.take() {
|
||||
tracing::info!("Waiting for EnvelopeIndexManager to sync all data...");
|
||||
let _ = handle.await;
|
||||
tracing::info!("EnvelopeIndexManager synchronized and closed.");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
let (sender, mut receiver) = mpsc::channel::<(Envelope, Vec<AttachmentInfo>)>(1000);
|
||||
let handle = task::spawn(async move {
|
||||
let mut buffer: Vec<(Envelope, Vec<AttachmentInfo>)> =
|
||||
Vec::with_capacity(ENVELOPE_BATCH_SIZE);
|
||||
let mut interval = tokio::time::interval(MAX_BUFFER_DURATION);
|
||||
let mut shutdown = SIGNAL_MANAGER.subscribe();
|
||||
loop {
|
||||
tokio::select! {
|
||||
maybe_msg = receiver.recv() => {
|
||||
match maybe_msg {
|
||||
Some(doc) => {
|
||||
buffer.push(doc);
|
||||
if buffer.len() >= ENVELOPE_BATCH_SIZE {
|
||||
ENVELOPE_INDEX_MANAGER.drain_and_commit(&mut buffer).await;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if !buffer.is_empty() {
|
||||
tracing::info!("Channel closed, flushing remaining {} items", buffer.len());
|
||||
ENVELOPE_INDEX_MANAGER.drain_and_commit(&mut buffer).await;
|
||||
}
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
_ = interval.tick() => {
|
||||
if !buffer.is_empty() {
|
||||
ENVELOPE_INDEX_MANAGER.drain_and_commit(&mut buffer).await;
|
||||
}
|
||||
}
|
||||
_ = shutdown.recv() => {
|
||||
ENVELOPE_INDEX_MANAGER.drain_and_commit(&mut buffer).await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Self {
|
||||
sender,
|
||||
handle: Mutex::new(Some(handle)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_document(&self, doc: (Envelope, Vec<AttachmentInfo>)) {
|
||||
let _ = self.sender.send(doc).await;
|
||||
}
|
||||
|
||||
async fn drain_and_commit(&self, buffer: &mut Vec<(Envelope, Vec<AttachmentInfo>)>) {
|
||||
if buffer.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let items: Vec<(Envelope, Vec<AttachmentInfo>)> = buffer.drain(..).collect();
|
||||
|
||||
let result = (|| -> BichonResult<()> {
|
||||
duckdb()?.append_envelopes_with_attachments(&items)?;
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
if let Err(e) = result {
|
||||
tracing::error!("Failed to drain and commit envelopes to DuckDB: {:#?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn total_emails(&self, accounts: Option<HashSet<u64>>) -> BichonResult<u64> {
|
||||
tokio::task::spawn_blocking(move || duckdb()?.total_emails(accounts))
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn delete_account_envelopes(&self, account_id: u64) -> BichonResult<Vec<String>> {
|
||||
let content_hashes = tokio::task::spawn_blocking(move || {
|
||||
duckdb()?.delete_account_envelopes_with_orphans(account_id)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))??;
|
||||
Ok(content_hashes)
|
||||
}
|
||||
|
||||
pub async fn delete_mailbox_envelopes(
|
||||
&self,
|
||||
account_id: u64,
|
||||
mailbox_ids: Vec<u64>,
|
||||
) -> BichonResult<Vec<String>> {
|
||||
if mailbox_ids.is_empty() {
|
||||
tracing::warn!("delete_mailbox_envelopes: mailbox_ids is empty, nothing to delete");
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let content_hashes = tokio::task::spawn_blocking(move || {
|
||||
duckdb()?.delete_mailbox_envelopes_with_orphans(account_id, mailbox_ids)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))??;
|
||||
Ok(content_hashes)
|
||||
}
|
||||
|
||||
pub async fn get_all_tags(
|
||||
&self,
|
||||
accounts: Option<HashSet<u64>>,
|
||||
) -> BichonResult<Vec<TagCount>> {
|
||||
tokio::task::spawn_blocking(move || duckdb()?.get_all_tags(accounts))
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn get_all_contacts(
|
||||
&self,
|
||||
accounts: Option<HashSet<u64>>,
|
||||
) -> BichonResult<HashSet<String>> {
|
||||
tokio::task::spawn_blocking(move || duckdb()?.get_all_contacts(accounts))
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn get_attachment_metadata(
|
||||
&self,
|
||||
accounts: Option<HashSet<u64>>,
|
||||
) -> BichonResult<AttachmentMetadata> {
|
||||
tokio::task::spawn_blocking(move || duckdb()?.get_attachment_metadata(accounts))
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn get_orphan_hashes_in_memory(
|
||||
&self,
|
||||
deletes: HashMap<u64, Vec<String>>,
|
||||
) -> BichonResult<Vec<String>> {
|
||||
tokio::task::spawn_blocking(move || duckdb()?.get_orphan_hashes_in_memory(deletes))
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn delete_envelopes_multi_account(
|
||||
&self,
|
||||
deletes: HashMap<u64, Vec<String>>, // HashMap<account_id, envelope_ids>
|
||||
) -> BichonResult<()> {
|
||||
if deletes.is_empty() {
|
||||
tracing::warn!("delete_envelopes_multi_account: deletes is empty, nothing to delete");
|
||||
return Ok(());
|
||||
}
|
||||
tokio::task::spawn_blocking(move || duckdb()?.delete_envelopes_multi_account(deletes))
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn update_envelope_tags(&self, request: TagsRequest) -> BichonResult<()> {
|
||||
if request.updates.is_empty() {
|
||||
tracing::warn!("update_envelope_tags: request is empty, nothing to update");
|
||||
return Ok(());
|
||||
}
|
||||
tokio::task::spawn_blocking(move || duckdb()?.update_envelope_tags(request))
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn search(
|
||||
&self,
|
||||
accounts: Option<HashSet<u64>>,
|
||||
filter: SearchFilter,
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
desc: bool,
|
||||
sort_by: SortBy,
|
||||
) -> BichonResult<DataPage<Envelope>> {
|
||||
assert!(page > 0, "Page number must be greater than 0");
|
||||
assert!(page_size > 0, "Page size must be greater than 0");
|
||||
tokio::task::spawn_blocking(move || {
|
||||
duckdb()?.search(accounts, filter, page, page_size, desc, sort_by)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn list_mailbox_envelopes(
|
||||
&self,
|
||||
account_id: u64,
|
||||
mailbox_id: u64,
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
desc: bool,
|
||||
) -> BichonResult<DataPage<Envelope>> {
|
||||
assert!(page > 0, "Page number must be greater than 0");
|
||||
assert!(page_size > 0, "Page size must be greater than 0");
|
||||
tokio::task::spawn_blocking(move || {
|
||||
duckdb()?.list_mailbox_envelopes(account_id, mailbox_id, page, page_size, desc)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn list_thread_envelopes(
|
||||
&self,
|
||||
account_id: u64,
|
||||
thread_id: String,
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
desc: bool,
|
||||
) -> BichonResult<DataPage<Envelope>> {
|
||||
assert!(page > 0, "Page number must be greater than 0");
|
||||
assert!(page_size > 0, "Page size must be greater than 0");
|
||||
tokio::task::spawn_blocking(move || {
|
||||
duckdb()?.list_thread_envelopes(account_id, thread_id, page, page_size, desc)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn get_envelope_by_id(
|
||||
&self,
|
||||
account_id: u64,
|
||||
envelope_id: String,
|
||||
) -> BichonResult<Option<Envelope>> {
|
||||
tokio::task::spawn_blocking(move || duckdb()?.get_envelope_by_id(account_id, envelope_id))
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn get_attachments_by_envelope_id(
|
||||
&self,
|
||||
account_id: u64,
|
||||
envelope_id: String,
|
||||
) -> BichonResult<Vec<AttachmentDetail>> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
duckdb()?.get_attachments_by_envelope_id(account_id, envelope_id)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn top_10_largest_emails(
|
||||
&self,
|
||||
accounts: Option<HashSet<u64>>,
|
||||
) -> BichonResult<Vec<LargestEmail>> {
|
||||
tokio::task::spawn_blocking(move || duckdb()?.top_10_largest_emails(accounts))
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn get_max_uid(&self, account_id: u64, mailbox_id: u64) -> BichonResult<Option<u64>> {
|
||||
tokio::task::spawn_blocking(move || duckdb()?.get_max_uid(account_id, mailbox_id))
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn num_messages_in_mailbox(
|
||||
&self,
|
||||
account_id: u64,
|
||||
mailbox_id: u64,
|
||||
) -> BichonResult<u64> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
duckdb()?.num_messages_in_mailbox(account_id, mailbox_id)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn num_messages_in_thread(
|
||||
&self,
|
||||
account_id: u64,
|
||||
thread_id: String,
|
||||
) -> BichonResult<u64> {
|
||||
tokio::task::spawn_blocking(move || duckdb()?.num_messages_in_thread(account_id, thread_id))
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn get_dashboard_stats(
|
||||
&self,
|
||||
accounts: Option<HashSet<u64>>,
|
||||
) -> BichonResult<DashboardStats> {
|
||||
tokio::task::spawn_blocking(move || duckdb()?.get_dashboard_stats(accounts))
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
pub mod envelope;
|
||||
pub mod manager;
|
||||
pub mod storage;
|
||||
@@ -0,0 +1,184 @@
|
||||
use crate::modules::{
|
||||
common::signal::SIGNAL_MANAGER,
|
||||
envelope::extractor::reattach_eml_content,
|
||||
error::{code::ErrorCode, BichonResult},
|
||||
settings::dir::DATA_DIR_MANAGER,
|
||||
};
|
||||
use crate::raise_error;
|
||||
use bytes::Bytes;
|
||||
use fjall::{CompressionType, Database, Keyspace, KeyspaceCreateOptions, KvSeparationOptions};
|
||||
|
||||
use std::{io::Cursor, sync::LazyLock};
|
||||
use tokio::{
|
||||
sync::{mpsc, Mutex},
|
||||
task::{self, JoinHandle},
|
||||
};
|
||||
|
||||
pub static BLOB_MANAGER: LazyLock<BlobManager> = LazyLock::new(BlobManager::new);
|
||||
|
||||
pub struct DetachedEmail {
|
||||
pub email: (String, Bytes),
|
||||
pub attachments: Option<Vec<(String, Bytes)>>,
|
||||
}
|
||||
|
||||
pub struct BlobManager {
|
||||
sender: mpsc::Sender<DetachedEmail>,
|
||||
db: Database,
|
||||
email_keyspace: Keyspace,
|
||||
attachments_keyspace: Keyspace,
|
||||
handle: Mutex<Option<JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
impl BlobManager {
|
||||
pub async fn shutdown(&self) {
|
||||
let mut guard = self.handle.lock().await;
|
||||
if let Some(handle) = guard.take() {
|
||||
let _ = handle.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
let db = Database::builder(&DATA_DIR_MANAGER.eml_dir)
|
||||
.open()
|
||||
.expect("Failed to initialize Fjall database: Check if the directory exists and has write permissions.");
|
||||
let email_keyspace = db
|
||||
.keyspace("email", || {
|
||||
KeyspaceCreateOptions::default()
|
||||
.with_kv_separation(Some(
|
||||
KvSeparationOptions::default()
|
||||
.separation_threshold(0)
|
||||
.compression(CompressionType::Lz4)
|
||||
.file_target_size(128 * 1024 * 1024)
|
||||
.staleness_threshold(0.5)
|
||||
.age_cutoff(0.6),
|
||||
))
|
||||
.max_memtable_size(64 * 1024 * 1024)
|
||||
})
|
||||
.expect("Failed to open 'email' keyspace: The partition metadata might be corrupted or inaccessible.");
|
||||
|
||||
let attachments_keyspace = db
|
||||
.keyspace("attachments", || {
|
||||
KeyspaceCreateOptions::default()
|
||||
.with_kv_separation(Some(
|
||||
KvSeparationOptions::default()
|
||||
.separation_threshold(0)
|
||||
.compression(CompressionType::Lz4)
|
||||
.file_target_size(256 * 1024 * 1024)
|
||||
.staleness_threshold(0.5)
|
||||
.age_cutoff(0.6),
|
||||
))
|
||||
.max_memtable_size(64 * 1024 * 1024)
|
||||
})
|
||||
.expect("Failed to open 'attachments' keyspace: Check disk space for blob storage initialization.");
|
||||
|
||||
let (sender, mut receiver) = mpsc::channel::<DetachedEmail>(100);
|
||||
|
||||
let store = db.clone();
|
||||
let email_keyspace_clone = email_keyspace.clone();
|
||||
let attachments_keyspace_clone = attachments_keyspace.clone();
|
||||
let handler = task::spawn(async move {
|
||||
let mut shutdown = SIGNAL_MANAGER.subscribe();
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = receiver.recv() => {
|
||||
match res {
|
||||
Some(email) => {
|
||||
let mut batch = store.batch();
|
||||
batch.insert(&email_keyspace_clone, email.email.0, email.email.1);
|
||||
if let Some(attachments) = email.attachments {
|
||||
for a in attachments {
|
||||
batch.insert(&attachments_keyspace_clone,a.0, a.1);
|
||||
}
|
||||
}
|
||||
if let Err(e) = batch.commit() {
|
||||
tracing::error!("Fjall Put Error {:?}", e);
|
||||
} else {
|
||||
tracing::info!("Fjall Put Success");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
tracing::info!("BlobManager: All senders dropped, closing storage.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = shutdown.recv() => {
|
||||
receiver.close();
|
||||
let remaining = receiver.len();
|
||||
tracing::info!(
|
||||
"BlobManager: Shutdown signal received. Processing {} remaining tasks...",
|
||||
remaining
|
||||
);
|
||||
|
||||
while let Some(email) = receiver.recv().await {
|
||||
let mut batch = store.batch();
|
||||
batch.insert(&email_keyspace_clone, email.email.0, email.email.1);
|
||||
if let Some(attachments) = email.attachments {
|
||||
for a in attachments {
|
||||
batch.insert(&attachments_keyspace_clone,a.0, a.1);
|
||||
}
|
||||
}
|
||||
if let Err(e) = batch.commit() {
|
||||
tracing::error!("Fjall Put Error {:?}", e);
|
||||
} else {
|
||||
tracing::info!("Fjall Put Success");
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("BlobManager: All remaining tasks processed. Closing Fjall.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
sender,
|
||||
db,
|
||||
email_keyspace,
|
||||
attachments_keyspace,
|
||||
handle: Mutex::new(Some(handler)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn queue(&self, email: DetachedEmail) {
|
||||
let _ = self.sender.send(email).await;
|
||||
}
|
||||
|
||||
pub fn get_email(&self, content_hash: &str) -> BichonResult<Option<Bytes>> {
|
||||
self.email_keyspace
|
||||
.get(content_hash)
|
||||
.map(|user_value| user_value.map(|s| s.into()))
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))
|
||||
}
|
||||
|
||||
pub fn get_attachment(&self, content_hash: &str) -> BichonResult<Option<Bytes>> {
|
||||
self.attachments_keyspace
|
||||
.get(content_hash)
|
||||
.map(|user_value| user_value.map(|s| s.into()))
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))
|
||||
}
|
||||
|
||||
pub fn delete(
|
||||
&self,
|
||||
email_content_hashes: &[String],
|
||||
attachment_content_hashes: &[String],
|
||||
) -> BichonResult<()> {
|
||||
let mut batch = self.db.batch();
|
||||
for hash in email_content_hashes {
|
||||
batch.remove(&self.email_keyspace, hash);
|
||||
}
|
||||
for hash in attachment_content_hashes {
|
||||
batch.remove(&self.attachments_keyspace, hash);
|
||||
}
|
||||
batch
|
||||
.commit()
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_reader(account_id: u64, eid: String) -> BichonResult<Cursor<Bytes>> {
|
||||
let (_, data) = reattach_eml_content(account_id, eid).await?;
|
||||
Ok(Cursor::new(data))
|
||||
}
|
||||
Reference in New Issue
Block a user