paths.rs (15329B)
1 use std::path::{Path, PathBuf}; 2 3 use radroots_runtime_paths::{ 4 RadrootsPathProfile, RadrootsPathResolver, RadrootsRuntimePathSelection, 5 }; 6 use serde::{Deserialize, Serialize}; 7 8 use crate::{ 9 config::{ 10 MycConfig, MycIdentityBackend, MycIdentitySourceSpec, config_parse_error, 11 parse_optional_path_env, 12 }, 13 error::MycError, 14 }; 15 16 pub const DEFAULT_ENV_PATH: &str = "config.env"; 17 const DEFAULT_STATE_DIR_NAME: &str = "state"; 18 const DEFAULT_CUSTODY_DIR_NAME: &str = "custody"; 19 const DEFAULT_SIGNER_IDENTITY_FILE_NAME: &str = "signer-identity.json"; 20 const DEFAULT_USER_IDENTITY_FILE_NAME: &str = "user-identity.json"; 21 const DEFAULT_DISCOVERY_APP_IDENTITY_FILE_NAME: &str = "discovery-app-identity.json"; 22 const DEFAULT_SIGNER_MANAGED_ACCOUNT_FILE_NAME: &str = "signer-accounts.json"; 23 const DEFAULT_USER_MANAGED_ACCOUNT_FILE_NAME: &str = "user-accounts.json"; 24 const DEFAULT_DISCOVERY_MANAGED_ACCOUNT_FILE_NAME: &str = "discovery-accounts.json"; 25 const DEFAULT_DISCOVERY_PUBLIC_DIR_NAME: &str = "public"; 26 const DEFAULT_DISCOVERY_NIP05_RELATIVE_PATH: &str = ".well-known/nostr.json"; 27 const MYC_PATHS_PROFILE_ENV: &str = "MYC_PATHS_PROFILE"; 28 const MYC_PATHS_REPO_LOCAL_ROOT_ENV: &str = "MYC_PATHS_REPO_LOCAL_ROOT"; 29 30 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 31 #[serde(default, deny_unknown_fields)] 32 pub struct MycPathsConfig { 33 pub profile: MycPathProfile, 34 pub repo_local_root: Option<PathBuf>, 35 pub config_env_path: PathBuf, 36 pub run_dir: PathBuf, 37 pub state_dir: PathBuf, 38 pub signer_identity_backend: MycIdentityBackend, 39 pub signer_identity_path: PathBuf, 40 pub signer_identity_keyring_account_id: Option<String>, 41 pub signer_identity_keyring_service_name: String, 42 pub signer_identity_profile_path: Option<PathBuf>, 43 pub user_identity_backend: MycIdentityBackend, 44 pub user_identity_path: PathBuf, 45 pub user_identity_keyring_account_id: Option<String>, 46 pub user_identity_keyring_service_name: String, 47 pub user_identity_profile_path: Option<PathBuf>, 48 } 49 50 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 51 #[serde(rename_all = "snake_case")] 52 pub enum MycPathProfile { 53 InteractiveUser, 54 ServiceHost, 55 RepoLocal, 56 } 57 58 #[derive(Debug, Clone, PartialEq, Eq)] 59 struct MycResolvedRuntimePaths { 60 config_env_path: PathBuf, 61 logs_dir: PathBuf, 62 run_dir: PathBuf, 63 state_dir: PathBuf, 64 signer_identity_path: PathBuf, 65 user_identity_path: PathBuf, 66 signer_managed_account_path: PathBuf, 67 user_managed_account_path: PathBuf, 68 discovery_app_identity_path: PathBuf, 69 discovery_managed_account_path: PathBuf, 70 discovery_nip05_output_path: PathBuf, 71 } 72 73 #[derive(Debug, Clone, Default, PartialEq, Eq)] 74 pub(crate) struct MycPathOverrideFlags { 75 pub(crate) logging_output_dir: bool, 76 pub(crate) state_dir: bool, 77 pub(crate) signer_identity_path: bool, 78 pub(crate) user_identity_path: bool, 79 pub(crate) discovery_app_identity_path: bool, 80 pub(crate) discovery_nip05_output_path: bool, 81 } 82 83 impl Default for MycPathsConfig { 84 fn default() -> Self { 85 Self::default_with_path_selection( 86 &RadrootsPathResolver::current(), 87 MycPathProfile::InteractiveUser, 88 None, 89 ) 90 .expect("current process should resolve myc runtime paths") 91 } 92 } 93 94 impl Default for MycPathProfile { 95 fn default() -> Self { 96 Self::InteractiveUser 97 } 98 } 99 100 impl MycPathProfile { 101 pub fn as_str(self) -> &'static str { 102 match self { 103 Self::InteractiveUser => "interactive_user", 104 Self::ServiceHost => "service_host", 105 Self::RepoLocal => "repo_local", 106 } 107 } 108 109 fn into_radroots_profile(self) -> RadrootsPathProfile { 110 match self { 111 Self::InteractiveUser => RadrootsPathProfile::InteractiveUser, 112 Self::ServiceHost => RadrootsPathProfile::ServiceHost, 113 Self::RepoLocal => RadrootsPathProfile::RepoLocal, 114 } 115 } 116 } 117 118 impl MycResolvedRuntimePaths { 119 fn resolve( 120 resolver: &RadrootsPathResolver, 121 profile: MycPathProfile, 122 repo_local_root: Option<&Path>, 123 ) -> Result<Self, MycError> { 124 let selection = RadrootsRuntimePathSelection::caller( 125 profile.into_radroots_profile(), 126 repo_local_root.map(Path::to_path_buf), 127 ); 128 let namespaced = selection 129 .resolve_service_roots( 130 resolver, 131 "myc", 132 MYC_PATHS_PROFILE_ENV, 133 MYC_PATHS_REPO_LOCAL_ROOT_ENV, 134 ) 135 .map_err(|error| { 136 MycError::InvalidConfig(format!("resolve myc runtime paths: {error}")) 137 })?; 138 let custody_dir = namespaced.data.join(DEFAULT_CUSTODY_DIR_NAME); 139 Ok(Self { 140 config_env_path: namespaced.config.join(DEFAULT_ENV_PATH), 141 logs_dir: namespaced.logs, 142 run_dir: namespaced.run, 143 state_dir: namespaced.data.join(DEFAULT_STATE_DIR_NAME), 144 signer_identity_path: namespaced.secrets.join(DEFAULT_SIGNER_IDENTITY_FILE_NAME), 145 user_identity_path: namespaced.secrets.join(DEFAULT_USER_IDENTITY_FILE_NAME), 146 signer_managed_account_path: custody_dir.join(DEFAULT_SIGNER_MANAGED_ACCOUNT_FILE_NAME), 147 user_managed_account_path: custody_dir.join(DEFAULT_USER_MANAGED_ACCOUNT_FILE_NAME), 148 discovery_app_identity_path: namespaced 149 .secrets 150 .join(DEFAULT_DISCOVERY_APP_IDENTITY_FILE_NAME), 151 discovery_managed_account_path: custody_dir 152 .join(DEFAULT_DISCOVERY_MANAGED_ACCOUNT_FILE_NAME), 153 discovery_nip05_output_path: namespaced 154 .data 155 .join(DEFAULT_DISCOVERY_PUBLIC_DIR_NAME) 156 .join(DEFAULT_DISCOVERY_NIP05_RELATIVE_PATH), 157 }) 158 } 159 } 160 161 impl MycPathsConfig { 162 pub(crate) fn default_with_path_selection( 163 resolver: &RadrootsPathResolver, 164 profile: MycPathProfile, 165 repo_local_root: Option<&Path>, 166 ) -> Result<Self, MycError> { 167 let resolved = MycResolvedRuntimePaths::resolve(resolver, profile, repo_local_root)?; 168 Ok(Self { 169 profile, 170 repo_local_root: repo_local_root.map(Path::to_path_buf), 171 config_env_path: resolved.config_env_path, 172 run_dir: resolved.run_dir, 173 state_dir: resolved.state_dir, 174 signer_identity_backend: MycIdentityBackend::EncryptedFile, 175 signer_identity_path: resolved.signer_identity_path, 176 signer_identity_keyring_account_id: None, 177 signer_identity_keyring_service_name: "org.radroots.myc.signer".to_owned(), 178 signer_identity_profile_path: None, 179 user_identity_backend: MycIdentityBackend::EncryptedFile, 180 user_identity_path: resolved.user_identity_path, 181 user_identity_keyring_account_id: None, 182 user_identity_keyring_service_name: "org.radroots.myc.user".to_owned(), 183 user_identity_profile_path: None, 184 }) 185 } 186 187 pub fn signer_identity_source(&self) -> MycIdentitySourceSpec { 188 MycIdentitySourceSpec { 189 backend: self.signer_identity_backend, 190 path: match self.signer_identity_backend { 191 MycIdentityBackend::EncryptedFile 192 | MycIdentityBackend::PlaintextFile 193 | MycIdentityBackend::ManagedAccount 194 | MycIdentityBackend::ExternalCommand => Some(self.signer_identity_path.clone()), 195 MycIdentityBackend::HostVault => None, 196 }, 197 keyring_account_id: match self.signer_identity_backend { 198 MycIdentityBackend::EncryptedFile 199 | MycIdentityBackend::PlaintextFile 200 | MycIdentityBackend::ManagedAccount 201 | MycIdentityBackend::ExternalCommand => None, 202 MycIdentityBackend::HostVault => self.signer_identity_keyring_account_id.clone(), 203 }, 204 keyring_service_name: match self.signer_identity_backend { 205 MycIdentityBackend::EncryptedFile 206 | MycIdentityBackend::PlaintextFile 207 | MycIdentityBackend::ExternalCommand => None, 208 MycIdentityBackend::HostVault | MycIdentityBackend::ManagedAccount => { 209 Some(self.signer_identity_keyring_service_name.clone()) 210 } 211 }, 212 profile_path: match self.signer_identity_backend { 213 MycIdentityBackend::EncryptedFile 214 | MycIdentityBackend::PlaintextFile 215 | MycIdentityBackend::ManagedAccount 216 | MycIdentityBackend::ExternalCommand => None, 217 MycIdentityBackend::HostVault => self.signer_identity_profile_path.clone(), 218 }, 219 } 220 } 221 222 pub fn user_identity_source(&self) -> MycIdentitySourceSpec { 223 MycIdentitySourceSpec { 224 backend: self.user_identity_backend, 225 path: match self.user_identity_backend { 226 MycIdentityBackend::EncryptedFile 227 | MycIdentityBackend::PlaintextFile 228 | MycIdentityBackend::ManagedAccount 229 | MycIdentityBackend::ExternalCommand => Some(self.user_identity_path.clone()), 230 MycIdentityBackend::HostVault => None, 231 }, 232 keyring_account_id: match self.user_identity_backend { 233 MycIdentityBackend::EncryptedFile 234 | MycIdentityBackend::PlaintextFile 235 | MycIdentityBackend::ManagedAccount 236 | MycIdentityBackend::ExternalCommand => None, 237 MycIdentityBackend::HostVault => self.user_identity_keyring_account_id.clone(), 238 }, 239 keyring_service_name: match self.user_identity_backend { 240 MycIdentityBackend::EncryptedFile 241 | MycIdentityBackend::PlaintextFile 242 | MycIdentityBackend::ExternalCommand => None, 243 MycIdentityBackend::HostVault | MycIdentityBackend::ManagedAccount => { 244 Some(self.user_identity_keyring_service_name.clone()) 245 } 246 }, 247 profile_path: match self.user_identity_backend { 248 MycIdentityBackend::EncryptedFile 249 | MycIdentityBackend::PlaintextFile 250 | MycIdentityBackend::ManagedAccount 251 | MycIdentityBackend::ExternalCommand => None, 252 MycIdentityBackend::HostVault => self.user_identity_profile_path.clone(), 253 }, 254 } 255 } 256 } 257 258 pub(crate) fn process_path_selection() -> Result<(MycPathProfile, Option<PathBuf>), MycError> { 259 let selection = RadrootsRuntimePathSelection::from_env( 260 MYC_PATHS_PROFILE_ENV, 261 MYC_PATHS_REPO_LOCAL_ROOT_ENV, 262 RadrootsPathProfile::InteractiveUser, 263 ) 264 .map_err(|error| MycError::InvalidConfig(error.to_string()))?; 265 Ok(( 266 from_radroots_profile(selection.profile), 267 selection.repo_local_root, 268 )) 269 } 270 271 fn from_radroots_profile(profile: RadrootsPathProfile) -> MycPathProfile { 272 match profile { 273 RadrootsPathProfile::InteractiveUser => MycPathProfile::InteractiveUser, 274 RadrootsPathProfile::ServiceHost => MycPathProfile::ServiceHost, 275 RadrootsPathProfile::RepoLocal => MycPathProfile::RepoLocal, 276 RadrootsPathProfile::MobileNative => MycPathProfile::InteractiveUser, 277 } 278 } 279 280 pub(crate) fn default_env_path_with_path_selection( 281 resolver: &RadrootsPathResolver, 282 profile: MycPathProfile, 283 repo_local_root: Option<&Path>, 284 ) -> Result<PathBuf, MycError> { 285 Ok(MycResolvedRuntimePaths::resolve(resolver, profile, repo_local_root)?.config_env_path) 286 } 287 288 pub(crate) fn apply_path_defaults( 289 config: &mut MycConfig, 290 resolver: &RadrootsPathResolver, 291 overrides: &MycPathOverrideFlags, 292 ) -> Result<(), MycError> { 293 let resolved = MycResolvedRuntimePaths::resolve( 294 resolver, 295 config.paths.profile, 296 config.paths.repo_local_root.as_deref(), 297 )?; 298 config.paths.config_env_path = resolved.config_env_path; 299 config.paths.run_dir = resolved.run_dir; 300 if !overrides.logging_output_dir { 301 config.logging.output_dir = Some(resolved.logs_dir); 302 } 303 if !overrides.state_dir { 304 config.paths.state_dir = resolved.state_dir; 305 } 306 if !overrides.signer_identity_path { 307 config.paths.signer_identity_path = match config.paths.signer_identity_backend { 308 MycIdentityBackend::EncryptedFile | MycIdentityBackend::PlaintextFile => { 309 resolved.signer_identity_path 310 } 311 MycIdentityBackend::ManagedAccount => resolved.signer_managed_account_path, 312 MycIdentityBackend::HostVault => PathBuf::new(), 313 MycIdentityBackend::ExternalCommand => PathBuf::new(), 314 }; 315 } 316 if !overrides.user_identity_path { 317 config.paths.user_identity_path = match config.paths.user_identity_backend { 318 MycIdentityBackend::EncryptedFile | MycIdentityBackend::PlaintextFile => { 319 resolved.user_identity_path 320 } 321 MycIdentityBackend::ManagedAccount => resolved.user_managed_account_path, 322 MycIdentityBackend::HostVault => PathBuf::new(), 323 MycIdentityBackend::ExternalCommand => PathBuf::new(), 324 }; 325 } 326 if !overrides.discovery_app_identity_path { 327 config.discovery.app_identity_path = match config.discovery.app_identity_backend { 328 Some(MycIdentityBackend::EncryptedFile) | Some(MycIdentityBackend::PlaintextFile) => { 329 Some(resolved.discovery_app_identity_path) 330 } 331 Some(MycIdentityBackend::ManagedAccount) => { 332 Some(resolved.discovery_managed_account_path) 333 } 334 Some(MycIdentityBackend::HostVault) | None => None, 335 Some(MycIdentityBackend::ExternalCommand) => None, 336 }; 337 } 338 if !overrides.discovery_nip05_output_path { 339 config.discovery.nip05_output_path = Some(resolved.discovery_nip05_output_path); 340 } 341 Ok(()) 342 } 343 344 pub(crate) fn path_selection_from_entries( 345 entries: &[(String, String, usize)], 346 path: &Path, 347 ) -> Result<(MycPathProfile, Option<PathBuf>), MycError> { 348 let mut profile = MycPathProfile::InteractiveUser; 349 let mut repo_local_root = None; 350 for (key, value, line_number) in entries { 351 match key.as_str() { 352 MYC_PATHS_PROFILE_ENV => { 353 profile = parse_path_profile_env(key, value, path, *line_number)?; 354 } 355 MYC_PATHS_REPO_LOCAL_ROOT_ENV => { 356 repo_local_root = parse_optional_path_env(value); 357 } 358 _ => {} 359 } 360 } 361 Ok((profile, repo_local_root)) 362 } 363 364 pub(crate) fn parse_path_profile_env( 365 key: &str, 366 value: &str, 367 path: &Path, 368 line_number: usize, 369 ) -> Result<MycPathProfile, MycError> { 370 match value { 371 "interactive_user" => Ok(MycPathProfile::InteractiveUser), 372 "service_host" => Ok(MycPathProfile::ServiceHost), 373 "repo_local" => Ok(MycPathProfile::RepoLocal), 374 _ => Err(config_parse_error( 375 path, 376 line_number, 377 format!("{key} must be `interactive_user`, `service_host`, or `repo_local`"), 378 )), 379 } 380 }