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