diff --git a/src/modules/cache/disk/mod.rs b/src/modules/cache/disk/mod.rs index e9dd863..9c05abf 100644 --- a/src/modules/cache/disk/mod.rs +++ b/src/modules/cache/disk/mod.rs @@ -5,8 +5,8 @@ use crate::{ modules::{ database::{ - async_find_impl, delete_impl, list_all_impl, manager::DB_MANAGER, update_impl, - upsert_impl, + async_find_impl, batch_delete_impl, delete_impl, list_all_impl, manager::DB_MANAGER, + update_impl, upsert_impl, }, error::{code::ErrorCode, RustMailerResult}, settings::dir::DATA_DIR_MANAGER, @@ -20,6 +20,7 @@ use serde::{Deserialize, Serialize}; use std::{ path::{Path, PathBuf}, sync::LazyLock, + time::Instant, }; use sysinfo::Disks; use tokio::io::AsyncWriteExt; @@ -60,6 +61,39 @@ impl CacheItem { upsert_impl(DB_MANAGER.meta_db(), self).await } + pub async fn clear() -> RustMailerResult<()> { + const BATCH_SIZE: usize = 200; + let mut total_deleted = 0usize; + let start_time = Instant::now(); + loop { + let deleted = batch_delete_impl(DB_MANAGER.meta_db(), move |rw| { + let to_delete: Vec = rw + .scan() + .primary() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .all() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .filter_map(Result::ok) // filter only Ok values + .take(BATCH_SIZE) + .collect(); + Ok(to_delete) + }) + .await?; + total_deleted += deleted; + // If this batch is empty, break the loop + if deleted == 0 { + break; + } + } + + info!( + "Finished deleting cacheitems total_deleted={} in {:?}", + total_deleted, + start_time.elapsed() + ); + Ok(()) + } + pub async fn check_exist(key: &str) -> RustMailerResult { let item = async_find_impl::(DB_MANAGER.meta_db(), key.to_string()).await?; Ok(item.is_some()) @@ -159,6 +193,23 @@ impl DiskCache { Ok(Some(reader)) } + pub async fn clear(&self) -> RustMailerResult<()> { + CacheItem::clear().await?; + let cache_dir_str = match self.cache_dir.to_str() { + Some(dir) => dir, + None => { + error!("Failed to convert cache_dir to string"); + return Err(raise_error!( + "Failed to convert cache_dir to string".into(), + ErrorCode::InternalError + )); + } + }; + cacache::clear(cache_dir_str) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError)) + } + pub async fn clean_cache_if_needed(&self) { let cache_items = match CacheItem::list().await { Ok(items) => { @@ -211,7 +262,9 @@ impl DiskCache { // Helper function to handle item removal async fn remove_cache_item(cache_dir: &str, item: &CacheItem) -> Result<(), String> { - cacache::remove(cache_dir, &item.key) + cacache::RemoveOpts::new() + .remove_fully(true) + .remove(cache_dir, &item.key) .await .map_err(|e| format!("Failed to remove cache item from disk: {:?}", e))?; item.delete() diff --git a/src/modules/rest/api/system.rs b/src/modules/rest/api/system.rs index 65f90bd..f1f2829 100644 --- a/src/modules/rest/api/system.rs +++ b/src/modules/rest/api/system.rs @@ -2,6 +2,7 @@ // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. +use crate::modules::cache::disk::DISK_CACHE; use crate::modules::common::auth::ClientContext; use crate::modules::error::code::ErrorCode; use crate::modules::overview::Overview; @@ -106,4 +107,19 @@ impl SystemApi { context.require_root()?; Ok(Proxy::update(id.0, url.0).await?) } + + /// Delete all entries in the disk cache. Requires root permission. + /// + /// The disk cache stores temporary files such as email bodies, attachments, + /// and outgoing emails waiting to be sent. This operation will clear all + /// cached data, freeing disk space but removing all temporary content. + #[oai( + path = "/disk-cache", + method = "delete", + operation_id = "clear_disk_cache" + )] + async fn clear_disk_cache(&self, context: ClientContext) -> ApiResult<()> { + context.require_root()?; + Ok(DISK_CACHE.clear().await?) + } }