@@ -5,6 +5,7 @@
|
||||
|
||||
pub mod action;
|
||||
pub mod app;
|
||||
pub mod editor;
|
||||
pub mod runtime;
|
||||
pub mod sidebar;
|
||||
pub mod terminal;
|
||||
@@ -22,7 +23,7 @@ use ratatui::DefaultTerminal;
|
||||
|
||||
use crate::{
|
||||
action::resolve_key,
|
||||
app::{App, AppEffect, AsyncPayload, StartupData},
|
||||
app::{App, AppEffect, AsyncPayload, EditorSaveFailure, EditorSaveFailureKind, StartupData},
|
||||
runtime::{AsyncExecutor, AuthenticationCoordinator, AuthenticationEvent},
|
||||
};
|
||||
|
||||
@@ -98,6 +99,14 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
{
|
||||
apply_authentication_event(&mut app, coordinator, &executor, event);
|
||||
}
|
||||
if !key.modifiers.intersects(
|
||||
crossterm::event::KeyModifiers::CONTROL
|
||||
| crossterm::event::KeyModifiers::ALT
|
||||
| crossterm::event::KeyModifiers::SUPER,
|
||||
) && app.handle_editor_input(key.code)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if app.sidebar().is_editing_filter() {
|
||||
if handle_filter_key(&mut app, key.code) {
|
||||
submit_filter(&mut app, &executor);
|
||||
@@ -148,6 +157,27 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
});
|
||||
}
|
||||
}
|
||||
AppEffect::GenerateField(target) => {
|
||||
let token = app.begin_request();
|
||||
executor.submit(token, move || {
|
||||
ironstorage::generate::GeneratorConfig::pass_defaults()
|
||||
.generate_secret(None, false)
|
||||
.map(|password| AsyncPayload::GeneratedField {
|
||||
target,
|
||||
password,
|
||||
})
|
||||
.map_err(|error| error.to_string())
|
||||
});
|
||||
}
|
||||
AppEffect::SaveDocument {
|
||||
config,
|
||||
entry,
|
||||
editor,
|
||||
} => {
|
||||
let token = app.begin_request();
|
||||
executor
|
||||
.submit(token, move || Ok(save_document(&config, entry, editor)));
|
||||
}
|
||||
AppEffect::ManualLock => {
|
||||
if let Some(coordinator) = authentication.as_mut()
|
||||
&& let Err(error) = coordinator.lock()
|
||||
@@ -240,6 +270,61 @@ fn load_document(
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn save_document(
|
||||
config: &ironstorage::config::Config,
|
||||
entry: String,
|
||||
editor: Box<editor::EntryEditor>,
|
||||
) -> AsyncPayload {
|
||||
let result = save_document_inner(config, &entry, &editor);
|
||||
AsyncPayload::DocumentSaveFinished {
|
||||
entry,
|
||||
editor,
|
||||
result,
|
||||
}
|
||||
}
|
||||
|
||||
fn save_document_inner(
|
||||
config: &ironstorage::config::Config,
|
||||
entry: &str,
|
||||
editor: &editor::EntryEditor,
|
||||
) -> Result<ironstorage::write::WriteOutcome, EditorSaveFailure> {
|
||||
let repository =
|
||||
ironstorage::repository::Repository::open(config.vault()).map_err(|error| {
|
||||
EditorSaveFailure::new(EditorSaveFailureKind::Storage, error.to_string())
|
||||
})?;
|
||||
let keys = ironstorage::crypto::KeyStore::load(config.key_material()).map_err(|error| {
|
||||
EditorSaveFailure::new(EditorSaveFailureKind::Storage, error.to_string())
|
||||
})?;
|
||||
let identity = ironstorage::git::GitIdentity::ironstorage();
|
||||
let mut committer =
|
||||
ironstorage::git::AutomaticEntryCommitter::for_entry(&repository, entry, identity)
|
||||
.map_err(|error| {
|
||||
EditorSaveFailure::new(EditorSaveFailureKind::Storage, error.to_string())
|
||||
})?;
|
||||
ironstorage::document::EntryDocumentService::new(&repository, &keys)
|
||||
.save_recoverable(editor.document(), None, &mut committer)
|
||||
.map_err(|error| {
|
||||
let kind = if matches!(
|
||||
&error,
|
||||
ironstorage::document::DocumentError::Write(
|
||||
ironstorage::write::WriteError::ConcurrentModification { .. }
|
||||
)
|
||||
) {
|
||||
EditorSaveFailureKind::Conflict
|
||||
} else if matches!(
|
||||
&error,
|
||||
ironstorage::document::DocumentError::Write(
|
||||
ironstorage::write::WriteError::Unchanged
|
||||
)
|
||||
) {
|
||||
EditorSaveFailureKind::Unchanged
|
||||
} else {
|
||||
EditorSaveFailureKind::Storage
|
||||
};
|
||||
EditorSaveFailure::new(kind, error.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn load_tree(config: &ironstorage::config::Config) -> Result<ironstorage::read::TreeModel, String> {
|
||||
let repository = ironstorage::repository::Repository::open(config.vault())
|
||||
.map_err(|error| error.to_string())?;
|
||||
@@ -288,3 +373,70 @@ fn handle_filter_key(app: &mut App, code: crossterm::event::KeyCode) -> bool {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use super::*;
|
||||
use crate::{editor::EntryEditor, viewer::test_support::fixture_document_from};
|
||||
|
||||
#[test]
|
||||
fn editor_save_encrypts_and_automatically_commits_entirely_in_storage() {
|
||||
let temporary = tempfile::tempdir().expect("temporary editor store");
|
||||
let fixtures = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../crates/storage/tests/fixtures/compatibility");
|
||||
let store = temporary.path().join("store");
|
||||
copy_directory(&fixtures.join("stores/basic"), &store);
|
||||
|
||||
let repository = ironstorage::repository::Repository::open(&store).expect("repository");
|
||||
let identity = ironstorage::git::GitIdentity::ironstorage();
|
||||
let git = ironstorage::git::GitRepository::init(&repository, identity.clone())
|
||||
.expect("initialize git");
|
||||
let initial_commits = git.log(None).expect("initial log").len();
|
||||
assert!(initial_commits >= 1);
|
||||
drop(git);
|
||||
|
||||
let config_path = temporary.path().join("config.toml");
|
||||
fs::write(
|
||||
&config_path,
|
||||
format!(
|
||||
"vault = {:?}\ndefault_key = \"7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30\"\nkey_material = {:?}\n",
|
||||
store,
|
||||
fixtures.join("keys"),
|
||||
),
|
||||
)
|
||||
.expect("write config");
|
||||
let config = ironstorage::config::Config::load(Some(&config_path)).expect("config");
|
||||
|
||||
let mut editor = EntryEditor::new(fixture_document_from(&store, "email/personal"));
|
||||
editor.begin_input();
|
||||
editor.insert_character('x');
|
||||
let outcome = save_document_inner(&config, "email/personal", &editor).expect("save");
|
||||
assert_eq!(outcome.path().to_string(), "email/personal");
|
||||
|
||||
let reopened = fixture_document_from(&store, "email/personal");
|
||||
assert!(
|
||||
reopened
|
||||
.password()
|
||||
.expect("password")
|
||||
.value()
|
||||
.ends_with(b"x")
|
||||
);
|
||||
let git = ironstorage::git::GitRepository::open(&repository, identity).expect("reopen git");
|
||||
assert_eq!(git.log(None).expect("saved log").len(), initial_commits + 1);
|
||||
}
|
||||
|
||||
fn copy_directory(source: &Path, destination: &Path) {
|
||||
fs::create_dir_all(destination).expect("create destination");
|
||||
for entry in fs::read_dir(source).expect("read source") {
|
||||
let entry = entry.expect("directory entry");
|
||||
let target = destination.join(entry.file_name());
|
||||
if entry.file_type().expect("file type").is_dir() {
|
||||
copy_directory(&entry.path(), &target);
|
||||
} else {
|
||||
fs::copy(entry.path(), target).expect("copy fixture file");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user