use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::types::{
TrashConfig, TrashEmptyResult, TrashEntry, TrashError, TrashFilter, TrashMetadata,
};
use super::{TRASH_CONFIG, TRASH_DIR, TRASH_ITEMS, TRASH_METADATA};
use crate::FsError;
use crate::storage::zpl::{S_IFDIR, S_IFMT, ZPL};
pub fn trash_delete(dataset: &str, path: &str) -> Result<u64, TrashError> {
let config = get_trash_config(dataset);
if !config.enabled {
return permanent_delete(dataset, path).map(|_| 0);
}
let (size, is_directory) = get_file_info(dataset, path)?;
let trash_id = {
let mut metadata = TRASH_METADATA.lock();
let meta = metadata.entry(dataset.to_string()).or_default();
meta.allocate_id()
};
let entry = TrashEntry {
original_path: path.to_string(),
trash_id,
deleted_at: current_timestamp(),
size,
deleted_by: current_uid(),
expires_at: if config.retention_seconds > 0 {
current_timestamp() + config.retention_seconds
} else {
0
},
is_directory,
};
let trash_dir = format!("{}/{}/{}", dataset, TRASH_DIR, TRASH_ITEMS);
let trash_path = format!("{}/{}", trash_dir, trash_id);
create_directories(dataset, &trash_dir)?;
move_file(path, &trash_path)?;
{
let mut metadata = TRASH_METADATA.lock();
if let Some(meta) = metadata.get_mut(dataset) {
meta.add_entry(entry);
}
}
if config.auto_purge {
let _ = maybe_auto_purge(dataset, &config);
}
Ok(trash_id)
}
pub fn trash_restore(
dataset: &str,
trash_id: u64,
target_path: Option<&str>,
) -> Result<(), TrashError> {
let entry = {
let metadata = TRASH_METADATA.lock();
match metadata.get(dataset) {
Some(meta) => meta
.find_entry(trash_id)
.cloned()
.ok_or(TrashError::EntryNotFound(trash_id))?,
None => return Err(TrashError::EntryNotFound(trash_id)),
}
};
let restore_path = target_path.unwrap_or(&entry.original_path);
if path_exists(dataset, restore_path) {
return Err(TrashError::TargetExists(restore_path.to_string()));
}
if let Some(parent) = get_parent_path(restore_path) {
let _ = create_directories(dataset, parent);
}
let trash_path = format!("{}/{}/{}/{}", dataset, TRASH_DIR, TRASH_ITEMS, trash_id);
move_file(&trash_path, restore_path)?;
{
let mut metadata = TRASH_METADATA.lock();
if let Some(meta) = metadata.get_mut(dataset) {
meta.remove_entry(trash_id);
}
}
Ok(())
}
pub fn trash_list(dataset: &str) -> Result<Vec<TrashEntry>, TrashError> {
let metadata = TRASH_METADATA.lock();
match metadata.get(dataset) {
Some(meta) => Ok(meta.entries.clone()),
None => Ok(Vec::new()),
}
}
pub fn trash_empty(dataset: &str, filter: TrashFilter) -> Result<TrashEmptyResult, TrashError> {
let mut result = TrashEmptyResult::default();
let entries_to_delete: Vec<u64> = {
let metadata = TRASH_METADATA.lock();
let meta = match metadata.get(dataset) {
Some(m) => m,
None => return Ok(result),
};
let now = current_timestamp();
meta.entries
.iter()
.filter(|e| match &filter {
TrashFilter::All => true,
TrashFilter::OlderThan(secs) => now.saturating_sub(e.deleted_at) > *secs,
TrashFilter::Expired => e.expires_at > 0 && e.expires_at <= now,
TrashFilter::LargerThan(bytes) => e.size > *bytes,
TrashFilter::ById(id) => e.trash_id == *id,
TrashFilter::ByPattern(pattern) => glob_match(pattern, &e.original_path),
})
.map(|e| e.trash_id)
.collect()
};
for trash_id in entries_to_delete {
match delete_trash_entry(dataset, trash_id) {
Ok(size) => {
result.deleted_count += 1;
result.deleted_bytes += size;
}
Err(_) => {
result.failed_count += 1;
}
}
}
Ok(result)
}
fn delete_trash_entry(dataset: &str, trash_id: u64) -> Result<u64, TrashError> {
let entry = {
let mut metadata = TRASH_METADATA.lock();
let meta = metadata
.get_mut(dataset)
.ok_or(TrashError::EntryNotFound(trash_id))?;
meta.remove_entry(trash_id)
.ok_or(TrashError::EntryNotFound(trash_id))?
};
let trash_path = format!("{}/{}/{}/{}", dataset, TRASH_DIR, TRASH_ITEMS, trash_id);
delete_permanently(&trash_path)?;
Ok(entry.size)
}
pub fn trash_size(dataset: &str) -> Result<u64, TrashError> {
let metadata = TRASH_METADATA.lock();
match metadata.get(dataset) {
Some(meta) => Ok(meta.total_size),
None => Ok(0),
}
}
pub fn permanent_delete(dataset: &str, path: &str) -> Result<(), TrashError> {
let config = get_trash_config(dataset);
if config.secure_delete {
secure_delete_file(dataset, path)?;
}
delete_permanently(path)
}
fn secure_delete_file(dataset: &str, path: &str) -> Result<(), TrashError> {
use crate::crypto::secure_erase::{SecureEraser, WipeMethod};
let (size, is_directory) = get_file_info(dataset, path)?;
if is_directory {
return Ok(());
}
if size > 0 {
let mut wipe_buffer = alloc::vec![0u8; core::cmp::min(size as usize, 1024 * 1024)];
let mut eraser = SecureEraser::new();
let _ = eraser.erase_buffer(&mut wipe_buffer, WipeMethod::Dod3Pass);
crate::lcpfs_println!(
"[ TRASH ] Secure erase: prepared {} byte wipe buffer for {}",
wipe_buffer.len(),
path
);
}
let _ = dataset; Ok(())
}
pub fn get_trash_config(dataset: &str) -> TrashConfig {
let config = TRASH_CONFIG.lock();
config.get(dataset).cloned().unwrap_or_default()
}
pub fn set_trash_config(dataset: &str, config: TrashConfig) -> Result<(), TrashError> {
let mut configs = TRASH_CONFIG.lock();
configs.insert(dataset.to_string(), config);
Ok(())
}
fn maybe_auto_purge(dataset: &str, config: &TrashConfig) -> Result<(), TrashError> {
let (usage, quota) = get_dataset_usage_quota(dataset);
if quota == 0 {
return Ok(()); }
let usage_pct = ((usage * 100) / quota) as u8;
if usage_pct < config.purge_threshold {
return Ok(()); }
let target_usage = quota * (config.purge_threshold as u64 - 10) / 100;
let mut current_usage = usage;
let oldest_entries: Vec<u64> = {
let metadata = TRASH_METADATA.lock();
match metadata.get(dataset) {
Some(meta) => {
let mut entries = meta.oldest_entries();
entries.iter().map(|e| e.trash_id).collect()
}
None => return Ok(()),
}
};
for trash_id in oldest_entries {
if current_usage <= target_usage {
break;
}
if let Ok(size) = delete_trash_entry(dataset, trash_id) {
current_usage = current_usage.saturating_sub(size);
}
}
Ok(())
}
fn get_file_info(_dataset: &str, path: &str) -> Result<(u64, bool), TrashError> {
let zpl = ZPL.lock();
let object_id = resolve_path_to_object(&zpl, path)?;
match zpl.stat_by_id(object_id) {
Ok(stat) => {
let is_dir = (stat.st_mode & S_IFMT) == S_IFDIR;
let size = if stat.st_size < 0 {
0
} else {
stat.st_size as u64
};
Ok((size, is_dir))
}
Err(e) => Err(TrashError::IoError(format!("Failed to stat: {:?}", e))),
}
}
fn resolve_path_to_object(zpl: &crate::storage::zpl::Zpl, path: &str) -> Result<u64, TrashError> {
if path == "/" || path.is_empty() {
return Ok(zpl.root_id());
}
let path = path.trim_start_matches('/');
let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if components.is_empty() {
return Ok(zpl.root_id());
}
let mut current_id = zpl.root_id();
for component in &components[..components.len() - 1] {
match zpl.lookup(current_id, component) {
Ok(id) => current_id = id,
Err(_) => return Err(TrashError::PathNotFound(path.to_string())),
}
}
let name = components.last().unwrap();
zpl.lookup(current_id, name)
.map_err(|_| TrashError::PathNotFound(path.to_string()))
}
fn current_timestamp() -> u64 {
crate::time::now()
}
fn current_uid() -> u32 {
0
}
fn path_exists(_dataset: &str, path: &str) -> bool {
let zpl = ZPL.lock();
resolve_path_to_object(&zpl, path).is_ok()
}
fn get_parent_path(path: &str) -> Option<&str> {
let path = path.trim_end_matches('/');
path.rfind('/')
.map(|i| if i == 0 { "/" } else { &path[..i] })
}
fn create_directories(_dataset: &str, path: &str) -> Result<(), TrashError> {
let mut zpl = ZPL.lock();
let path = path.trim_start_matches('/');
let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let mut current_id = zpl.root_id();
for component in components {
match zpl.lookup(current_id, component) {
Ok(id) => current_id = id,
Err(FsError::NotFound) => {
match zpl.mkdir(current_id, component, 0o755, 0, 0) {
Ok(new_id) => current_id = new_id,
Err(e) => return Err(TrashError::IoError(format!("mkdir failed: {:?}", e))),
}
}
Err(e) => return Err(TrashError::IoError(format!("lookup failed: {:?}", e))),
}
}
Ok(())
}
fn get_dataset_usage_quota(_dataset: &str) -> (u64, u64) {
let zpl = ZPL.lock();
(zpl.used_bytes(), zpl.quota())
}
fn move_file(src_path: &str, dst_path: &str) -> Result<(), TrashError> {
let mut zpl = ZPL.lock();
let (src_dir_id, src_name) = parse_path_components(&zpl, src_path)?;
let (dst_dir_id, dst_name) = parse_path_components(&zpl, dst_path)?;
zpl.rename(src_dir_id, src_name, dst_dir_id, dst_name)
.map_err(|e| TrashError::IoError(format!("rename failed: {:?}", e)))
}
fn parse_path_components<'a>(
zpl: &crate::storage::zpl::Zpl,
path: &'a str,
) -> Result<(u64, &'a str), TrashError> {
let trimmed = path.trim_start_matches('/').trim_end_matches('/');
let components: Vec<&str> = trimmed.split('/').filter(|s| !s.is_empty()).collect();
if components.is_empty() {
return Err(TrashError::PathNotFound(path.to_string()));
}
let name = components.last().unwrap();
let mut parent_id = zpl.root_id();
for component in &components[..components.len() - 1] {
match zpl.lookup(parent_id, component) {
Ok(id) => parent_id = id,
Err(_) => return Err(TrashError::PathNotFound(path.to_string())),
}
}
Ok((parent_id, name))
}
fn delete_permanently(path: &str) -> Result<(), TrashError> {
let mut zpl = ZPL.lock();
let (dir_id, name) = parse_path_components(&zpl, path)?;
let object_id = zpl
.lookup(dir_id, name)
.map_err(|_| TrashError::PathNotFound(path.to_string()))?;
let stat = zpl
.stat_by_id(object_id)
.map_err(|e| TrashError::IoError(format!("stat failed: {:?}", e)))?;
let is_dir = (stat.st_mode & S_IFMT) == S_IFDIR;
if is_dir {
delete_directory_recursive(&mut zpl, object_id)?;
zpl.rmdir(dir_id, name)
.map_err(|e| TrashError::IoError(format!("rmdir failed: {:?}", e)))
} else {
zpl.unlink(dir_id, name)
.map_err(|e| TrashError::IoError(format!("unlink failed: {:?}", e)))
}
}
fn delete_directory_recursive(
zpl: &mut crate::storage::zpl::Zpl,
dir_id: u64,
) -> Result<(), TrashError> {
let entries: Vec<(String, u64)> = zpl
.readdir(dir_id)
.map_err(|e| TrashError::IoError(format!("readdir failed: {:?}", e)))?
.into_iter()
.filter(|entry| entry.name != "." && entry.name != "..")
.map(|entry| (entry.name, entry.object_id))
.collect();
for (name, object_id) in entries {
let stat = zpl
.stat_by_id(object_id)
.map_err(|e| TrashError::IoError(format!("stat failed: {:?}", e)))?;
let is_dir = (stat.st_mode & S_IFMT) == S_IFDIR;
if is_dir {
delete_directory_recursive(zpl, object_id)?;
zpl.rmdir(dir_id, &name)
.map_err(|e| TrashError::IoError(format!("rmdir failed: {:?}", e)))?;
} else {
zpl.unlink(dir_id, &name)
.map_err(|e| TrashError::IoError(format!("unlink failed: {:?}", e)))?;
}
}
Ok(())
}
fn glob_match(pattern: &str, path: &str) -> bool {
if pattern == "*" {
return true;
}
if pattern.contains("**") {
let parts: Vec<&str> = pattern.split("**").collect();
if parts.len() == 2 {
let prefix = parts[0].trim_end_matches('/');
let suffix = parts[1].trim_start_matches('/');
let suffix_pattern = suffix.strip_prefix('*').unwrap_or(suffix);
let matches_prefix = prefix.is_empty() || path.starts_with(prefix);
let matches_suffix = suffix_pattern.is_empty() || path.ends_with(suffix_pattern);
return matches_prefix && matches_suffix;
}
}
if let Some(suffix) = pattern.strip_prefix('*') {
return path.ends_with(suffix);
}
if let Some(prefix) = pattern.strip_suffix('*') {
return path.starts_with(prefix);
}
pattern == path
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trash_config_default() {
let config = TrashConfig::default();
assert!(config.enabled);
assert_eq!(config.retention_seconds, 30 * 24 * 3600);
assert!(config.auto_purge);
}
#[test]
fn test_trash_list_empty() {
let entries = trash_list("nonexistent").unwrap();
assert!(entries.is_empty());
}
#[test]
fn test_trash_size_empty() {
let size = trash_size("nonexistent").unwrap();
assert_eq!(size, 0);
}
#[test]
fn test_glob_match_wildcard() {
assert!(glob_match("*", "anything"));
assert!(glob_match("*", ""));
assert!(glob_match("*", "/path/to/file.txt"));
}
#[test]
fn test_glob_match_suffix() {
assert!(glob_match("*.txt", "file.txt"));
assert!(glob_match("*.txt", "path/to/file.txt"));
assert!(!glob_match("*.txt", "file.doc"));
assert!(!glob_match("*.txt", "file.txt.bak"));
}
#[test]
fn test_glob_match_prefix() {
assert!(glob_match("/data/*", "/data/file"));
assert!(glob_match("/data/*", "/data/subdir/file"));
assert!(!glob_match("/data/*", "/other/file"));
}
#[test]
fn test_glob_match_double_wildcard() {
assert!(glob_match("**/*.rs", "src/lib.rs"));
assert!(glob_match("**/*.rs", "src/deep/nested/file.rs"));
assert!(!glob_match("**/*.rs", "src/file.txt"));
}
#[test]
fn test_glob_match_exact() {
assert!(glob_match("exact_match", "exact_match"));
assert!(!glob_match("exact_match", "not_exact_match"));
}
#[test]
fn test_get_parent_path() {
assert_eq!(get_parent_path("/a/b/c"), Some("/a/b"));
assert_eq!(get_parent_path("/a/b"), Some("/a"));
assert_eq!(get_parent_path("/a"), Some("/"));
assert_eq!(get_parent_path("/"), None);
assert_eq!(get_parent_path("no_slash"), None);
}
#[test]
fn test_trash_config_get_set() {
let config = TrashConfig {
enabled: true,
retention_seconds: 7 * 24 * 3600,
max_size: 1024 * 1024 * 1024, max_percent: 5,
auto_purge: false,
purge_threshold: 95,
secure_delete: false,
};
set_trash_config("test_dataset", config.clone()).unwrap();
let retrieved = get_trash_config("test_dataset");
assert_eq!(retrieved.enabled, config.enabled);
assert_eq!(retrieved.retention_seconds, config.retention_seconds);
assert_eq!(retrieved.max_size, config.max_size);
}
#[test]
fn test_trash_metadata() {
use super::super::types::TrashMetadata;
let mut meta = TrashMetadata::new();
assert_eq!(meta.next_id, 1);
assert_eq!(meta.total_size, 0);
assert!(meta.entries.is_empty());
assert_eq!(meta.allocate_id(), 1);
assert_eq!(meta.allocate_id(), 2);
assert_eq!(meta.allocate_id(), 3);
assert_eq!(meta.next_id, 4);
let entry = super::super::types::TrashEntry {
original_path: "/test/file.txt".to_string(),
trash_id: 1,
deleted_at: 1000,
size: 4096,
deleted_by: 1000,
expires_at: 2000,
is_directory: false,
};
meta.add_entry(entry);
assert_eq!(meta.entries.len(), 1);
assert_eq!(meta.total_size, 4096);
let found = meta.find_entry(1);
assert!(found.is_some());
assert_eq!(found.unwrap().original_path, "/test/file.txt");
let removed = meta.remove_entry(1);
assert!(removed.is_some());
assert_eq!(meta.entries.len(), 0);
assert_eq!(meta.total_size, 0);
}
}