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