app

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

commit d7ac6be9cd6bc558da2c77c20b1b16ef3382d9c2
parent c75542ae9ee228704a41cbbc5b8fc4aecd648f80
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Mar 2026 00:24:24 +0000

ios: prune stale offline geocoder revisions

- keep only the active staged ios geocoder revision after successful initialization
- preserve non-blocking startup by treating revision cleanup as best-effort work
- add focused ios tests for stale revision pruning alongside the existing staging coverage
- leave the current ios geocoder revision contract unchanged in this slice

Diffstat:
Mcrates/ios/src/offline_geocoder.rs | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
1 file changed, 91 insertions(+), 19 deletions(-)

diff --git a/crates/ios/src/offline_geocoder.rs b/crates/ios/src/offline_geocoder.rs @@ -1,8 +1,6 @@ #![cfg_attr(not(target_os = "ios"), allow(dead_code))] -use radroots_app_core::{ - RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, -}; +use radroots_app_core::{RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind}; use radroots_geocoder::Geocoder; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -83,25 +81,29 @@ fn initialize_offline_geocoder_inner( if !source_path.is_file() { return Err(( RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - format!("ios bundled geocoder asset missing at {}", source_path.display()), + format!( + "ios bundled geocoder asset missing at {}", + source_path.display() + ), )); } - let staged_path = staged_db_path(app_data_root, asset_revision(source_path.as_path())?); + let revision = asset_revision(source_path.as_path())?; + let staged_path = staged_db_path(app_data_root, revision.as_str()); stage_bundled_asset(source_path.as_path(), staged_path.as_path()).map_err(|debug_message| { ( RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, debug_message, ) })?; - Geocoder::open_path(staged_path.as_path()) - .map(|_| ()) - .map_err(|source| { - ( - RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, - format!("failed to open staged ios geocoder db: {source}"), - ) - }) + Geocoder::open_path(staged_path.as_path()).map_err(|source| { + ( + RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + format!("failed to open staged ios geocoder db: {source}"), + ) + })?; + let _ = prune_stale_revisions(staged_geocoder_root(app_data_root), revision.as_str()); + Ok(()) } fn bundled_asset_path() -> Result<PathBuf, String> { @@ -137,9 +139,12 @@ fn asset_revision( Ok(format!("{:x}-{:x}", metadata.len(), modified.as_secs())) } -fn staged_db_path(app_data_root: &Path, revision: String) -> PathBuf { - app_data_root - .join("geocoder") +fn staged_geocoder_root(app_data_root: &Path) -> PathBuf { + app_data_root.join("geocoder") +} + +fn staged_db_path(app_data_root: &Path, revision: &str) -> PathBuf { + staged_geocoder_root(app_data_root) .join(revision) .join(GEOCODER_ASSET_FILENAME) } @@ -158,6 +163,45 @@ fn stage_bundled_asset(source_path: &Path, staged_path: &Path) -> Result<bool, S Ok(true) } +fn prune_stale_revisions(staged_root: PathBuf, active_revision: &str) -> Result<(), String> { + if !staged_root.is_dir() { + return Ok(()); + } + + for entry in std::fs::read_dir(staged_root.as_path()) + .map_err(|source| format!("failed to list ios geocoder revisions: {source}"))? + { + let entry = entry + .map_err(|source| format!("failed to read ios geocoder revision entry: {source}"))?; + if entry.file_name() == std::ffi::OsStr::new(active_revision) { + continue; + } + + let path = entry.path(); + if entry + .file_type() + .map_err(|source| format!("failed to inspect ios geocoder revision entry: {source}"))? + .is_dir() + { + std::fs::remove_dir_all(path.as_path()).map_err(|source| { + format!( + "failed to remove stale ios geocoder revision {}: {source}", + path.display() + ) + })?; + } else { + std::fs::remove_file(path.as_path()).map_err(|source| { + format!( + "failed to remove stale ios geocoder revision file {}: {source}", + path.display() + ) + })?; + } + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -170,7 +214,7 @@ mod tests { ); assert_eq!( - staged_db_path(app_data_root.as_path(), "abcd".to_owned()), + staged_db_path(app_data_root.as_path(), "abcd"), PathBuf::from( "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/app/ios/geocoder/abcd/geonames.db" ) @@ -188,8 +232,7 @@ mod tests { state, RadrootsOfflineGeocoderState::Unavailable { kind: RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - debug_message: "ios bundled geocoder asset missing at /tmp/geonames.db" - .to_owned(), + debug_message: "ios bundled geocoder asset missing at /tmp/geonames.db".to_owned(), } ); } @@ -218,4 +261,33 @@ mod tests { std::fs::remove_dir_all(temp_root.as_path()).unwrap(); } + + #[test] + fn prune_stale_revisions_keeps_active_revision_only() { + let temp_root = std::env::temp_dir().join(format!( + "radroots-ios-geocoder-prune-test-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let staged_root = temp_root.join("geocoder"); + let active_dir = staged_root.join("active"); + let stale_dir = staged_root.join("stale"); + let stale_file = staged_root.join("orphan.txt"); + + std::fs::create_dir_all(active_dir.as_path()).unwrap(); + std::fs::create_dir_all(stale_dir.as_path()).unwrap(); + std::fs::write(active_dir.join("geonames.db"), b"active").unwrap(); + std::fs::write(stale_dir.join("geonames.db"), b"stale").unwrap(); + std::fs::write(stale_file.as_path(), b"orphan").unwrap(); + + prune_stale_revisions(staged_root.clone(), "active").unwrap(); + + assert!(active_dir.exists()); + assert!(!stale_dir.exists()); + assert!(!stale_file.exists()); + + std::fs::remove_dir_all(temp_root.as_path()).unwrap(); + } }