app

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

commit 2bf0680eb18353f242769b5906e90e3553f9c40b
parent 691342faa94152217cab9999f9b6315359365cc7
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Mar 2026 00:03:05 +0000

desktop: make offline geocoder staging idempotent

- version the staged desktop geocoder path from the bundled asset metadata instead of rewriting one fixed file
- reuse the current staged geocoder database when it already exists for the active bundled asset revision
- keep the desktop offline geocoder init non-blocking while still opening the staged database for verification
- add a desktop regression test that proves existing staged copies are not overwritten on startup

Diffstat:
Mcrates/desktop/src/offline_geocoder.rs | 71++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 64 insertions(+), 7 deletions(-)

diff --git a/crates/desktop/src/offline_geocoder.rs b/crates/desktop/src/offline_geocoder.rs @@ -5,6 +5,7 @@ use radroots_geocoder::Geocoder; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; +use std::time::UNIX_EPOCH; const GEOCODER_ASSET_FILENAME: &str = "geonames.db"; @@ -87,7 +88,7 @@ fn initialize_offline_geocoder_inner( )); } - let staged_path = staged_db_path(app_data_root); + let staged_path = staged_db_path(app_data_root, asset_revision(source_path.as_path())?); stage_runtime_asset(source_path.as_path(), staged_path.as_path()).map_err(|debug_message| { ( RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, @@ -113,32 +114,63 @@ fn runtime_asset_path() -> Result<PathBuf, String> { Ok(parent.join(GEOCODER_ASSET_FILENAME)) } -fn staged_db_path(app_data_root: &Path) -> PathBuf { - app_data_root.join("geocoder").join(GEOCODER_ASSET_FILENAME) +fn asset_revision( + source_path: &Path, +) -> Result<String, (RadrootsOfflineGeocoderUnavailableKind, String)> { + let metadata = std::fs::metadata(source_path).map_err(|source| { + ( + RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + format!("failed to read desktop geocoder asset metadata: {source}"), + ) + })?; + let modified = metadata.modified().map_err(|source| { + ( + RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + format!("failed to read desktop geocoder asset mtime: {source}"), + ) + })?; + let modified = modified.duration_since(UNIX_EPOCH).map_err(|source| { + ( + RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + format!("failed to normalize desktop geocoder asset mtime: {source}"), + ) + })?; + Ok(format!("{:x}-{:x}", metadata.len(), modified.as_secs())) } -fn stage_runtime_asset(source_path: &Path, staged_path: &Path) -> Result<(), String> { +fn staged_db_path(app_data_root: &Path, revision: String) -> PathBuf { + app_data_root + .join("geocoder") + .join(revision) + .join(GEOCODER_ASSET_FILENAME) +} + +fn stage_runtime_asset(source_path: &Path, staged_path: &Path) -> Result<bool, String> { let Some(parent) = staged_path.parent() else { return Err("staged desktop geocoder path did not have a parent directory".to_owned()); }; std::fs::create_dir_all(parent) .map_err(|source| format!("failed to create desktop geocoder directory: {source}"))?; + if staged_path.is_file() { + return Ok(false); + } std::fs::copy(source_path, staged_path) .map_err(|source| format!("failed to stage desktop geocoder asset: {source}"))?; - Ok(()) + Ok(true) } #[cfg(test)] mod tests { use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; #[test] fn staged_db_path_uses_app_geocoder_directory() { let app_data_root = PathBuf::from("/Users/example/.radroots/app/desktop"); assert_eq!( - staged_db_path(app_data_root.as_path()), - PathBuf::from("/Users/example/.radroots/app/desktop/geocoder/geonames.db") + staged_db_path(app_data_root.as_path(), "abcd".to_owned()), + PathBuf::from("/Users/example/.radroots/app/desktop/geocoder/abcd/geonames.db") ); } @@ -158,4 +190,29 @@ mod tests { } ); } + + #[test] + fn stage_runtime_asset_reuses_existing_staged_copy() { + let temp_root = std::env::temp_dir().join(format!( + "radroots-desktop-geocoder-test-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let source_path = temp_root.join("source.db"); + let staged_path = temp_root.join("staged").join("geonames.db"); + + std::fs::create_dir_all(temp_root.as_path()).unwrap(); + std::fs::write(source_path.as_path(), b"source").unwrap(); + std::fs::create_dir_all(staged_path.parent().unwrap()).unwrap(); + std::fs::write(staged_path.as_path(), b"existing").unwrap(); + + let copied = stage_runtime_asset(source_path.as_path(), staged_path.as_path()).unwrap(); + + assert!(!copied); + assert_eq!(std::fs::read(staged_path.as_path()).unwrap(), b"existing"); + + std::fs::remove_dir_all(temp_root.as_path()).unwrap(); + } }