Skip to content

Commit eecb8d7

Browse files
committed
Overwrite warning: confirm before replacing existing output files
Replaces the read-only fallback hack with proper UX: Backend: - New command 'check_output_conflicts': derives output paths from config (same logic as core pipeline) and returns which ones already exist. Frontend: - Before running, calls check_output_conflicts. If files exist, shows a native confirm dialog listing the filenames. User can cancel or proceed to overwrite. If the check fails (e.g. permission error), analysis proceeds anyway to let the core report the real error.
1 parent 2d67c02 commit eecb8d7

3 files changed

Lines changed: 60 additions & 26 deletions

File tree

frontend/src/App.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -834,13 +834,31 @@ function App() {
834834
<button
835835
className={`run-button${justFinished ? " run-button--success" : ""}`}
836836
disabled={running}
837-
onClick={() => {
837+
onClick={async () => {
838838
if (!filesReady || samples.length === 0) {
839839
setShowValidation(true);
840840
return;
841841
}
842842
setShowValidation(false);
843-
(runnableCount > 0 ? handleRunAll : handleRerunAll)();
843+
844+
// Check for existing output files before running
845+
if (runnableCount > 0) {
846+
try {
847+
const conflicts = await invoke<string[]>("check_output_conflicts", { config });
848+
if (conflicts.length > 0) {
849+
const names = conflicts.map((p) => p.split(/[\\/]/).pop()).join(", ");
850+
const ok = window.confirm(
851+
`The following output files already exist and will be overwritten:\n\n${names}\n\nContinue?`
852+
);
853+
if (!ok) return;
854+
}
855+
} catch {
856+
// If check fails, proceed anyway
857+
}
858+
handleRunAll();
859+
} else {
860+
handleRerunAll();
861+
}
844862
}}
845863
>
846864
{justFinished ? (

src-tauri/src/commands.rs

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -136,28 +136,7 @@ pub fn run_analysis(
136136
app_handle: tauri::AppHandle,
137137
config: AnalysisConfig,
138138
) -> Result<serde_json::Value, String> {
139-
let mut args = config.into_args();
140-
141-
// If no explicit output dir, default to VCF's parent directory.
142-
// If that's not writable (e.g., read-only volume, .dmg), fall back to ~/Desktop.
143-
if args.output_dir.is_none() {
144-
let vcf_dir = std::path::Path::new(&args.vcf_file)
145-
.parent()
146-
.unwrap_or(std::path::Path::new("."));
147-
let test_file = vcf_dir.join(".get_mnv_write_test");
148-
let writable = std::fs::write(&test_file, b"").is_ok();
149-
if writable {
150-
let _ = std::fs::remove_file(&test_file);
151-
} else if let Ok(home) = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")) {
152-
let home_path = std::path::PathBuf::from(&home);
153-
let desktop = home_path.join("Desktop");
154-
if desktop.is_dir() {
155-
args.output_dir = Some(desktop.to_string_lossy().into_owned());
156-
} else {
157-
args.output_dir = Some(home);
158-
}
159-
}
160-
}
139+
let args = config.into_args();
161140

162141
let progress_callback = move |evt: get_mnv::pipeline::ProgressEvent| {
163142
let _ = app_handle.emit("analysis-progress", &evt);
@@ -169,6 +148,42 @@ pub fn run_analysis(
169148
serde_json::to_value(&summary).map_err(|e| format!("Failed to serialize summary: {}", e))
170149
}
171150

151+
// ---------------------------------------------------------------------------
152+
// check_output_conflicts
153+
// ---------------------------------------------------------------------------
154+
155+
/// Check if any output files already exist. Returns paths that would be overwritten.
156+
#[tauri::command]
157+
pub fn check_output_conflicts(config: AnalysisConfig) -> Vec<String> {
158+
let vcf_path = std::path::Path::new(&config.vcf_file);
159+
160+
let base_name = vcf_path
161+
.file_name()
162+
.unwrap_or_default()
163+
.to_string_lossy()
164+
.replace(".vcf.gz", "")
165+
.replace(".vcf", "");
166+
167+
let stem = config.output_prefix.as_deref().unwrap_or(&base_name);
168+
let dir = config.output_dir.as_deref().map(std::path::Path::new)
169+
.unwrap_or_else(|| vcf_path.parent().unwrap_or(std::path::Path::new(".")));
170+
let base = dir.join(stem);
171+
172+
let mut paths = Vec::new();
173+
if config.output_tsv.unwrap_or(true) {
174+
paths.push(format!("{}.MNV.tsv", base.display()));
175+
}
176+
if config.output_vcf.unwrap_or(false) {
177+
if config.vcf_gz.unwrap_or(false) {
178+
paths.push(format!("{}.MNV.vcf.gz", base.display()));
179+
} else {
180+
paths.push(format!("{}.MNV.vcf", base.display()));
181+
}
182+
}
183+
184+
paths.into_iter().filter(|p| std::path::Path::new(p).exists()).collect()
185+
}
186+
172187
// ---------------------------------------------------------------------------
173188
// ensure_fasta_index
174189
// ---------------------------------------------------------------------------

src-tauri/src/main.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
mod commands;
55

66
use commands::{
7-
ensure_fasta_index, get_bam_view, get_core_version, get_gff_features, read_tsv_file,
8-
run_analysis, write_text_file,
7+
check_output_conflicts, ensure_fasta_index, get_bam_view, get_core_version, get_gff_features,
8+
read_tsv_file, run_analysis, write_text_file,
99
};
1010

1111
fn main() {
@@ -19,6 +19,7 @@ fn main() {
1919
get_core_version,
2020
get_gff_features,
2121
ensure_fasta_index,
22+
check_output_conflicts,
2223
read_tsv_file,
2324
get_bam_view,
2425
write_text_file,

0 commit comments

Comments
 (0)