diff --git a/src-tauri/src/commands/docker.rs b/src-tauri/src/commands/docker.rs index 34ee5e2..dabec3f 100644 --- a/src-tauri/src/commands/docker.rs +++ b/src-tauri/src/commands/docker.rs @@ -400,6 +400,67 @@ fn pg_dump_shell_cmd(has_local: bool, pg_version: &str, extra_args: &str, source } } +async fn run_pipe_cmd( + app: &AppHandle, + clone_id: &str, + pipe_cmd: &str, + label: &str, +) -> TuskResult { + // Use bash with pipefail so pg_dump failures are not swallowed + let wrapped = format!("set -o pipefail; {}", pipe_cmd); + + emit_progress(app, clone_id, "transfer", 50, label, None); + + let output = Command::new("bash") + .args(["-c", &wrapped]) + .output() + .await + .map_err(|e| docker_err(format!("{} failed to start: {}", label, e)))?; + + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + + // Always log stderr if present + if !stderr.is_empty() { + // Truncate for progress display (full log can be long) + let short = if stderr.len() > 500 { + format!("{}...", &stderr[..500]) + } else { + stderr.clone() + }; + emit_progress(app, clone_id, "transfer", 55, &format!("{}: stderr output", label), Some(&short)); + } + + // Count DDL statements in stdout for feedback + if !stdout.is_empty() { + let creates = stdout.lines() + .filter(|l| l.starts_with("CREATE") || l.starts_with("ALTER") || l.starts_with("SET")) + .count(); + if creates > 0 { + emit_progress(app, clone_id, "transfer", 58, &format!("Applied {} SQL statements", creates), None); + } + } + + if !output.status.success() { + let code = output.status.code().unwrap_or(-1); + emit_progress( + app, clone_id, "transfer", 55, + &format!("{} exited with code {}", label, code), + Some(&stderr), + ); + + // Only hard-fail on connection / fatal errors + if stderr.contains("FATAL") || stderr.contains("could not connect") + || stderr.contains("No such file") || stderr.contains("password authentication failed") + || stderr.contains("does not exist") || (stdout.is_empty() && stderr.is_empty()) + { + return Err(docker_err(format!("{} failed (exit {}): {}", label, code, stderr))); + } + } + + Ok(output) +} + async fn transfer_schema_only( app: &AppHandle, clone_id: &str, @@ -410,43 +471,16 @@ async fn transfer_schema_only( ) -> TuskResult<()> { let has_local = try_local_pg_dump().await; let label = if has_local { "local pg_dump" } else { "Docker-based pg_dump" }; - emit_progress(app, clone_id, "transfer", 50, &format!("Using {} for schema...", label), None); + emit_progress(app, clone_id, "transfer", 48, &format!("Using {} for schema...", label), None); - let dump_cmd = pg_dump_shell_cmd(has_local, pg_version, "--schema-only", source_url); + let dump_cmd = pg_dump_shell_cmd(has_local, pg_version, "--schema-only --no-owner --no-acl", source_url); let escaped_db = shell_escape(database); let pipe_cmd = format!( "{} | docker exec -i '{}' psql -U postgres -d '{}'", dump_cmd, shell_escape(container_name), escaped_db ); - let output = Command::new("sh") - .args(["-c", &pipe_cmd]) - .output() - .await - .map_err(|e| docker_err(format!("Schema transfer failed: {}", e)))?; - - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); - - if !output.status.success() { - // psql often emits non-fatal warnings; only fail on actual errors - if stderr.contains("FATAL") || stderr.contains("could not connect") || stderr.contains("No such file") { - emit_progress(app, clone_id, "error", 55, "Schema transfer failed", Some(&stderr)); - return Err(docker_err(format!("Schema transfer failed: {}", stderr))); - } - } - - // Log any output for debugging - if !stderr.is_empty() { - emit_progress(app, clone_id, "transfer", 55, "Schema transferred with warnings", Some(&stderr)); - } - if !stdout.is_empty() { - // Count CREATE statements to give user feedback - let creates = stdout.lines().filter(|l| l.starts_with("CREATE") || l.starts_with("ALTER")).count(); - if creates > 0 { - emit_progress(app, clone_id, "transfer", 58, &format!("Applied {} DDL statements", creates), None); - } - } + run_pipe_cmd(app, clone_id, &pipe_cmd, "Schema transfer").await?; emit_progress(app, clone_id, "transfer", 60, "Schema transferred successfully", None); Ok(()) @@ -462,34 +496,17 @@ async fn transfer_full_clone( ) -> TuskResult<()> { let has_local = try_local_pg_dump().await; let label = if has_local { "local pg_dump" } else { "Docker-based pg_dump" }; - emit_progress(app, clone_id, "transfer", 50, &format!("Using {} for full clone...", label), None); + emit_progress(app, clone_id, "transfer", 48, &format!("Using {} for full clone...", label), None); - let dump_cmd = pg_dump_shell_cmd(has_local, pg_version, "-Fc", source_url); + // Use plain text format piped to psql (more reliable than -Fc | pg_restore through docker exec) + let dump_cmd = pg_dump_shell_cmd(has_local, pg_version, "--no-owner --no-acl", source_url); let escaped_db = shell_escape(database); let pipe_cmd = format!( - "{} | docker exec -i '{}' pg_restore -U postgres -d '{}' --no-owner", + "{} | docker exec -i '{}' psql -U postgres -d '{}'", dump_cmd, shell_escape(container_name), escaped_db ); - let output = Command::new("sh") - .args(["-c", &pipe_cmd]) - .output() - .await - .map_err(|e| docker_err(format!("Full clone failed: {}", e)))?; - - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - - if !output.status.success() { - if stderr.contains("FATAL") || stderr.contains("could not connect") { - emit_progress(app, clone_id, "error", 55, "Full clone failed", Some(&stderr)); - return Err(docker_err(format!("Full clone failed: {}", stderr))); - } - } - - // pg_restore often emits warnings about ownership/permissions — log them - if !stderr.is_empty() { - emit_progress(app, clone_id, "transfer", 80, "Clone completed with warnings", Some(&stderr)); - } + run_pipe_cmd(app, clone_id, &pipe_cmd, "Full clone").await?; emit_progress(app, clone_id, "transfer", 85, "Full clone completed", None); Ok(()) @@ -567,11 +584,11 @@ async fn transfer_sample_data( }; let pipe_cmd = format!( - "{} | docker exec -i '{}' psql -U postgres -d '{}' -c \"{}\"", + "set -o pipefail; {} | docker exec -i '{}' psql -U postgres -d '{}' -c \"{}\"", source_cmd, escaped_container, escaped_db, copy_in_sql ); - let output = Command::new("sh") + let output = Command::new("bash") .args(["-c", &pipe_cmd]) .output() .await; diff --git a/src/components/docker/CloneDatabaseDialog.tsx b/src/components/docker/CloneDatabaseDialog.tsx index 5e07d59..89c5329 100644 --- a/src/components/docker/CloneDatabaseDialog.tsx +++ b/src/components/docker/CloneDatabaseDialog.tsx @@ -67,17 +67,17 @@ function ProcessLog({ Process Log ({entries.length}) {logOpen && ( -
+
{entries.map((entry, i) => ( -
- +
+ {entry.percent}% - + {" "} {entry.message} {entry.detail && ( - - — {entry.detail} - +
+ {entry.detail} +
)}
))}