Add desktop search and entry mutation workflows

This commit is contained in:
2026-08-10 19:19:46 +02:00
parent bb650c2b5c
commit de5dea28ef
8 changed files with 1292 additions and 30 deletions

View File

@@ -6,20 +6,21 @@ use crate::{
authentication::{
AuthenticationTimeout, NativeAuthenticationHandle, NativeAuthenticationSession,
},
command::InitRequest,
command::{CopyRequest, FindRequest, GrepRequest, InitRequest, MoveRequest, RemoveRequest},
config::{Config, ConfigSettings, EditorCommand},
crypto::{KeyInfo, KeyStore, SecretProvider},
document::{DocumentError, EntryDocument, EntryDocumentService},
git::{AutomaticEntryCommitter, AutomaticPolicyCommitter, GitIdentity},
git::{AutomaticEntryCommitter, AutomaticPolicyCommitter, AutomaticTreeCommitter, GitIdentity},
mutation::{MutationOutcome, TreeMutator},
presentation::ClipboardTimeout,
read::{TreeModel, VaultReader},
read::{FindResults, GrepResults, TreeModel, VaultReader},
recipient::{
PolicyCommit, PolicyCommitError, PolicyCommitter, RecipientPolicyManager,
RecipientPolicyOutcome,
},
repository::{DirectoryPath, Repository},
secret_store::SecretProtectionPolicy,
write::{VaultWriter, WriteError, WriteOutcome},
write::{OverwriteDecision, VaultWriter, WriteError, WriteOutcome},
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -35,6 +36,7 @@ pub enum DesktopErrorKind {
Unchanged,
MissingDefaultKey,
EntryExists,
Mutation,
}
#[derive(Debug)]
@@ -80,6 +82,23 @@ pub struct DesktopStorage {
config: Config,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DesktopMutationRequest {
Remove(RemoveRequest),
Move(MoveRequest),
Copy(CopyRequest),
}
impl DesktopMutationRequest {
pub fn source(&self) -> &str {
match self {
Self::Remove(request) => &request.entry,
Self::Move(request) => &request.source,
Self::Copy(request) => &request.source,
}
}
}
impl DesktopStorage {
pub fn load(explicit: Option<&Path>) -> Result<Self, DesktopError> {
Config::load(explicit)
@@ -199,6 +218,80 @@ impl DesktopStorage {
.map_err(|error| DesktopError::new(DesktopErrorKind::Read, error))
}
pub fn find(&self, request: &FindRequest) -> Result<FindResults, DesktopError> {
let repository = self.repository()?;
let keys = self.keys()?;
VaultReader::new(&repository, &keys)
.find(&request.terms)
.map_err(|error| DesktopError::new(DesktopErrorKind::Read, error))
}
pub fn grep_active(
&self,
handle: &NativeAuthenticationHandle,
request: &GrepRequest,
) -> Result<GrepResults, DesktopError> {
handle
.ensure_active()
.map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
let mut provider = handle.clone();
self.grep(request, &mut provider)
}
pub fn grep(
&self,
request: &GrepRequest,
provider: &mut impl SecretProvider,
) -> Result<GrepResults, DesktopError> {
let repository = self.repository()?;
let keys = self.keys()?;
VaultReader::new(&repository, &keys)
.grep(request, provider)
.map_err(|error| DesktopError::new(DesktopErrorKind::Read, error))
}
pub fn mutate_active(
&self,
handle: &NativeAuthenticationHandle,
request: &DesktopMutationRequest,
overwrite: OverwriteDecision,
) -> Result<MutationOutcome, DesktopError> {
handle
.ensure_active()
.map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
let mut provider = handle.clone();
self.mutate(request, overwrite, &mut provider)
}
pub fn mutate(
&self,
request: &DesktopMutationRequest,
overwrite: OverwriteDecision,
provider: &mut impl SecretProvider,
) -> Result<MutationOutcome, DesktopError> {
let repository = self.repository()?;
let keys = self.keys()?;
let mut committer = AutomaticTreeCommitter::for_source(
&repository,
request.source(),
GitIdentity::ironstorage(),
)
.map_err(|error| DesktopError::new(DesktopErrorKind::Git, error))?;
let mutator = TreeMutator::new(&repository, &keys);
match request {
DesktopMutationRequest::Remove(request) => {
mutator.remove(request, overwrite, &mut committer)
}
DesktopMutationRequest::Move(request) => {
mutator.move_tree(request, overwrite, None, provider, &mut committer)
}
DesktopMutationRequest::Copy(request) => {
mutator.copy(request, overwrite, None, provider, &mut committer)
}
}
.map_err(|error| DesktopError::new(DesktopErrorKind::Mutation, error))
}
/// Enumerate storage-validated encryption keys for recipient selection.
pub fn key_infos(&self) -> Result<Vec<KeyInfo>, DesktopError> {
Ok(self.keys()?.infos().filter(KeyInfo::can_encrypt).collect())

View File

@@ -229,17 +229,23 @@ impl fmt::Debug for ShowResult {
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NameMatch {
path: String,
kind: TreeNodeKind,
id: TreeNodeId,
}
impl NameMatch {
pub fn path(&self) -> &str {
&self.path
self.id
.path()
.to_str()
.expect("search construction rejects non-UTF-8 paths")
}
pub fn kind(&self) -> TreeNodeKind {
self.kind
self.id.kind()
}
pub fn id(&self) -> &TreeNodeId {
&self.id
}
}
@@ -489,8 +495,7 @@ impl<'a> VaultReader<'a> {
let name = display_name(directory.path().as_path())?;
if folded.iter().any(|term| name.to_lowercase().contains(term)) {
matches.push(NameMatch {
path: path_text(directory.path().as_path())?,
kind: TreeNodeKind::Directory,
id: TreeNodeId::Directory(directory.path().clone()),
});
}
}
@@ -501,16 +506,12 @@ impl<'a> VaultReader<'a> {
let name = display_name(entry.path().as_path())?;
if folded.iter().any(|term| name.to_lowercase().contains(term)) {
matches.push(NameMatch {
path: path_text(entry.path().as_path())?,
kind: TreeNodeKind::Entry,
id: TreeNodeId::Entry(entry.path().clone()),
});
}
}
matches.sort_by(|left, right| left.path.cmp(&right.path));
let included = matches
.iter()
.map(|matched| matched.path.as_str())
.collect::<Vec<_>>();
matches.sort_by(|left, right| left.path().cmp(right.path()));
let included = matches.iter().map(NameMatch::path).collect::<Vec<_>>();
let tree = build_tree(&snapshot, &DirectoryPath::root(), Some(included.as_slice()))?;
Ok(FindResults {
terms: terms.to_vec(),

View File

@@ -194,6 +194,10 @@ fn find_matches_entry_and_directory_names_case_insensitively() -> TestResult {
);
let personal = reader.find(&["PERSONAL".to_owned()])?;
assert_eq!(personal.matches()[0].path(), "email/personal");
assert_eq!(
personal.matches()[0].id(),
&ironstorage::read::TreeNodeId::Entry(EntryPath::parse("email/personal")?)
);
assert!(matches!(
reader.find(&[]),
Err(ReadError::MissingSearchTerms)