From ebd03108d51c6a9c59331efda4d0537cea569d3b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 13:30:35 +0000 Subject: [PATCH] chore(workers): platform-aware default paths Replace hardcoded `/etc/willow/*.key` and `/var/lib/willow/storage.db` clap defaults in replay + storage bins with dirs::config_dir() / dirs::data_dir() lookups, falling back to historical Linux paths when no XDG dir resolves. Behaviour unchanged when --identity-path or --db-path passed (e.g. Docker images). Refs #324 --- Cargo.lock | 2 ++ crates/replay/Cargo.toml | 1 + crates/replay/src/main.rs | 40 +++++++++++++++++++++- crates/storage/Cargo.toml | 1 + crates/storage/src/main.rs | 68 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 109 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7273ddc7..954c769c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6208,6 +6208,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "dirs", "tokio", "tracing", "tracing-subscriber", @@ -6237,6 +6238,7 @@ dependencies = [ "anyhow", "bincode", "clap", + "dirs", "rusqlite", "serde", "tempfile", diff --git a/crates/replay/Cargo.toml b/crates/replay/Cargo.toml index 85e54421..0e648e25 100644 --- a/crates/replay/Cargo.toml +++ b/crates/replay/Cargo.toml @@ -13,6 +13,7 @@ willow-state = { path = "../state" } willow-identity = { path = "../identity" } willow-network = { path = "../network" } clap = { version = "4", features = ["derive"] } +dirs = "6" anyhow = { workspace = true } tracing = { workspace = true } tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/crates/replay/src/main.rs b/crates/replay/src/main.rs index cd066083..2014bd52 100644 --- a/crates/replay/src/main.rs +++ b/crates/replay/src/main.rs @@ -2,14 +2,28 @@ pub mod role; +use std::path::PathBuf; + use clap::Parser; use role::{ReplayConfig, ReplayRole}; +/// Compute the platform-aware default path for the replay identity key. +/// +/// Prefers the user's config dir (e.g. `$XDG_CONFIG_HOME/willow/replay.key` +/// on Linux, `~/Library/Application Support/willow/replay.key` on macOS), +/// falling back to `/etc/willow/replay.key` when no config dir is available +/// (matches the historical Linux deployment path used by the Docker images). +fn default_replay_key() -> PathBuf { + dirs::config_dir() + .map(|d| d.join("willow").join("replay.key")) + .unwrap_or_else(|| PathBuf::from("/etc/willow/replay.key")) +} + #[derive(Parser)] #[command(name = "willow-replay", about = "Willow replay worker node")] struct Cli { /// Path to the Ed25519 identity keypair file. - #[arg(long, default_value = "/etc/willow/replay.key")] + #[arg(long, default_value_t = default_replay_key().display().to_string())] identity_path: String, /// Iroh relay URL to connect through. @@ -93,3 +107,27 @@ async fn main() -> anyhow::Result<()> { willow_worker::runtime::run(Box::new(role), config, network).await } + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + #[test] + fn default_replay_key_targets_willow_subdir() { + let p = default_replay_key(); + assert_eq!(p.file_name().and_then(|s| s.to_str()), Some("replay.key")); + let parent = p.parent().expect("default path has parent"); + assert_eq!( + parent.file_name().and_then(|s| s.to_str()), + Some("willow"), + "expected willow/ as parent dir, got {parent:?}" + ); + assert!(p.is_absolute(), "default path must be absolute, got {p:?}"); + // Sanity: path contains at least the `willow/replay.key` tail. + assert!( + p.ends_with(Path::new("willow/replay.key")), + "expected path to end with willow/replay.key, got {p:?}" + ); + } +} diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index a6afc977..b6cb9f0c 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -15,6 +15,7 @@ willow-network = { path = "../network" } willow-common = { path = "../common" } rusqlite = { version = "0.31", features = ["bundled"] } clap = { version = "4", features = ["derive"] } +dirs = "6" anyhow = { workspace = true } tracing = { workspace = true } tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/crates/storage/src/main.rs b/crates/storage/src/main.rs index 6133dbdd..1d9a9ee8 100644 --- a/crates/storage/src/main.rs +++ b/crates/storage/src/main.rs @@ -3,15 +3,39 @@ pub mod role; pub mod store; +use std::path::PathBuf; + use clap::Parser; use role::StorageRole; use store::StorageEventStore; +/// Compute the platform-aware default path for the storage identity key. +/// +/// Prefers the user's config dir (e.g. `$XDG_CONFIG_HOME/willow/storage.key` +/// on Linux), falling back to `/etc/willow/storage.key` when no config dir +/// is available (matches the historical Linux deployment path). +fn default_storage_key() -> PathBuf { + dirs::config_dir() + .map(|d| d.join("willow").join("storage.key")) + .unwrap_or_else(|| PathBuf::from("/etc/willow/storage.key")) +} + +/// Compute the platform-aware default path for the storage SQLite database. +/// +/// Prefers the user's data dir (e.g. `$XDG_DATA_HOME/willow/storage.db` on +/// Linux), falling back to `/var/lib/willow/storage.db` when no data dir is +/// available (matches the historical Linux deployment path). +fn default_storage_db() -> PathBuf { + dirs::data_dir() + .map(|d| d.join("willow").join("storage.db")) + .unwrap_or_else(|| PathBuf::from("/var/lib/willow/storage.db")) +} + #[derive(Parser)] #[command(name = "willow-storage", about = "Willow storage worker node")] struct Cli { /// Path to the Ed25519 identity keypair file. - #[arg(long, default_value = "/etc/willow/storage.key")] + #[arg(long, default_value_t = default_storage_key().display().to_string())] identity_path: String, /// Iroh relay URL to connect through. @@ -19,7 +43,7 @@ struct Cli { relay_url: Option, /// Path to SQLite database. - #[arg(long, default_value = "/var/lib/willow/storage.db")] + #[arg(long, default_value_t = default_storage_db().display().to_string())] db_path: String, /// Active sync interval in seconds. @@ -89,3 +113,43 @@ async fn main() -> anyhow::Result<()> { willow_worker::runtime::run(Box::new(role), config, network).await } + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + #[test] + fn default_storage_key_targets_willow_subdir() { + let p = default_storage_key(); + assert_eq!(p.file_name().and_then(|s| s.to_str()), Some("storage.key")); + let parent = p.parent().expect("default path has parent"); + assert_eq!( + parent.file_name().and_then(|s| s.to_str()), + Some("willow"), + "expected willow/ as parent dir, got {parent:?}" + ); + assert!(p.is_absolute(), "default path must be absolute, got {p:?}"); + assert!( + p.ends_with(Path::new("willow/storage.key")), + "expected path to end with willow/storage.key, got {p:?}" + ); + } + + #[test] + fn default_storage_db_targets_willow_subdir() { + let p = default_storage_db(); + assert_eq!(p.file_name().and_then(|s| s.to_str()), Some("storage.db")); + let parent = p.parent().expect("default path has parent"); + assert_eq!( + parent.file_name().and_then(|s| s.to_str()), + Some("willow"), + "expected willow/ as parent dir, got {parent:?}" + ); + assert!(p.is_absolute(), "default path must be absolute, got {p:?}"); + assert!( + p.ends_with(Path::new("willow/storage.db")), + "expected path to end with willow/storage.db, got {p:?}" + ); + } +}