diff --git a/src/app.rs b/src/app.rs index 3729d38..3290976 100644 --- a/src/app.rs +++ b/src/app.rs @@ -60,29 +60,6 @@ pub struct DoctorReport { pub issues: Vec, } -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum ConfigAuditAction { - Create, - UpdateMode, - Unchanged, -} - -impl ConfigAuditAction { - pub(crate) fn as_str(&self) -> &'static str { - match self { - Self::Create => "create", - Self::UpdateMode => "update-mode", - Self::Unchanged => "unchanged", - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ConfigAuditItem { - pub path: std::path::PathBuf, - pub action: ConfigAuditAction, -} - #[derive(Clone, Debug)] pub struct ConflictInfo { pub path: String, @@ -129,40 +106,30 @@ impl AppaService { .save_roster_with_audit(&roster, &audit_event) } - pub fn write_config_template(&self, config_path: &Path) -> AppResult<()> { + pub fn export_folder_inventory(&self, config_path: &Path) -> AppResult<()> { AppaConfig::write_template(config_path, &self.state_store.folders()?) } - pub fn check_config(&self, config_path: &Path) -> AppResult<()> { + pub fn validate_folder_inventory(&self, config_path: &Path) -> AppResult<()> { let config = AppaConfig::read(config_path)?; for folder in &config.folders { if !folder.path.is_dir() { anyhow::bail!("{} is not a directory", folder.path.display()); } - config.capability(folder)?; } Ok(()) } - pub fn apply_config(&self, config_path: &Path) -> AppResult> { + pub fn import_folder_inventory(&self, config_path: &Path) -> AppResult> { let config = AppaConfig::read(config_path)?; let folders = config .folders .iter() - .map(|folder| self.apply_configured_folder(&config, folder)) + .map(|folder| self.import_folder(folder)) .collect::>>()?; Ok(folders) } - pub fn audit_config(&self, config_path: &Path) -> AppResult> { - let config = AppaConfig::read(config_path)?; - config - .folders - .iter() - .map(|folder| self.audit_configured_folder(&config, folder)) - .collect() - } - pub async fn create_invite(&self, folder_path: &Path) -> AppResult { let node = self.load_node().await?; let result = self.create_invite_with_node(folder_path, &node).await; @@ -426,43 +393,16 @@ impl AppaService { }) } - fn apply_configured_folder( - &self, - config: &AppaConfig, - folder: &ConfiguredFolder, - ) -> AppResult { - let capability = config.capability(folder)?; + fn import_folder(&self, folder: &ConfiguredFolder) -> AppResult { let configured_folder = self.state_store.register_configured_folder( &folder.path, folder.name.clone(), - folder.folder_id, - capability, folder.mode, )?; self.ensure_owner_roster(&configured_folder)?; Ok(configured_folder) } - fn audit_configured_folder( - &self, - config: &AppaConfig, - configured_folder: &ConfiguredFolder, - ) -> AppResult { - config.capability(configured_folder)?; - if !configured_folder.path.is_dir() { - anyhow::bail!("{} is not a directory", configured_folder.path.display()); - } - let existing = self.state_store.find_folder(&configured_folder.path)?; - let action = match existing { - None => ConfigAuditAction::Create, - Some(folder) if folder.mode != configured_folder.mode => ConfigAuditAction::UpdateMode, - Some(_) => ConfigAuditAction::Unchanged, - }; - Ok(ConfigAuditItem { - path: configured_folder.path.clone(), - action, - }) - } fn load_roster(&self, folder_id: crate::domain::FolderId) -> AppResult { self.state_store diff --git a/src/app/tests.rs b/src/app/tests.rs index 65d14e3..85c4706 100644 --- a/src/app/tests.rs +++ b/src/app/tests.rs @@ -14,7 +14,7 @@ use crate::{ iroh::FolderSession, }; -use super::{AppPaths, AppaService, ConfigAuditAction, INVITATION_PROTOCOL_VERSION, Invite}; +use super::{AppPaths, AppaService, INVITATION_PROTOCOL_VERSION, Invite}; const INITIAL_SYNC_ATTEMPTS: usize = 3; const INITIAL_SYNC_RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(100); @@ -49,8 +49,6 @@ fn applies_configuration() -> anyhow::Result<()> { folders: vec![ConfiguredFolder { path: folder_directory.path().to_owned(), name: Some("notes".to_owned()), - folder_id: None, - capability_environment_variable: None, mode: crate::domain::FolderMode::ReceiveOnly, }], }; @@ -59,11 +57,8 @@ fn applies_configuration() -> anyhow::Result<()> { data_directory.path().join("state"), )?)?; - let audit = appa.audit_config(&config_path)?; - assert_eq!(audit[0].action, ConfigAuditAction::Create); - let folders = appa.apply_config(&config_path)?; - let audit = appa.audit_config(&config_path)?; - assert_eq!(audit[0].action, ConfigAuditAction::Unchanged); + appa.validate_folder_inventory(&config_path)?; + let folders = appa.import_folder_inventory(&config_path)?; assert_eq!(folders[0].mode, crate::domain::FolderMode::ReceiveOnly); assert!( diff --git a/src/cli.rs b/src/cli.rs index 8ea2105..30d1b45 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -206,7 +206,7 @@ enum Command { #[command(subcommand)] command: ServiceCommand, }, - #[command(about = "Validate and apply declarative folder configuration")] + #[command(about = "Export, validate, or import a local folder inventory")] Config { #[command(subcommand)] command: ConfigCommand, @@ -220,23 +220,18 @@ enum Command { #[derive(Debug, Subcommand)] enum ConfigCommand { - #[command(about = "Write a configuration template; fails if the file exists")] - Init { - #[arg(default_value = DEFAULT_CONFIG_PATH, value_name = "PATH", help = "Configuration path")] - path: PathBuf, - }, - #[command(about = "Validate a configuration file and its referenced folders")] - Check { + #[command(about = "Write a local folder inventory; fails if the file exists")] + Template { #[arg(default_value = DEFAULT_CONFIG_PATH, value_name = "PATH", help = "Configuration path")] path: PathBuf, }, - #[command(about = "Register configured folders and update their modes")] - Apply { + #[command(about = "Validate a local folder inventory and its paths")] + Validate { #[arg(default_value = DEFAULT_CONFIG_PATH, value_name = "PATH", help = "Configuration path")] path: PathBuf, }, - #[command(about = "Preview configuration changes without modifying state")] - Audit { + #[command(about = "Register folders and update local modes from an inventory")] + Import { #[arg(default_value = DEFAULT_CONFIG_PATH, value_name = "PATH", help = "Configuration path")] path: PathBuf, }, diff --git a/src/cli/setup.rs b/src/cli/setup.rs index 0f7728c..43868d7 100644 --- a/src/cli/setup.rs +++ b/src/cli/setup.rs @@ -35,30 +35,25 @@ pub(super) fn run_identity_command( pub(super) fn run_config(appa: &AppaService, command: ConfigCommand) -> anyhow::Result<()> { match command { - ConfigCommand::Init { path } => { - appa.write_config_template(&path)?; + ConfigCommand::Template { path } => { + appa.export_folder_inventory(&path)?; println!( "Wrote {}. Keep capabilities in environment variables, not this file.", path.display() ); } - ConfigCommand::Check { path } => { - appa.check_config(&path)?; + ConfigCommand::Validate { path } => { + appa.validate_folder_inventory(&path)?; println!("{} is valid.", path.display()); } - ConfigCommand::Apply { path } => { - let folders = appa.apply_config(&path)?; + ConfigCommand::Import { path } => { + let folders = appa.import_folder_inventory(&path)?; println!( "Applied {} folder(s) from {}.", folders.len(), path.display() ); } - ConfigCommand::Audit { path } => { - for item in appa.audit_config(&path)? { - println!("{}\t{}", item.action.as_str(), item.path.display()); - } - } } Ok(()) } diff --git a/src/config.rs b/src/config.rs index 5708af9..021d095 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,16 +1,12 @@ -//! Declarative, non-secret local folder configuration. +//! Non-secret local folder inventory used for bulk import. -use std::{ - env, fs, - path::{Path, PathBuf}, -}; +use std::{fs, path::{Path, PathBuf}}; use serde::{Deserialize, Serialize}; -use uuid::Uuid; use crate::{domain::FolderMode, storage::FolderConfig}; -/// Version of the declarative `appa.toml` format. +/// Version of the local folder inventory format. pub const CONFIG_VERSION: u16 = 1; pub const DEFAULT_CONFIG_PATH: &str = "appa.toml"; @@ -28,9 +24,6 @@ pub struct AppaConfig { pub struct ConfiguredFolder { pub(crate) path: PathBuf, pub(crate) name: Option, - pub(crate) folder_id: Option, - #[serde(rename = "capability_env")] - pub(crate) capability_environment_variable: Option, #[serde(default)] pub(crate) mode: FolderMode, } @@ -60,32 +53,10 @@ impl AppaConfig { Ok(()) } - pub fn capability(&self, folder: &ConfiguredFolder) -> anyhow::Result> { - folder - .capability_environment_variable - .as_ref() - .map(|variable| match env::var(variable) { - Ok(value) => Ok(value), - Err(env::VarError::NotPresent) => anyhow::bail!("{variable} is not set"), - Err(env::VarError::NotUnicode(_)) => { - anyhow::bail!("{variable} contains non-Unicode text") - } - }) - .transpose() - } - fn validate(&self) -> anyhow::Result<()> { if self.version != CONFIG_VERSION { anyhow::bail!("unsupported Appa config version {}", self.version); } - for folder in &self.folders { - if folder.folder_id.is_some() != folder.capability_environment_variable.is_some() { - anyhow::bail!( - "{} must set both folder_id and capability_env to create a joined folder", - folder.path.display() - ); - } - } Ok(()) } } @@ -95,8 +66,6 @@ impl ConfiguredFolder { Self { path: folder.path.clone(), name: Some(folder.name.clone()), - folder_id: None, - capability_environment_variable: None, mode: folder.mode, } } @@ -111,7 +80,7 @@ mod tests { use super::{AppaConfig, CONFIG_VERSION}; #[test] - fn rejects_a_folder_id_without_a_capability_reference() { + fn rejects_shared_folder_fields() { let directory = TempDir::new().expect("temporary directory"); let config_path = directory.path().join("appa.toml"); fs::write(&config_path, format!("version = {CONFIG_VERSION}\n[[folders]]\npath = \"notes\"\nfolder_id = \"00000000-0000-0000-0000-000000000000\"\n")).expect("write config"); diff --git a/src/storage.rs b/src/storage.rs index 1568425..95c0ed4 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -126,8 +126,6 @@ impl StateStore { &self, path: &Path, name: Option, - id: Option, - capability: Option, mode: FolderMode, ) -> anyhow::Result { if !path.is_dir() { @@ -137,10 +135,10 @@ impl StateStore { return self.update_folder_mode(folder, mode); } let folder = FolderConfig { - id: id.unwrap_or_else(Uuid::new_v4), + id: Uuid::new_v4(), name: name.unwrap_or(folder_name(path)?), path: path.canonicalize()?, - capability: capability.unwrap_or_else(create_capability), + capability: create_capability(), mode, }; self.connection.execute(