Simpl-Resultat/src-tauri/src/commands/backup_commands.rs
le king fu 0132e6e164
All checks were successful
PR Check / rust (push) Successful in 21m45s
PR Check / frontend (push) Successful in 2m17s
PR Check / rust (pull_request) Successful in 21m1s
PR Check / frontend (pull_request) Successful in 2m13s
feat(categories): add restore backup banner and permanent restore action (#122)
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
2026-04-20 21:47:43 -04:00

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)),
}
}