Skip to content

Commit 9106742

Browse files
authored
Merge pull request #79 from saagpatel/codex/refactor/app-error-startup-backup-search
refactor(src): migrate backup commands to AppError with [CANCELLED] code
2 parents 9d8f959 + 13b7e06 commit 9106742

3 files changed

Lines changed: 58 additions & 36 deletions

File tree

src-tauri/src/commands/backup.rs

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Backup, restore, and export commands
22
33
use crate::backup::{ExportSummary, ImportPreview, ImportSummary};
4+
use crate::error::AppError;
45
use crate::AppState;
56
use tauri::State;
67
use tauri_plugin_dialog::DialogExt;
@@ -57,13 +58,23 @@ impl ExportFormat {
5758
}
5859
}
5960

61+
/// Map a `crate::backup` error to AppError, preserving the detail for logs.
62+
fn backup_err(op: &str, e: impl std::fmt::Display) -> AppError {
63+
AppError::new(
64+
crate::error::ErrorCode::INTERNAL_ERROR,
65+
format!("Backup {} failed", op),
66+
crate::error::ErrorCategory::Internal,
67+
)
68+
.with_detail(e.to_string())
69+
}
70+
6071
/// Export a draft response to a file
6172
#[tauri::command]
6273
pub async fn export_draft(
6374
app: tauri::AppHandle,
6475
response_text: String,
6576
format: ExportFormat,
66-
) -> Result<bool, String> {
77+
) -> Result<bool, AppError> {
6778
let content = format.format_content(&response_text);
6879
let default_filename = format!("response.{}", format.extension());
6980

@@ -77,18 +88,17 @@ pub async fn export_draft(
7788

7889
match file_handle {
7990
Some(path) => {
80-
// Get the actual path
8191
let file_path = path
8292
.as_path()
83-
.ok_or_else(|| "Invalid file path".to_string())?;
93+
.ok_or_else(|| AppError::invalid_path("Invalid file path"))?;
8494

85-
std::fs::write(file_path, content)
86-
.map_err(|e| format!("Failed to write file: {}", e))?;
95+
std::fs::write(file_path, content).map_err(AppError::from)?;
8796

8897
Ok(true)
8998
}
9099
None => {
91-
// User cancelled
100+
// User cancelled — return Ok(false) to preserve the existing
101+
// "cancelled is not an error" semantic for this command.
92102
Ok(false)
93103
}
94104
}
@@ -104,9 +114,9 @@ pub async fn export_backup(
104114
app: tauri::AppHandle,
105115
state: State<'_, AppState>,
106116
password: Option<String>,
107-
) -> Result<ExportSummary, String> {
108-
let db_lock = state.db.lock().map_err(|e| e.to_string())?;
109-
let db = db_lock.as_ref().ok_or("Database not initialized")?;
117+
) -> Result<ExportSummary, AppError> {
118+
let db_lock = state.db.lock().map_err(|_| AppError::db_lock_failed())?;
119+
let db = db_lock.as_ref().ok_or_else(AppError::db_not_initialized)?;
110120

111121
// Determine file extension based on encryption
112122
let (filename, filter_name, extensions) = if password.is_some() {
@@ -127,12 +137,12 @@ pub async fn export_backup(
127137
Some(path) => {
128138
let file_path = path
129139
.as_path()
130-
.ok_or_else(|| "Invalid file path".to_string())?;
140+
.ok_or_else(|| AppError::invalid_path("Invalid file path"))?;
131141

132142
crate::backup::export_backup(db, file_path, password.as_deref())
133-
.map_err(|e| e.to_string())
143+
.map_err(|e| backup_err("export", e))
134144
}
135-
None => Err("Export cancelled".to_string()),
145+
None => Err(AppError::cancelled()),
136146
}
137147
}
138148

@@ -141,7 +151,7 @@ pub async fn export_backup(
141151
pub async fn preview_backup_import(
142152
app: tauri::AppHandle,
143153
password: Option<String>,
144-
) -> Result<ImportPreview, String> {
154+
) -> Result<ImportPreview, AppError> {
145155
// Show open file dialog (accept both ZIP and encrypted backups)
146156
let file_handle = app
147157
.dialog()
@@ -155,11 +165,12 @@ pub async fn preview_backup_import(
155165
Some(path) => {
156166
let file_path = path
157167
.as_path()
158-
.ok_or_else(|| "Invalid file path".to_string())?;
168+
.ok_or_else(|| AppError::invalid_path("Invalid file path"))?;
159169

160-
crate::backup::preview_import(file_path, password.as_deref()).map_err(|e| e.to_string())
170+
crate::backup::preview_import(file_path, password.as_deref())
171+
.map_err(|e| backup_err("preview", e))
161172
}
162-
None => Err("Import cancelled".to_string()),
173+
None => Err(AppError::cancelled()),
163174
}
164175
}
165176

@@ -169,7 +180,7 @@ pub async fn import_backup(
169180
app: tauri::AppHandle,
170181
state: State<'_, AppState>,
171182
password: Option<String>,
172-
) -> Result<ImportSummary, String> {
183+
) -> Result<ImportSummary, AppError> {
173184
// Show open file dialog (accept both ZIP and encrypted backups)
174185
let file_handle = app
175186
.dialog()
@@ -183,27 +194,35 @@ pub async fn import_backup(
183194
Some(path) => {
184195
let file_path = path
185196
.as_path()
186-
.ok_or_else(|| "Invalid file path".to_string())?;
197+
.ok_or_else(|| AppError::invalid_path("Invalid file path"))?;
187198

188199
{
189-
let db_lock = state.db.lock().map_err(|e| e.to_string())?;
200+
let db_lock = state.db.lock().map_err(|_| AppError::db_lock_failed())?;
190201
if let Some(db) = db_lock.as_ref() {
191202
return crate::backup::import_backup(db, file_path, password.as_deref())
192-
.map_err(|e| e.to_string());
203+
.map_err(|e| backup_err("import", e));
193204
}
194205
}
195206

207+
// No live DB — fall back to the recovery-mode restore path.
196208
let restore_result = {
197-
let recovery_lock = state.recovery.lock().map_err(|e| e.to_string())?;
198-
let recovery = recovery_lock.as_ref().ok_or("Database not initialized")?;
199-
let db_path = recovery
200-
.db_path
201-
.as_ref()
202-
.ok_or("Backup restore is not available for this recovery issue")?;
203-
let master_key = recovery
204-
.master_key
209+
let recovery_lock = state
210+
.recovery
211+
.lock()
212+
.map_err(|_| AppError::lock_failed("recovery context"))?;
213+
let recovery = recovery_lock
205214
.as_ref()
206-
.ok_or("Backup restore is not available for this recovery issue")?;
215+
.ok_or_else(AppError::db_not_initialized)?;
216+
let db_path = recovery.db_path.as_ref().ok_or_else(|| {
217+
AppError::invalid_format(
218+
"Backup restore is not available for this recovery issue",
219+
)
220+
})?;
221+
let master_key = recovery.master_key.as_ref().ok_or_else(|| {
222+
AppError::invalid_format(
223+
"Backup restore is not available for this recovery issue",
224+
)
225+
})?;
207226

208227
crate::backup::restore_backup_into_fresh_database(
209228
db_path,
@@ -214,12 +233,15 @@ pub async fn import_backup(
214233
};
215234

216235
if restore_result.is_ok() {
217-
let mut recovery_lock = state.recovery.lock().map_err(|e| e.to_string())?;
236+
let mut recovery_lock = state
237+
.recovery
238+
.lock()
239+
.map_err(|_| AppError::lock_failed("recovery context"))?;
218240
*recovery_lock = None;
219241
}
220242

221-
restore_result.map_err(|e| e.to_string())
243+
restore_result.map_err(|e| backup_err("restore", e))
222244
}
223-
None => Err("Import cancelled".to_string()),
245+
None => Err(AppError::cancelled()),
224246
}
225247
}

src/components/Settings/SettingsTab.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ export function SettingsTab() {
517517
`Exported ${result.drafts_count} drafts, ${result.templates_count} templates, ${result.variables_count} variables, ${result.trees_count} trees`,
518518
);
519519
} catch (err) {
520-
if (String(err) !== "Export cancelled") {
520+
if (!String(err).startsWith("[CANCELLED_BY_USER]")) {
521521
showError(`Export failed: ${err}`);
522522
}
523523
} finally {
@@ -541,7 +541,7 @@ export function SettingsTab() {
541541
loadInitialState();
542542
loadVariables();
543543
} catch (err) {
544-
if (String(err) !== "Import cancelled") {
544+
if (!String(err).startsWith("[CANCELLED_BY_USER]")) {
545545
showError(`Import failed: ${err}`);
546546
}
547547
} finally {
@@ -568,7 +568,7 @@ export function SettingsTab() {
568568
});
569569
showSuccess(`Audit log exported to ${output}`);
570570
} catch (err) {
571-
if (String(err) !== "Export cancelled") {
571+
if (!String(err).startsWith("[CANCELLED_BY_USER]")) {
572572
showError(`Audit export failed: ${err}`);
573573
}
574574
} finally {

src/components/shared/RecoveryScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function RecoveryScreen({ issue }: RecoveryScreenProps) {
4242
`Backup restored: ${result.drafts_imported} drafts, ${result.templates_imported} templates, ${result.variables_imported} variables, and ${result.trees_imported} trees imported. Restart AssistSupport to continue.`,
4343
);
4444
} catch (err) {
45-
if (String(err) !== "Import cancelled") {
45+
if (!String(err).startsWith("[CANCELLED_BY_USER]")) {
4646
setError(String(err));
4747
}
4848
} finally {

0 commit comments

Comments
 (0)