Remove unused assets, refactor migration handling, and add crash reporting
- Deleted obsolete icons, layout files, and SVG assets. - Added `useAutoUpdate` hook for streamlined update logic. - Introduced `auto_migrate_electron` and migration commands for Electron-to-Tauri data migration. - Implemented `init_panic_hook` for improved crash reporting. - Updated `.gitignore` for Tauri build output and IDE-specific files.
This commit is contained in:
378
src-tauri/src/domains/migration/commands.rs
Normal file
378
src-tauri/src/domains/migration/commands.rs
Normal file
@@ -0,0 +1,378 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tauri::State;
|
||||
|
||||
use crate::crypto::key_manager;
|
||||
use crate::db::connection::DbManager;
|
||||
use crate::db::schema;
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::shared::session::SessionState;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportMigrationData {
|
||||
pub migration_file_path: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MigrationResult {
|
||||
pub success: bool,
|
||||
pub user_id: Option<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MigrationCheckResult {
|
||||
pub found: bool,
|
||||
pub user_id: Option<String>,
|
||||
pub has_db: bool,
|
||||
pub migration_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AutoMigrationResult {
|
||||
pub migrated: bool,
|
||||
pub user_id: Option<String>,
|
||||
pub token_migrated: bool,
|
||||
pub key_migrated: bool,
|
||||
pub pin_migrated: bool,
|
||||
pub db_migrated: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ElectronMigrationFile {
|
||||
version: u32,
|
||||
user_id: String,
|
||||
encryption_key: String,
|
||||
pin_hash: Option<String>,
|
||||
db_source: String,
|
||||
}
|
||||
|
||||
// ─── Electron safeStorage vault format ───────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
struct ElectronVault {
|
||||
#[serde(default)]
|
||||
token: Option<String>,
|
||||
#[serde(flatten)]
|
||||
extra: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
// ─── Auto-migration from Electron ────────────────────────────────────────────
|
||||
|
||||
fn electron_data_paths() -> Vec<PathBuf> {
|
||||
let home = dirs_next::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let mut paths = Vec::new();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let app_support = home.join("Library").join("Application Support");
|
||||
paths.push(app_support.join("ERitors Scribe"));
|
||||
paths.push(app_support.join("Electron"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Some(appdata) = dirs_next::data_dir() {
|
||||
paths.push(appdata.join("ERitors Scribe"));
|
||||
paths.push(appdata.join("Electron"));
|
||||
}
|
||||
}
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
fn find_electron_vault(data_path: &PathBuf) -> Option<PathBuf> {
|
||||
let candidate = data_path.join("secure-config.json");
|
||||
if candidate.exists() { return Some(candidate); }
|
||||
None
|
||||
}
|
||||
|
||||
fn find_electron_dbs(data_path: &PathBuf) -> Vec<(String, PathBuf)> {
|
||||
let mut result = Vec::new();
|
||||
if let Ok(entries) = std::fs::read_dir(data_path) {
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.starts_with("eritors-local-") && name.ends_with(".db") {
|
||||
let user_id = name
|
||||
.trim_start_matches("eritors-local-")
|
||||
.trim_end_matches(".db")
|
||||
.to_string();
|
||||
result.push((user_id, entry.path()));
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn decrypt_electron_value(encrypted_b64: &str) -> Option<String> {
|
||||
use aes::Aes128;
|
||||
use cbc::cipher::{block_padding::Pkcs7, BlockDecryptMut as _, KeyIvInit};
|
||||
use pbkdf2::pbkdf2_hmac;
|
||||
use sha1::Sha1;
|
||||
|
||||
type Aes128CbcDec = cbc::Decryptor<Aes128>;
|
||||
|
||||
let b64 = if encrypted_b64.starts_with("encrypted:") {
|
||||
&encrypted_b64[10..]
|
||||
} else {
|
||||
encrypted_b64
|
||||
};
|
||||
|
||||
let data = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
b64,
|
||||
).ok()?;
|
||||
|
||||
if !data.starts_with(b"v10") {
|
||||
return None;
|
||||
}
|
||||
let ciphertext = &data[3..];
|
||||
|
||||
let app_names = ["ERitors Scribe", "Electron"];
|
||||
for app_name in &app_names {
|
||||
let service = format!("{} Safe Storage", app_name);
|
||||
if let Ok(entry) = keyring::Entry::new(&service, app_name) {
|
||||
if let Ok(password) = entry.get_password() {
|
||||
let mut key = [0u8; 16];
|
||||
pbkdf2_hmac::<Sha1>(
|
||||
password.as_bytes(),
|
||||
b"saltysalt",
|
||||
1003,
|
||||
&mut key,
|
||||
);
|
||||
let iv = [0x20u8; 16];
|
||||
let mut buf = ciphertext.to_vec();
|
||||
if let Ok(decryptor) = Aes128CbcDec::new_from_slices(&key, &iv) {
|
||||
if let Ok(decrypted) = decryptor.decrypt_padded_mut::<Pkcs7>(&mut buf) {
|
||||
if let Ok(s) = String::from_utf8(decrypted.to_vec()) {
|
||||
return Some(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn decrypt_electron_value(_encrypted_b64: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn read_electron_vault(vault_path: &PathBuf) -> HashMap<String, String> {
|
||||
let mut result = HashMap::new();
|
||||
let Ok(content) = std::fs::read_to_string(vault_path) else { return result; };
|
||||
let Ok(parsed) = serde_json::from_str::<HashMap<String, serde_json::Value>>(&content) else { return result; };
|
||||
|
||||
for (key, value) in &parsed {
|
||||
if let Some(s) = value.as_str() {
|
||||
if let Some(decrypted) = decrypt_electron_value(s) {
|
||||
result.insert(key.clone(), decrypted);
|
||||
} else if !s.starts_with("encrypted:") {
|
||||
result.insert(key.clone(), s.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Called automatically at startup in production — detects and migrates Electron data.
|
||||
#[tauri::command]
|
||||
pub fn auto_migrate_electron(
|
||||
db: State<DbManager>,
|
||||
session: State<SessionState>,
|
||||
) -> AppResult<AutoMigrationResult> {
|
||||
let already_migrated = key_manager::get_last_user_id().ok().flatten();
|
||||
if already_migrated.is_some() {
|
||||
return Ok(AutoMigrationResult {
|
||||
migrated: false, user_id: None,
|
||||
token_migrated: false, key_migrated: false,
|
||||
pin_migrated: false, db_migrated: false,
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
|
||||
for data_path in electron_data_paths() {
|
||||
if !data_path.exists() { continue; }
|
||||
|
||||
let dbs = find_electron_dbs(&data_path);
|
||||
if dbs.is_empty() { continue; }
|
||||
|
||||
let vault_data = if let Some(vault_path) = find_electron_vault(&data_path) {
|
||||
read_electron_vault(&vault_path)
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
let token = vault_data.get("token").cloned();
|
||||
let last_user_id = vault_data.get("lastUserId").cloned()
|
||||
.or_else(|| dbs.first().map(|(uid, _)| uid.clone()));
|
||||
|
||||
let Some(user_id) = last_user_id else { continue; };
|
||||
|
||||
let encryption_key = vault_data.get(&format!("encryptionKey-{}", user_id)).cloned();
|
||||
let pin_hash = vault_data.get(&format!("pinHash-{}", user_id)).cloned();
|
||||
|
||||
// Copy DB files
|
||||
let mut db_migrated = false;
|
||||
for (uid, db_path) in &dbs {
|
||||
if uid != &user_id { continue; }
|
||||
{
|
||||
let mut db_manager = db.lock()
|
||||
.map_err(|e| AppError::Internal(format!("DB lock: {}", e)))?;
|
||||
db_manager.initialize(uid)?;
|
||||
let dest = db_manager.get_db_path(uid);
|
||||
if let Some(parent) = dest.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
if std::fs::copy(db_path, &dest).is_ok() {
|
||||
for ext in &["db-wal", "db-shm"] {
|
||||
let src_extra = db_path.with_extension(ext);
|
||||
if src_extra.exists() {
|
||||
let dst_extra = dest.with_extension(ext);
|
||||
let _ = std::fs::copy(&src_extra, &dst_extra);
|
||||
}
|
||||
}
|
||||
db_migrated = true;
|
||||
}
|
||||
let conn = db_manager.get_connection(uid)?;
|
||||
schema::initialize_schema(conn)?;
|
||||
if !cfg!(debug_assertions) { schema::run_migrations(conn)?; }
|
||||
}
|
||||
}
|
||||
|
||||
// Store secrets
|
||||
let token_migrated = if let Some(ref t) = token {
|
||||
key_manager::set_token(t).is_ok()
|
||||
} else { false };
|
||||
|
||||
let key_migrated = if let Some(ref k) = encryption_key {
|
||||
key_manager::set_user_encryption_key(&user_id, k).is_ok()
|
||||
} else { false };
|
||||
|
||||
let pin_migrated = if let Some(ref p) = pin_hash {
|
||||
key_manager::set_pin_hash(&user_id, p).is_ok()
|
||||
} else { false };
|
||||
|
||||
key_manager::set_last_user_id(&user_id)?;
|
||||
|
||||
let mut session_guard = session.lock()
|
||||
.map_err(|e| AppError::Internal(format!("Session lock: {}", e)))?;
|
||||
session_guard.user_id = Some(user_id.clone());
|
||||
|
||||
return Ok(AutoMigrationResult {
|
||||
migrated: true,
|
||||
user_id: Some(user_id),
|
||||
token_migrated,
|
||||
key_migrated,
|
||||
pin_migrated,
|
||||
db_migrated,
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(AutoMigrationResult {
|
||||
migrated: false, user_id: None,
|
||||
token_migrated: false, key_migrated: false,
|
||||
pin_migrated: false, db_migrated: false,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if an Electron migration file + DB exist at the given path (legacy).
|
||||
#[tauri::command]
|
||||
pub fn check_electron_migration(migration_file_path: String) -> AppResult<MigrationCheckResult> {
|
||||
let path = PathBuf::from(&migration_file_path);
|
||||
if !path.exists() {
|
||||
return Ok(MigrationCheckResult { found: false, user_id: None, has_db: false, migration_path: None });
|
||||
}
|
||||
let content = std::fs::read_to_string(&path).map_err(AppError::Io)?;
|
||||
let migration: ElectronMigrationFile = serde_json::from_str(&content)?;
|
||||
let db_name = format!("eritors-local-{}.db", migration.user_id);
|
||||
let db_path = path.parent().unwrap_or(&path).join(&db_name);
|
||||
Ok(MigrationCheckResult {
|
||||
found: true,
|
||||
user_id: Some(migration.user_id),
|
||||
has_db: db_path.exists(),
|
||||
migration_path: Some(migration_file_path),
|
||||
})
|
||||
}
|
||||
|
||||
/// Import from Electron export file (legacy).
|
||||
#[tauri::command]
|
||||
pub fn import_from_electron(
|
||||
data: ImportMigrationData,
|
||||
db: State<DbManager>,
|
||||
session: State<SessionState>,
|
||||
) -> AppResult<MigrationResult> {
|
||||
let migration_path = PathBuf::from(&data.migration_file_path);
|
||||
if !migration_path.exists() {
|
||||
return Ok(MigrationResult { success: false, user_id: None, error: Some("Migration file not found".into()) });
|
||||
}
|
||||
let content = std::fs::read_to_string(&migration_path)?;
|
||||
let migration: ElectronMigrationFile = serde_json::from_str(&content)?;
|
||||
if migration.version != 1 {
|
||||
return Ok(MigrationResult { success: false, user_id: None, error: Some(format!("Unsupported version: {}", migration.version)) });
|
||||
}
|
||||
let source_db = migration_path.parent()
|
||||
.unwrap_or_else(|| std::path::Path::new("."))
|
||||
.join(format!("eritors-local-{}.db", migration.user_id));
|
||||
if !source_db.exists() {
|
||||
let electron_db = PathBuf::from(&migration.db_source);
|
||||
if !electron_db.exists() {
|
||||
return Ok(MigrationResult { success: false, user_id: None, error: Some("Database file not found.".into()) });
|
||||
}
|
||||
copy_db_files(&electron_db, &migration.user_id, &db)?;
|
||||
} else {
|
||||
copy_db_files(&source_db, &migration.user_id, &db)?;
|
||||
}
|
||||
key_manager::set_user_encryption_key(&migration.user_id, &migration.encryption_key)?;
|
||||
if let Some(ref pin_hash) = migration.pin_hash {
|
||||
key_manager::set_pin_hash(&migration.user_id, pin_hash)?;
|
||||
}
|
||||
key_manager::set_last_user_id(&migration.user_id)?;
|
||||
{
|
||||
let mut db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock: {}", e)))?;
|
||||
db_manager.initialize(&migration.user_id)?;
|
||||
let conn = db_manager.get_connection(&migration.user_id)?;
|
||||
schema::initialize_schema(conn)?;
|
||||
if !cfg!(debug_assertions) { schema::run_migrations(conn)?; }
|
||||
}
|
||||
let mut session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock: {}", e)))?;
|
||||
session_guard.user_id = Some(migration.user_id.clone());
|
||||
cleanup_migration_files(&migration_path, &migration.user_id);
|
||||
Ok(MigrationResult { success: true, user_id: Some(migration.user_id), error: None })
|
||||
}
|
||||
|
||||
fn cleanup_migration_files(migration_path: &PathBuf, user_id: &str) {
|
||||
let _ = std::fs::remove_file(migration_path);
|
||||
if let Some(parent) = migration_path.parent() {
|
||||
let db_copy = parent.join(format!("eritors-local-{}.db", user_id));
|
||||
let _ = std::fs::remove_file(&db_copy);
|
||||
let _ = std::fs::remove_file(db_copy.with_extension("db-wal"));
|
||||
let _ = std::fs::remove_file(db_copy.with_extension("db-shm"));
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_db_files(source_db: &PathBuf, user_id: &str, db: &State<DbManager>) -> AppResult<()> {
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock: {}", e)))?;
|
||||
let dest_db = db_manager.get_db_path(user_id);
|
||||
if let Some(parent) = dest_db.parent() { std::fs::create_dir_all(parent)?; }
|
||||
std::fs::copy(source_db, &dest_db)?;
|
||||
for ext in &["db-wal", "db-shm"] {
|
||||
let src = source_db.with_extension(ext);
|
||||
if src.exists() {
|
||||
let dst = dest_db.with_file_name(format!("eritors-local-{}.{}", user_id, ext));
|
||||
let _ = std::fs::copy(&src, &dst);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user