Skip to main content

kaish_kernel/
trash.rs

1//! Trash backend trait and types.
2//!
3//! Abstracts trash operations behind a trait so the implementation
4//! can be swapped (system trash, WASI, pure-Rust disk trash, etc.)
5//! without changing builtins.
6
7#[cfg(feature = "os-integration")]
8use std::ffi::OsString;
9use std::path::{Path, PathBuf};
10
11use async_trait::async_trait;
12
13/// Errors from trash operations.
14#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum TrashError {
17    #[error("{0}")]
18    Backend(String),
19    #[error("task join failed: {0}")]
20    Join(String),
21}
22
23/// Opaque trash item identifier.
24///
25/// Wraps backend-specific IDs so callers don't depend on the `trash` crate directly.
26pub struct TrashId(pub(crate) TrashIdInner);
27
28pub(crate) enum TrashIdInner {
29    /// System trash: wraps the `trash` crate's `OsString` ID.
30    #[cfg(feature = "os-integration")]
31    System(OsString),
32    /// Placeholder — TrashId is opaque and never constructed without a backend.
33    #[cfg(not(feature = "os-integration"))]
34    _Unavailable,
35}
36
37impl TrashId {
38    /// Create a TrashId wrapping a system trash item ID.
39    #[cfg(feature = "os-integration")]
40    pub(crate) fn system(id: OsString) -> Self {
41        Self(TrashIdInner::System(id))
42    }
43}
44
45impl std::fmt::Debug for TrashId {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match &self.0 {
48            #[cfg(feature = "os-integration")]
49            TrashIdInner::System(_) => f.write_str("TrashId::System(..)"),
50            #[cfg(not(feature = "os-integration"))]
51            TrashIdInner::_Unavailable => f.write_str("TrashId::Unavailable"),
52        }
53    }
54}
55
56/// A trashed item, independent of backend.
57#[derive(Debug)]
58pub struct TrashEntry {
59    pub id: TrashId,
60    pub name: String,
61    pub original_path: PathBuf,
62    /// Seconds since UNIX epoch when the item was deleted.
63    pub deleted_at: i64,
64}
65
66/// Find restore matches: exact (1) wins, else substring.
67///
68/// Single pass over items. Returns matched items or error message.
69/// Used by backends that need name-based matching (e.g., SystemTrash).
70pub fn find_restore_match<T>(items: Vec<(String, T)>, target: &str) -> Result<Vec<T>, String> {
71    let mut exact = Vec::new();
72    let mut substring = Vec::new();
73    let mut substring_names = Vec::new();
74
75    for (name, item) in items {
76        if name == target {
77            exact.push(item);
78        } else if name.contains(target) {
79            substring_names.push(name);
80            substring.push(item);
81        }
82    }
83
84    if exact.len() == 1 {
85        return Ok(exact);
86    }
87
88    // Combine exact + substring if no single exact match
89    let mut all_names: Vec<String> = Vec::new();
90    if !exact.is_empty() {
91        all_names.extend(std::iter::repeat_n(target.to_string(), exact.len()));
92    }
93    all_names.extend(substring_names);
94
95    let mut all: Vec<T> = exact;
96    all.extend(substring);
97
98    if all.is_empty() {
99        return Err(format!("'{}' not found in trash", target));
100    }
101    if all.len() > 1 {
102        return Err(format!(
103            "multiple matches for '{}': {}. Be more specific.",
104            target,
105            all_names.join(", ")
106        ));
107    }
108    Ok(all)
109}
110
111/// Backend trait for trash operations.
112#[async_trait]
113pub trait TrashBackend: Send + Sync {
114    /// Move a file or directory to trash.
115    async fn trash(&self, path: &Path) -> Result<(), TrashError>;
116
117    /// Snapshot raw bytes into the trash under a name derived from
118    /// `original_path`'s basename.
119    ///
120    /// Used by the write-model gate to back up a file's prior content before a
121    /// truncating overwrite (`tee`/`patch`/`sed -i`). Unlike [`trash`](Self::trash)
122    /// it *copies* rather than moves, so the file stays in place for the overwrite
123    /// (and for read-modify-write callers). What's recoverable is the snapshot's
124    /// bytes (via `list`/`restore`), not its original location.
125    async fn trash_bytes(&self, original_path: &Path, bytes: &[u8]) -> Result<(), TrashError>;
126
127    /// List trashed items, optionally filtered by name substring.
128    async fn list(&self, filter: Option<&str>) -> Result<Vec<TrashEntry>, TrashError>;
129
130    /// Find entries matching a name (exact first, then substring).
131    ///
132    /// Returns matched entries or an error describing the ambiguity.
133    async fn find_by_name(&self, name: &str) -> Result<Vec<TrashEntry>, TrashError>;
134
135    /// Restore trashed items to their original locations.
136    async fn restore(&self, entries: Vec<TrashEntry>) -> Result<(), TrashError>;
137
138    /// Permanently delete all trashed items. Returns count purged.
139    async fn purge_all(&self) -> Result<usize, TrashError>;
140}