Surfaces the pre-migration SREF backup to the user so they can roll back a category migration without digging into the filesystem: - 90-day dismissable banner at the top of Settings > Categories pointing to the automatic backup (hidden once reverted, once dismissed, or past 90d). - Permanent "Restore a backup" entry in Settings > Categories, available as long as a migration journal exists (even past the 90-day window). - Confirmation modal with two-step consent, red Restore button, fallback file picker when the recorded path is missing, PIN prompt for encrypted SREF files, full-page reload on success. Internals: - New `categoryRestoreService` wrapping `read_import_file` + `importTransactionsWithCategories` with stable error codes (file_missing, read_failed, parse_failed, wrong_envelope_type, needs_password, wrong_password, import_failed). - New `file_exists` Tauri command for the pre-flight presence check. - On success: `categories_schema_version=v2` + merge `reverted_at` into `last_categories_migration`. - Pure `shouldShowBanner` / `isWithinBannerWindow` helpers with tests. - FR/EN i18n keys under `settings.categoriesCard.restore*`. - CHANGELOG entries in both locales. Closes #122
63 lines
2.6 KiB
Rust
63 lines
2.6 KiB
Rust
use std::fs;
|
|
use std::path::PathBuf;
|
|
use tauri::Manager;
|
|
|
|
/// Subdirectory under the user's Documents folder where pre-migration backups
|
|
/// are written by default. Keeping the location predictable makes it easy for
|
|
/// users to find their backup files even if the app is uninstalled.
|
|
const BACKUP_SUBDIR: &str = "Simpl-Resultat/backups";
|
|
|
|
fn resolve_backup_dir(app: &tauri::AppHandle) -> Result<PathBuf, String> {
|
|
let documents = app
|
|
.path()
|
|
.document_dir()
|
|
.map_err(|e| format!("Cannot resolve Documents directory: {}", e))?;
|
|
Ok(documents.join(BACKUP_SUBDIR))
|
|
}
|
|
|
|
/// Resolve `~/Documents/Simpl-Resultat/backups/` and create it if missing.
|
|
/// Returns the absolute path as a string. Used by the pre-migration backup
|
|
/// flow to place SREF files in a predictable, user-visible location.
|
|
#[tauri::command]
|
|
pub fn ensure_backup_dir(app: tauri::AppHandle) -> Result<String, String> {
|
|
let dir = resolve_backup_dir(&app)?;
|
|
|
|
if !dir.exists() {
|
|
fs::create_dir_all(&dir).map_err(|e| {
|
|
// Surface permission issues explicitly — the TS layer maps this to
|
|
// a user-facing i18n key.
|
|
if e.kind() == std::io::ErrorKind::PermissionDenied {
|
|
format!("permission_denied: {}", dir.to_string_lossy())
|
|
} else {
|
|
format!("create_dir_failed: {}: {}", dir.to_string_lossy(), e)
|
|
}
|
|
})?;
|
|
}
|
|
|
|
Ok(dir.to_string_lossy().to_string())
|
|
}
|
|
|
|
/// Return the size of a file on disk in bytes. Used to report the size of a
|
|
/// freshly-written backup to the UI. Returns a clear error if the file does
|
|
/// not exist or cannot be read.
|
|
#[tauri::command]
|
|
pub fn get_file_size(file_path: String) -> Result<u64, String> {
|
|
let metadata =
|
|
fs::metadata(&file_path).map_err(|e| format!("Cannot stat file {}: {}", file_path, e))?;
|
|
Ok(metadata.len())
|
|
}
|
|
|
|
/// Return true when the given path points to an existing regular file. Used
|
|
/// by the post-migration restore flow to detect that the recorded backup
|
|
/// path is still reachable before opening the confirmation modal — when the
|
|
/// file was moved or deleted, the UI falls back to a manual file picker.
|
|
/// Never throws on a missing file (just returns `false`); only returns an
|
|
/// error for unexpected I/O conditions other than "not found".
|
|
#[tauri::command]
|
|
pub fn file_exists(file_path: String) -> Result<bool, String> {
|
|
match fs::metadata(&file_path) {
|
|
Ok(meta) => Ok(meta.is_file()),
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
|
|
Err(e) => Err(format!("Cannot stat file {}: {}", file_path, e)),
|
|
}
|
|
}
|