waybetter/crates/waybetter-host/src/vault_commands.rs

80 lines
3.3 KiB
Rust

//! The drafts vault, native side, for every app that sets [`crate::app::AppConfig::vault`]: opens the
//! app's [`Vault`] in app data at launch and serves the shell-only `draft_list`, `draft_save`,
//! `draft_revisions`, `draft_status`, `observation_list` and `data_export` ([`crate::commands::VAULT`]).
//! A write is acknowledged only after its SQLite transaction committed. The app's own native code (its
//! capture, a popover that reads drafts, a backup import) takes the same vault through [`lock`]; what
//! it does with it is the app's.
use crate::app::config;
use crate::host::{data_file, shell_only};
use crate::vault::{Draft, DraftRevision, ExportData, Observation, SaveDraftInput, Vault, VaultRules};
use std::sync::{Mutex, MutexGuard};
use tauri::{AppHandle, Manager, Webview};
/// What an app declares about its vault: the database file in app data and its rules.
#[derive(Clone, Copy, Debug)]
pub struct VaultConfig {
pub file: &'static str,
pub rules: &'static VaultRules,
}
struct VaultState(Mutex<Vault>);
/// Opens the database. Does nothing unless the app set [`crate::app::AppConfig::vault`]. A database
/// that cannot be opened (a newer schema, a damaged file) stops the launch rather than starting empty.
pub(crate) fn init(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
let Some(vault) = config(app).vault else { return Ok(()) };
let path = data_file(app, vault.file).ok_or("STORAGE: no application data directory")?;
app.manage(VaultState(Mutex::new(Vault::open(path, vault.rules).map_err(std::io::Error::other)?)));
Ok(())
}
/// The vault, locked. Hold it only for the call that needs it; never across a webview call.
pub fn lock(app: &AppHandle) -> Result<MutexGuard<'_, Vault>, String> {
app.try_state::<VaultState>()
.ok_or("DATABASE: vault unavailable; restart safely")?
.inner()
.0
.lock()
.map_err(|_| "DATABASE: workspace state unavailable; restart safely".into())
}
#[tauri::command]
pub(crate) async fn draft_list(webview: Webview, app: AppHandle) -> Result<Vec<Draft>, String> {
shell_only(&webview)?;
lock(&app)?.list_drafts()
}
#[tauri::command]
pub(crate) async fn draft_save(webview: Webview, app: AppHandle, input: SaveDraftInput) -> Result<Draft, String> {
shell_only(&webview)?;
lock(&app)?.save_draft(&input)
}
#[tauri::command]
pub(crate) async fn draft_revisions(webview: Webview, app: AppHandle, draft_id: String) -> Result<Vec<DraftRevision>, String> {
shell_only(&webview)?;
lock(&app)?.revisions(&draft_id)
}
#[tauri::command]
pub(crate) async fn draft_status(webview: Webview, app: AppHandle, id: String, expected_revision: i64, status: String) -> Result<Draft, String> {
shell_only(&webview)?;
lock(&app)?.set_status(&id, expected_revision, &status)
}
#[tauri::command]
pub(crate) async fn observation_list(webview: Webview, app: AppHandle) -> Result<Vec<Observation>, String> {
shell_only(&webview)?;
lock(&app)?.observations()
}
#[tauri::command]
pub(crate) async fn data_export(webview: Webview, app: AppHandle) -> Result<ExportData, String> {
shell_only(&webview)?;
lock(&app)?.export()
}
pub(crate) fn handler() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
tauri::generate_handler![draft_list, draft_save, draft_revisions, draft_status, observation_list, data_export]
}