Full DB backup: export all data to a file and restore it (swap DB on next launch)
This commit is contained in:
parent
75eca0e501
commit
682bee6e9a
6 changed files with 205 additions and 1 deletions
|
|
@ -25,6 +25,7 @@ class MainActivity : TauriActivity() {
|
|||
private var commandObserver: FileObserver? = null
|
||||
private var pendingSaveContent: String? = null
|
||||
private var pendingImageBytes: ByteArray? = null
|
||||
private var pendingDbFile: File? = null
|
||||
private var backWebView: WebView? = null
|
||||
|
||||
// Disable WryActivity's generated back handler: its mWebView.goBack() does NOT
|
||||
|
|
@ -78,6 +79,22 @@ class MainActivity : TauriActivity() {
|
|||
pendingImageBytes = null
|
||||
}
|
||||
|
||||
// SAF "create document" for the full DB backup — streams the staged file (can be tens of MB)
|
||||
// straight to the picked location instead of loading it into memory.
|
||||
private val saveDbLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/octet-stream")
|
||||
) { uri: Uri? ->
|
||||
val src = pendingDbFile
|
||||
if (uri != null && src != null && src.exists()) {
|
||||
try {
|
||||
contentResolver.openOutputStream(uri)?.use { out -> src.inputStream().use { it.copyTo(out, 1 shl 20) } }
|
||||
Log.d("BalDav", "DB exported to $uri (${src.length()} bytes)")
|
||||
src.delete()
|
||||
} catch (e: Exception) { Log.e("BalDav", "Failed to export DB", e) }
|
||||
}
|
||||
pendingDbFile = null
|
||||
}
|
||||
|
||||
// Share a PNG via the system chooser (write to cache, FileProvider content:// URI, ACTION_SEND).
|
||||
private fun shareImage(bytes: ByteArray, filename: String) {
|
||||
try {
|
||||
|
|
@ -127,6 +144,22 @@ class MainActivity : TauriActivity() {
|
|||
val appDataDir = filesDir.parentFile ?: filesDir
|
||||
Log.d("BalDav", "App data dir: ${appDataDir.absolutePath}")
|
||||
|
||||
// Restore-on-launch: a full-DB import stages pending_restore.db; swap it onto baldav.db NOW,
|
||||
// before the app opens the DB (tauri-plugin-sql opens it later, when db.js runs). Then clear
|
||||
// any stale journal so the restored file is the single source of truth.
|
||||
try {
|
||||
val pending = File(appDataDir, "pending_restore.db")
|
||||
if (pending.exists() && pending.length() > 0) {
|
||||
val dbFile = File(appDataDir, "baldav.db")
|
||||
pending.inputStream().use { i -> dbFile.outputStream().use { o -> i.copyTo(o, 1 shl 20) } }
|
||||
File(appDataDir, "baldav.db-journal").delete()
|
||||
File(appDataDir, "baldav.db-wal").delete()
|
||||
File(appDataDir, "baldav.db-shm").delete()
|
||||
pending.delete()
|
||||
Log.d("BalDav", "Restored DB from pending_restore.db (${dbFile.length()} bytes)")
|
||||
}
|
||||
} catch (e: Exception) { Log.e("BalDav", "DB restore failed", e) }
|
||||
|
||||
// Create command files if not exist (FileObserver needs them)
|
||||
val cmdFile = File(appDataDir, "gps_command.txt")
|
||||
if (!cmdFile.exists()) cmdFile.createNewFile()
|
||||
|
|
@ -515,6 +548,21 @@ class MainActivity : TauriActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
cmd.startsWith("exportdb:") -> {
|
||||
// exportdb:<filename> — SAF-save the DB copy Rust wrote to export_db.tmp.
|
||||
val filename = cmd.substringAfter("exportdb:").ifBlank { "baldav-backup.db" }
|
||||
val f = File(appDataDir, "export_db.tmp")
|
||||
if (f.exists()) { pendingDbFile = f; saveDbLauncher.launch(filename) }
|
||||
else Log.e("BalDav", "exportdb: export_db.tmp missing")
|
||||
}
|
||||
cmd == "restartapp" -> {
|
||||
// Relaunch the process so the staged DB (pending_restore.db) is swapped in onCreate.
|
||||
Log.d("BalDav", "Restarting app (DB restore)")
|
||||
val intent = packageManager.getLaunchIntentForPackage(packageName)
|
||||
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -624,6 +624,73 @@ fn read_file_base64(path: String) -> Result<String, String> {
|
|||
Ok(base64::engine::general_purpose::STANDARD.encode(&bytes))
|
||||
}
|
||||
|
||||
// Resolve the main baldav.db path (same fallbacks as rebuild_roads/get_road_points) and the
|
||||
// app root (where Kotlin's FileObserver watches). Root = data_dir/../.. on Android.
|
||||
fn db_and_root(app: &tauri::AppHandle) -> Result<(std::path::PathBuf, std::path::PathBuf), String> {
|
||||
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
let p1 = data_dir.join("../../baldav.db");
|
||||
let p2 = data_dir.join("baldav.db");
|
||||
let p3 = std::path::PathBuf::from(data_dir.to_str().unwrap_or("").replace("/files/dev.baldav.app", "")).join("baldav.db");
|
||||
let db = if p1.exists() { p1 } else if p2.exists() { p2.clone() } else { p3 };
|
||||
let root = data_dir.parent().and_then(|p| p.parent()).map(|p| p.to_path_buf()).unwrap_or(data_dir.clone());
|
||||
Ok((db, root))
|
||||
}
|
||||
|
||||
// Full backup: VACUUM the live DB into a clean copy in the app root, then tell Kotlin to
|
||||
// SAF-save it as `filename`. VACUUM INTO is consistent even while the app holds the DB open.
|
||||
#[tauri::command]
|
||||
fn export_db(app: tauri::AppHandle, filename: String) -> Result<String, String> {
|
||||
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
let (db_path, root) = db_and_root(&app)?;
|
||||
if !db_path.exists() { return Err("baldav.db не найдена".into()); }
|
||||
let out = root.join("export_db.tmp");
|
||||
let _ = std::fs::remove_file(&out);
|
||||
{
|
||||
let conn = rusqlite::Connection::open(&db_path).map_err(|e| format!("open: {e}"))?;
|
||||
conn.execute("VACUUM INTO ?1", rusqlite::params![out.to_str().unwrap_or("")]).map_err(|e| format!("vacuum: {e}"))?;
|
||||
}
|
||||
let size = std::fs::metadata(&out).map(|m| m.len()).unwrap_or(0);
|
||||
let cmd = format!("exportdb:{}", filename);
|
||||
let _ = std::fs::write(data_dir.join("gps_command.txt"), &cmd);
|
||||
let _ = std::fs::write(root.join("gps_command.txt"), &cmd);
|
||||
Ok(format!("{{\"size\":{}}}", size))
|
||||
}
|
||||
|
||||
// Full restore: validate the picked DB (picked_import.db) and stage it as pending_restore.db.
|
||||
// Kotlin swaps it onto baldav.db on the next launch, BEFORE the app opens the DB. Returns ok;
|
||||
// caller then restarts the app.
|
||||
#[tauri::command]
|
||||
fn import_db(app: tauri::AppHandle) -> Result<String, String> {
|
||||
let (_db, root) = db_and_root(&app)?;
|
||||
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
let picked = if root.join("picked_import.db").exists() { root.join("picked_import.db") }
|
||||
else { data_dir.join("picked_import.db") };
|
||||
if !picked.exists() { return Err("файл не выбран".into()); }
|
||||
{
|
||||
let conn = rusqlite::Connection::open(&picked).map_err(|e| format!("не открыть файл: {e}"))?;
|
||||
let ok: String = conn.query_row("PRAGMA quick_check", [], |r| r.get(0)).map_err(|e| format!("проверка: {e}"))?;
|
||||
if ok != "ok" { return Err(format!("база повреждена: {ok}")); }
|
||||
let has: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN ('gps_sessions','products','profile')",
|
||||
[], |r| r.get(0)).map_err(|e| format!("схема: {e}"))?;
|
||||
if has < 1 { return Err("это не похоже на базу BalDav".into()); }
|
||||
}
|
||||
let pending = root.join("pending_restore.db");
|
||||
std::fs::copy(&picked, &pending).map_err(|e| format!("подготовка: {e}"))?;
|
||||
let _ = std::fs::remove_file(&picked);
|
||||
Ok("staged".into())
|
||||
}
|
||||
|
||||
// Ask Kotlin to restart the app process (used after import_db to load the restored DB).
|
||||
#[tauri::command]
|
||||
fn restart_app(app: tauri::AppHandle) -> Result<String, String> {
|
||||
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
let (_db, root) = db_and_root(&app)?;
|
||||
let _ = std::fs::write(data_dir.join("gps_command.txt"), "restartapp");
|
||||
let _ = std::fs::write(root.join("gps_command.txt"), "restartapp");
|
||||
Ok("ok".into())
|
||||
}
|
||||
|
||||
// Read tiles from a SQLite file in batches for import into IndexedDB
|
||||
#[tauri::command]
|
||||
fn import_tiles(path: String, offset: u32, batch_size: u32) -> Result<String, String> {
|
||||
|
|
@ -1088,7 +1155,7 @@ pub fn run() {
|
|||
.add_migrations("sqlite:baldav.db", migrations)
|
||||
.build(),
|
||||
)
|
||||
.invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, import_tiles, download_file, get_points_filename, delete_points_file, gps_set_session_id, list_points_files, read_points_file, pick_file_open, get_picked_file, debug_native_paths, rebuild_roads, get_road_points, get_ride_graph, get_app_info, gps_service_command, gps_service_read_points, brouter_route, brouter_download_tile, brouter_download_status, brouter_list_tiles, save_file, export_image, read_file_base64, get_update_info, http_request, chzn_http, ble::ble_scan, ble::ble_scan_results, ble::ble_connect, ble::ble_disconnect, ble::ble_get_hr, ble::ble_is_connected, ble::ble_check_bluetooth, ble::ble_save_device, ble::ble_clear_hr])
|
||||
.invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, import_tiles, download_file, get_points_filename, delete_points_file, gps_set_session_id, list_points_files, read_points_file, pick_file_open, get_picked_file, debug_native_paths, rebuild_roads, get_road_points, get_ride_graph, get_app_info, gps_service_command, gps_service_read_points, brouter_route, brouter_download_tile, brouter_download_status, brouter_list_tiles, save_file, export_image, read_file_base64, export_db, import_db, restart_app, get_update_info, http_request, chzn_http, ble::ble_scan, ble::ble_scan_results, ble::ble_connect, ble::ble_disconnect, ble::ble_get_hr, ble::ble_is_connected, ble::ble_check_bluetooth, ble::ble_save_device, ble::ble_clear_hr])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue