From c448f01b418395f6700fe8a7e37f441a996842d5 Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Mon, 3 Aug 2026 08:53:45 -0400 Subject: [PATCH] Route folder imports through the daemon --- README.md | 10 +++++----- src/app.rs | 1 - src/cli.rs | 48 ++++++++++++++++++++++++++++-------------------- src/cli/setup.rs | 10 ++-------- src/config.rs | 5 ++++- src/daemon.rs | 3 +++ src/ipc.rs | 13 +++++++++++++ 7 files changed, 55 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 991c82a..71ee811 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ state. Join shared folders with an invitation instead. Write an inventory from your current folders ```sh -appa --offline folder template +appa folder template ``` This writes `appa.toml` by default with one `[[folders]]` entry per folder. @@ -154,13 +154,13 @@ Each entry has a `path`, optional `name`, and local `mode`. `mode` is Validate and import ```sh -appa --offline folder validate -appa --offline folder import +appa folder validate +appa folder import ``` `import` registers new folders and updates the local mode of existing ones. It -does not remove folders that are absent from the inventory. Start the daemon -afterward; it picks up registered folders within a second. +uses the daemon, so it is safe to run while Appa is active. It does not remove +folders that are absent from the inventory. ## Change folders while Appa runs diff --git a/src/app.rs b/src/app.rs index 3290976..f99f882 100644 --- a/src/app.rs +++ b/src/app.rs @@ -403,7 +403,6 @@ impl AppaService { Ok(configured_folder) } - fn load_roster(&self, folder_id: crate::domain::FolderId) -> AppResult { self.state_store .load_roster(folder_id)? diff --git a/src/cli.rs b/src/cli.rs index 5cdcb12..fa1268b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,6 +10,7 @@ use std::{ use crate::app::AppaService; use crate::config::DEFAULT_CONFIG_PATH; +use crate::storage::AppPaths; mod setup; mod tooling; @@ -53,12 +54,6 @@ impl FolderRemoval { after_help = "Run `appa --help` for command-specific usage. Most commands connect to the local daemon; start it with `appa run` or `appa service install`." )] struct CommandLine { - #[arg( - long, - global = true, - help = "Required for identity and folder inventory commands after stopping the daemon; ignored by other commands" - )] - offline: bool, #[command(subcommand)] command: Command, } @@ -276,7 +271,7 @@ enum DaemonCommand { pub async fn run() -> anyhow::Result<()> { initialize_logging(); - let CommandLine { command, offline } = CommandLine::parse(); + let CommandLine { command } = CommandLine::parse(); tracing::debug!(command = ?command, "dispatched Appa command"); match command { Command::Daemon { @@ -285,19 +280,17 @@ pub async fn run() -> anyhow::Result<()> { Command::Service { command } => tooling::run_service_command(command)?, Command::Completions { shell } => tooling::print_completions(shell), Command::Run { folder } => crate::daemon::serve(folder.as_deref().map(Path::new)).await?, - command => run_app_command(command, offline).await?, + command => run_app_command(command).await?, } Ok(()) } -async fn run_app_command(command: Command, offline: bool) -> anyhow::Result<()> { - if matches!(command, Command::Identity { .. } | Command::Folder { .. }) { - if !offline { - anyhow::bail!( - "this command changes local state directly; stop the daemon and pass --offline" - ) - } - return run_offline_command(command).await; +async fn run_app_command(command: Command) -> anyhow::Result<()> { + if matches!(command, Command::Identity { .. }) { + return run_local_command(command).await; + } + if let Command::Folder { command } = command { + return run_folder_command(command).await; } let client = crate::ipc::DaemonClient::connect()?; match command { @@ -378,19 +371,34 @@ async fn run_app_command(command: Command, offline: bool) -> anyhow::Result<()> Command::Service { .. } => unreachable!("service commands are handled before opening Appa"), Command::Daemon { .. } => unreachable!("daemon commands are handled before opening Appa"), Command::Identity { .. } | Command::Folder { .. } => { - unreachable!("offline commands are handled first") + unreachable!("handled before opening Appa") } Command::Run { .. } => unreachable!("run is handled before opening Appa"), } Ok(()) } -async fn run_offline_command(command: Command) -> anyhow::Result<()> { +async fn run_local_command(command: Command) -> anyhow::Result<()> { + let paths = AppPaths::discover()?; + if paths.daemon_socket_path().exists() { + anyhow::bail!("stop the Appa daemon before replacing or exporting its identity"); + } let appa = AppaService::open()?; match command { Command::Identity { command } => setup::run_identity_command(&appa, command)?, - Command::Folder { command } => setup::run_folder_command(&appa, command)?, - _ => unreachable!("only offline commands reach this handler"), + _ => unreachable!("only identity commands reach this handler"), + } + Ok(()) +} + +async fn run_folder_command(command: FolderCommand) -> anyhow::Result<()> { + match command { + FolderCommand::Import { path } => { + let client = crate::ipc::DaemonClient::connect()?; + let count = client.import_folder_inventory(path.clone()).await?; + println!("Imported {count} folder(s) from {}.", path.display()); + } + command => setup::run_folder_command(&AppaService::open()?, command)?, } Ok(()) } diff --git a/src/cli/setup.rs b/src/cli/setup.rs index 8be399f..5bf0291 100644 --- a/src/cli/setup.rs +++ b/src/cli/setup.rs @@ -33,17 +33,11 @@ pub(super) fn run_identity_command( Ok(()) } -pub(super) fn run_folder_command( - appa: &AppaService, - command: FolderCommand, -) -> anyhow::Result<()> { +pub(super) fn run_folder_command(appa: &AppaService, command: FolderCommand) -> anyhow::Result<()> { match command { FolderCommand::Template { path } => { appa.export_folder_inventory(&path)?; - println!( - "Wrote local folder inventory to {}.", - path.display() - ); + println!("Wrote local folder inventory to {}.", path.display()); } FolderCommand::Validate { path } => { appa.validate_folder_inventory(&path)?; diff --git a/src/config.rs b/src/config.rs index 021d095..2aeb93f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,9 @@ //! Non-secret local folder inventory used for bulk import. -use std::{fs, path::{Path, PathBuf}}; +use std::{ + fs, + path::{Path, PathBuf}, +}; use serde::{Deserialize, Serialize}; diff --git a/src/daemon.rs b/src/daemon.rs index 0b96fe3..4144efd 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -103,6 +103,9 @@ async fn handle_command( Command::RegisterFolder { folder_path } => service .register_folder(&folder_path) .map(|_| Response::Success), + Command::ImportFolderInventory { inventory_path } => service + .import_folder_inventory(&inventory_path) + .map(|folders| Response::Count(folders.len())), Command::JoinFolder { folder_path, invitation, diff --git a/src/ipc.rs b/src/ipc.rs index d4c16b9..82670d9 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -33,6 +33,9 @@ pub(crate) enum Command { RegisterFolder { folder_path: PathBuf, }, + ImportFolderInventory { + inventory_path: PathBuf, + }, JoinFolder { folder_path: PathBuf, invitation: String, @@ -120,6 +123,16 @@ impl DaemonClient { .await } + pub async fn import_folder_inventory(&self, inventory_path: PathBuf) -> anyhow::Result { + match self + .request(Command::ImportFolderInventory { inventory_path }) + .await? + { + Response::Count(count) => Ok(count), + response => unexpected_response(response), + } + } + pub async fn join_folder( &self, folder_path: PathBuf, -- 2.51.2