Files
IronStorage/crates/storage/src/desktop.rs

179 lines
5.6 KiB
Rust

//! Storage-owned service boundary for the Iced desktop presentation adapter.
use std::{error::Error, fmt, path::Path};
use crate::{
authentication::{NativeAuthenticationHandle, NativeAuthenticationSession},
config::Config,
crypto::{KeyInfo, KeyStore, SecretProvider},
document::{DocumentError, EntryDocument, EntryDocumentService},
git::{AutomaticEntryCommitter, GitIdentity},
presentation::ClipboardTimeout,
read::{TreeModel, VaultReader},
repository::{DirectoryPath, Repository},
secret_store::SecretProtectionPolicy,
write::{WriteError, WriteOutcome},
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DesktopErrorKind {
Configuration,
Authentication,
KeyMaterial,
Repository,
Read,
Document,
Git,
Conflict,
Unchanged,
MissingDefaultKey,
}
#[derive(Debug)]
pub struct DesktopError {
kind: DesktopErrorKind,
message: String,
}
impl DesktopError {
pub fn kind(&self) -> DesktopErrorKind {
self.kind
}
fn new(kind: DesktopErrorKind, error: impl fmt::Display) -> Self {
Self {
kind,
message: error.to_string(),
}
}
fn document(error: DocumentError) -> Self {
let kind = match &error {
DocumentError::Write(WriteError::ConcurrentModification { .. }) => {
DesktopErrorKind::Conflict
}
DocumentError::Write(WriteError::Unchanged) => DesktopErrorKind::Unchanged,
_ => DesktopErrorKind::Document,
};
Self::new(kind, error)
}
}
impl fmt::Display for DesktopError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}
impl Error for DesktopError {}
#[derive(Clone, Debug)]
pub struct DesktopStorage {
config: Config,
}
impl DesktopStorage {
pub fn load(explicit: Option<&Path>) -> Result<Self, DesktopError> {
Config::load(explicit)
.map(|config| Self { config })
.map_err(|error| DesktopError::new(DesktopErrorKind::Configuration, error))
}
pub fn system() -> Result<DesktopBootstrap, DesktopError> {
let storage = Self::load(None)?;
let keys = KeyStore::load(storage.config.key_material())
.map_err(|error| DesktopError::new(DesktopErrorKind::KeyMaterial, error))?;
let handle = keys
.resolve(storage.config.default_key().as_str())
.map_err(|error| DesktopError::new(DesktopErrorKind::KeyMaterial, error))?;
let key = keys
.infos()
.find(|key| key.fingerprint() == handle.fingerprint())
.ok_or_else(|| {
DesktopError::new(
DesktopErrorKind::MissingDefaultKey,
"the configured GPG key is unavailable",
)
})?;
let authentication = NativeAuthenticationSession::system(
SecretProtectionPolicy::default(),
storage.config.authentication_timeout(),
)
.map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
Ok(DesktopBootstrap {
storage,
authentication,
key,
})
}
pub fn clipboard_timeout(&self) -> ClipboardTimeout {
self.config.clipboard_timeout()
}
pub fn tree(&self) -> Result<TreeModel, DesktopError> {
let repository = self.repository()?;
let keys = self.keys()?;
VaultReader::new(&repository, &keys)
.list(&DirectoryPath::root())
.map_err(|error| DesktopError::new(DesktopErrorKind::Read, error))
}
pub fn open_document(
&self,
entry: &str,
secrets: &mut impl SecretProvider,
) -> Result<EntryDocument, DesktopError> {
let repository = self.repository()?;
let keys = self.keys()?;
EntryDocumentService::new(&repository, &keys)
.open(entry, secrets)
.map_err(DesktopError::document)
}
pub fn save_document(&self, document: &EntryDocument) -> Result<WriteOutcome, DesktopError> {
let repository = self.repository()?;
let keys = self.keys()?;
let entry = document.path().to_string();
let mut committer =
AutomaticEntryCommitter::for_entry(&repository, &entry, GitIdentity::ironstorage())
.map_err(|error| DesktopError::new(DesktopErrorKind::Git, error))?;
EntryDocumentService::new(&repository, &keys)
.save_recoverable(document, None, &mut committer)
.map_err(DesktopError::document)
}
pub fn save_active_document(
&self,
handle: &NativeAuthenticationHandle,
document: &EntryDocument,
) -> Result<WriteOutcome, DesktopError> {
handle
.ensure_active()
.map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
self.save_document(document)
}
fn repository(&self) -> Result<Repository, DesktopError> {
Repository::open(self.config.vault())
.map_err(|error| DesktopError::new(DesktopErrorKind::Repository, error))
}
fn keys(&self) -> Result<KeyStore, DesktopError> {
KeyStore::load(self.config.key_material())
.map_err(|error| DesktopError::new(DesktopErrorKind::KeyMaterial, error))
}
}
pub struct DesktopBootstrap {
storage: DesktopStorage,
authentication: NativeAuthenticationSession,
key: KeyInfo,
}
impl DesktopBootstrap {
pub fn into_parts(self) -> (DesktopStorage, NativeAuthenticationSession, KeyInfo) {
(self.storage, self.authentication, self.key)
}
}