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:
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();
+ }
}