app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 6091f15ce2b48e4146d17ebc465d253937db9569
parent 9701a78ac02d186d736cbe5414120a2192298fd7
Author: triesap <tyson@radroots.org>
Date:   Thu,  9 Apr 2026 04:14:12 +0000

app: add shared runtime path adapter

Diffstat:
MCargo.lock | 1+
Mcrates/android/src/storage.rs | 24+++---------------------
Mcrates/core/Cargo.toml | 1+
Mcrates/core/src/lib.rs | 5+++++
Acrates/core/src/storage_paths.rs | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/desktop/src/main.rs | 36+++++++-----------------------------
Mcrates/ios/src/storage.rs | 25++++---------------------
7 files changed, 143 insertions(+), 71 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3083,6 +3083,7 @@ dependencies = [ "eframe", "egui", "radroots-app-test-support", + "radroots-runtime-paths", "zeroize", ] diff --git a/crates/android/src/storage.rs b/crates/android/src/storage.rs @@ -2,36 +2,18 @@ use crate::security::{ANDROID_NOSTR_SERVICE, resolve_radroots_base_root}; #[cfg(target_os = "android")] use crate::vault::RadrootsAndroidKeystoreVault; +use radroots_app_core::mobile_native_app_storage_layout; #[cfg(target_os = "android")] use radroots_nostr_accounts::prelude::{ RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore, }; -use radroots_runtime_paths::{ - RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, - RadrootsPaths, RadrootsPlatform, RadrootsRuntimeNamespace, -}; +use radroots_runtime_paths::{RadrootsPaths, RadrootsPlatform}; use std::path::{Path, PathBuf}; #[cfg(target_os = "android")] use std::sync::Arc; -fn app_namespace() -> Result<RadrootsRuntimeNamespace, String> { - RadrootsRuntimeNamespace::app("app") - .map_err(|source| format!("failed to resolve android app namespace: {source}")) -} - fn app_paths_from_base_root(base_root: &Path) -> Result<RadrootsPaths, String> { - let resolver = RadrootsPathResolver::new( - RadrootsPlatform::Android, - RadrootsHostEnvironment::default(), - ); - let namespace = app_namespace()?; - resolver - .resolve( - RadrootsPathProfile::MobileNative, - &RadrootsPathOverrides::mobile(RadrootsPaths::from_base_root(base_root)), - ) - .map(|roots| roots.namespaced(&namespace)) - .map_err(|source| format!("failed to resolve android mobile-native roots: {source}")) + Ok(mobile_native_app_storage_layout(RadrootsPlatform::Android, base_root)?.app_paths) } #[cfg(target_os = "android")] diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml @@ -16,6 +16,7 @@ workspace = true [dependencies] eframe.workspace = true egui.workspace = true +radroots-runtime-paths.workspace = true zeroize.workspace = true [dev-dependencies] diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs @@ -10,6 +10,7 @@ mod location_resolver; mod offline_geocoder; mod remote_signer; mod secret_keys; +mod storage_paths; pub const APP_NAME: &str = "Rad Roots"; @@ -28,6 +29,10 @@ pub use remote_signer::{ RadrootsRemoteSignerSignedNote, }; pub use secret_keys::{RadrootsSecretImportMode, RadrootsSecretImportRequest}; +pub use storage_paths::{ + RadrootsAppStorageLayout, interactive_user_app_storage_layout_with_resolver, + mobile_native_app_storage_layout, +}; use home_location_tools::HomeLocationTools; diff --git a/crates/core/src/storage_paths.rs b/crates/core/src/storage_paths.rs @@ -0,0 +1,122 @@ +use std::path::{Path, PathBuf}; + +use radroots_runtime_paths::{ + RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, + RadrootsPaths, RadrootsPlatform, RadrootsRuntimeNamespace, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsAppStorageLayout { + pub runtime_root: PathBuf, + pub app_paths: RadrootsPaths, +} + +fn app_namespace() -> Result<RadrootsRuntimeNamespace, String> { + RadrootsRuntimeNamespace::app("app") + .map_err(|source| format!("failed to resolve app runtime namespace: {source}")) +} + +fn runtime_root_from_paths(roots: &RadrootsPaths) -> Result<PathBuf, String> { + roots + .config + .parent() + .map(Path::to_path_buf) + .ok_or_else(|| "resolved app config root had no parent".to_owned()) +} + +pub fn interactive_user_app_storage_layout_with_resolver( + resolver: &RadrootsPathResolver, +) -> Result<RadrootsAppStorageLayout, String> { + let roots = resolver + .resolve( + RadrootsPathProfile::InteractiveUser, + &RadrootsPathOverrides::default(), + ) + .map_err(|source| format!("failed to resolve app interactive-user roots: {source}"))?; + let namespace = app_namespace()?; + Ok(RadrootsAppStorageLayout { + runtime_root: runtime_root_from_paths(&roots)?, + app_paths: roots.namespaced(&namespace), + }) +} + +pub fn mobile_native_app_storage_layout( + platform: RadrootsPlatform, + base_root: &Path, +) -> Result<RadrootsAppStorageLayout, String> { + let resolver = RadrootsPathResolver::new(platform, RadrootsHostEnvironment::default()); + let roots = resolver + .resolve( + RadrootsPathProfile::MobileNative, + &RadrootsPathOverrides::mobile(RadrootsPaths::from_base_root(base_root)), + ) + .map_err(|source| format!("failed to resolve app mobile-native roots: {source}"))?; + let namespace = app_namespace()?; + Ok(RadrootsAppStorageLayout { + runtime_root: runtime_root_from_paths(&roots)?, + app_paths: roots.namespaced(&namespace), + }) +} + +#[cfg(test)] +mod tests { + use super::{ + interactive_user_app_storage_layout_with_resolver, mobile_native_app_storage_layout, + }; + use radroots_runtime_paths::{RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform}; + use std::path::PathBuf; + + #[test] + fn interactive_user_layout_keeps_runtime_root_and_namespaced_paths() { + let resolver = RadrootsPathResolver::new( + RadrootsPlatform::Linux, + RadrootsHostEnvironment { + home_dir: Some(PathBuf::from("/home/treesap")), + ..RadrootsHostEnvironment::default() + }, + ); + + let layout = + interactive_user_app_storage_layout_with_resolver(&resolver).expect("app layout"); + + assert_eq!( + layout.runtime_root, + PathBuf::from("/home/treesap/.radroots") + ); + assert_eq!( + layout.app_paths.data, + PathBuf::from("/home/treesap/.radroots/data/apps/app") + ); + assert_eq!( + layout.app_paths.logs, + PathBuf::from("/home/treesap/.radroots/logs/apps/app") + ); + } + + #[test] + fn mobile_native_layout_keeps_explicit_runtime_root_and_namespaced_paths() { + let base_root = PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots"); + + let layout = + mobile_native_app_storage_layout(RadrootsPlatform::Android, base_root.as_path()) + .expect("mobile layout"); + + assert_eq!(layout.runtime_root, base_root); + assert_eq!( + layout.app_paths.config, + PathBuf::from( + "/data/user/0/org.radroots.app.android/no_backup/RadRoots/config/apps/app" + ) + ); + assert_eq!( + layout.app_paths.data, + PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app") + ); + assert_eq!( + layout.app_paths.secrets, + PathBuf::from( + "/data/user/0/org.radroots.app.android/no_backup/RadRoots/secrets/apps/app" + ) + ); + } +} diff --git a/crates/desktop/src/main.rs b/crates/desktop/src/main.rs @@ -15,7 +15,7 @@ use radroots_app_core::{ RadrootsLocationReverseOptions, RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, RadrootsResolvedLocation, RadrootsReverseLocationLookupResult, RadrootsSecretImportMode, RadrootsSecretImportRequest, - SetupActionState, + SetupActionState, interactive_user_app_storage_layout_with_resolver, }; #[cfg(target_os = "macos")] use radroots_identity::RadrootsIdentity; @@ -23,10 +23,7 @@ use radroots_identity::RadrootsIdentity; use radroots_nostr_accounts::prelude::{ RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore, RadrootsNostrSelectedAccountStatus, }; -use radroots_runtime_paths::{ - RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsPaths, - RadrootsRuntimeNamespace, -}; +use radroots_runtime_paths::{RadrootsPathResolver, RadrootsPaths}; use std::path::{Path, PathBuf}; use std::sync::Arc; #[cfg(target_os = "macos")] @@ -79,25 +76,8 @@ struct DesktopBackend { } impl DesktopBackend { - fn app_namespace() -> Result<RadrootsRuntimeNamespace, String> { - RadrootsRuntimeNamespace::app("app") - .map_err(|source| format!("failed to resolve desktop app namespace: {source}")) - } - - fn interactive_user_roots_with_resolver( - resolver: &RadrootsPathResolver, - ) -> Result<RadrootsPaths, String> { - resolver - .resolve( - RadrootsPathProfile::InteractiveUser, - &RadrootsPathOverrides::default(), - ) - .map_err(|source| format!("failed to resolve desktop interactive-user roots: {source}")) - } - fn app_paths_with_resolver(resolver: &RadrootsPathResolver) -> Result<RadrootsPaths, String> { - let namespace = Self::app_namespace()?; - Ok(Self::interactive_user_roots_with_resolver(resolver)?.namespaced(&namespace)) + Ok(interactive_user_app_storage_layout_with_resolver(resolver)?.app_paths) } fn new() -> Self { @@ -131,12 +111,10 @@ impl DesktopBackend { } fn radroots_root() -> Result<PathBuf, String> { - let roots = Self::interactive_user_roots_with_resolver(&RadrootsPathResolver::current())?; - roots - .config - .parent() - .map(Path::to_path_buf) - .ok_or_else(|| "desktop interactive-user config root had no parent".to_owned()) + Ok( + interactive_user_app_storage_layout_with_resolver(&RadrootsPathResolver::current())? + .runtime_root, + ) } fn app_data_root() -> Result<PathBuf, String> { diff --git a/crates/ios/src/storage.rs b/crates/ios/src/storage.rs @@ -1,22 +1,15 @@ #[cfg(target_os = "ios")] use radroots_app_apple_security::{APPLE_NOSTR_SERVICE, RadrootsAppleKeychainVault}; +use radroots_app_core::mobile_native_app_storage_layout; #[cfg(target_os = "ios")] use radroots_nostr_accounts::prelude::{ RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore, }; -use radroots_runtime_paths::{ - RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, - RadrootsPaths, RadrootsPlatform, RadrootsRuntimeNamespace, -}; +use radroots_runtime_paths::{RadrootsPaths, RadrootsPlatform}; use std::path::{Path, PathBuf}; #[cfg(target_os = "ios")] use std::sync::Arc; -fn app_namespace() -> Result<RadrootsRuntimeNamespace, String> { - RadrootsRuntimeNamespace::app("app") - .map_err(|source| format!("failed to resolve ios app namespace: {source}")) -} - fn mobile_base_root_from_home(home: &Path) -> PathBuf { home.join("Library") .join("Application Support") @@ -24,18 +17,8 @@ fn mobile_base_root_from_home(home: &Path) -> PathBuf { } fn app_paths_from_home(home: &Path) -> Result<RadrootsPaths, String> { - let resolver = - RadrootsPathResolver::new(RadrootsPlatform::Ios, RadrootsHostEnvironment::default()); - let namespace = app_namespace()?; - resolver - .resolve( - RadrootsPathProfile::MobileNative, - &RadrootsPathOverrides::mobile(RadrootsPaths::from_base_root( - mobile_base_root_from_home(home), - )), - ) - .map(|roots| roots.namespaced(&namespace)) - .map_err(|source| format!("failed to resolve ios mobile-native roots: {source}")) + let base_root = mobile_base_root_from_home(home); + Ok(mobile_native_app_storage_layout(RadrootsPlatform::Ios, base_root.as_path())?.app_paths) } #[cfg(target_os = "ios")]