From a3c08f6eaa407f0daf0d8bdef4850001397b700d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Jane=C5=BEi=C4=8D?= Date: Tue, 8 Sep 2026 12:04:51 +0000 Subject: [PATCH] refactor: put every filesystem write behind one module --- Cargo.lock | 16 ++++ Cargo.toml | 1 + src/cli/ahab.rs | 56 +------------- src/cli/mod.rs | 4 +- src/fsops.rs | 165 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 9 ++- src/output.rs | 11 +++ src/scripts/django.rs | 43 ++++------- src/scripts/link.rs | 150 ++++++------------------------------ src/scripts/postgres.rs | 42 ++++------ 10 files changed, 261 insertions(+), 236 deletions(-) create mode 100644 src/fsops.rs create mode 100644 src/output.rs diff --git a/Cargo.lock b/Cargo.lock index 2a63163..9c143d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "anyhow", "clap", "clap_complete", + "fs-err", "serde_json", "shlex", ] @@ -69,6 +70,12 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "clap" version = "4.6.6" @@ -124,6 +131,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "fs-err" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +dependencies = [ + "autocfg", +] + [[package]] name = "heck" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index 0cc0a79..a574fca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ clap = { version = "4.6.6", features = ["derive", "env"] } clap_complete = "4.6.9" anyhow = "1.0.104" serde_json = "1.0.145" +fs-err = "3.3.1" shlex = "2.0.1" [build-dependencies] diff --git a/src/cli/ahab.rs b/src/cli/ahab.rs index 38d443a..e1f5feb 100644 --- a/src/cli/ahab.rs +++ b/src/cli/ahab.rs @@ -1,7 +1,7 @@ use super::{Django, Link, Postgres}; use clap::builder::styling::{AnsiColor, Effects, Styles}; -use clap::error::ErrorKind; -use clap::{CommandFactory, Parser, Subcommand}; + +use clap::{Parser, Subcommand}; use clap_complete::Shell; /// A program for interacting with various dockerized applications @@ -56,63 +56,13 @@ fn help_styles() -> Styles { .placeholder(AnsiColor::Cyan.on_default()) } -/// Exit with a usage error when `--dry-run` would be a lie -pub fn reject_unsupported_dry_run(command: &Commands) { - if let Some(name) = writes_locally(command) { - Ahab::command() - .error( - ErrorKind::ArgumentConflict, - format!("--dry-run is not supported by `{name}`, which writes to the working tree"), - ) - .exit() - } -} - -// TODO:(@janezicmatej) honour --dry-run in these commands instead of refusing it -fn writes_locally(command: &Commands) -> Option<&'static str> { - match command { - Commands::Link { command } => match command { - Link::Add { .. } => Some("link add"), - Link::Restore { .. } => Some("link restore"), - Link::Check { .. } => None, - }, - Commands::Django { command } => match command { - Django::MakeCommand { .. } => Some("django make-command"), - _ => None, - }, - Commands::Postgres { .. } | Commands::Completions { .. } => None, - } -} - #[cfg(test)] mod tests { use super::Ahab; - use clap::{CommandFactory, Parser}; + use clap::CommandFactory; #[test] fn the_command_is_well_formed() { Ahab::command().debug_assert(); } - - #[test] - fn dry_run_is_refused_only_where_it_cannot_be_honoured() { - let command = |args: &[&str]| Ahab::try_parse_from(args).unwrap().command; - - assert_eq!( - super::writes_locally(&command(&["ahab", "link", "add", ".env"])), - Some("link add") - ); - assert_eq!( - super::writes_locally(&command(&["ahab", "django", "make-command", "app", "name"])), - Some("django make-command") - ); - assert_eq!( - super::writes_locally(&command(&["ahab", "link", "check"])), - None - ); - assert_eq!( - super::writes_locally(&command(&["ahab", "postgres", "dump", "out.sql"])), - None - ); - } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 0e96624..72ef580 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,9 +1,11 @@ +// build.rs reaches these definitions with include!, so nothing here may refer to +// the rest of the crate: keep this tree to clap definitions only mod ahab; mod django; mod link; mod postgres; -pub use ahab::{Ahab, Commands, reject_unsupported_dry_run}; +pub use ahab::{Ahab, Commands}; pub use django::Django; pub use link::Link; pub use postgres::{Format, Postgres}; diff --git a/src/fsops.rs b/src/fsops.rs new file mode 100644 index 0000000..9edbd98 --- /dev/null +++ b/src/fsops.rs @@ -0,0 +1,165 @@ +use std::io::Write; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result}; +// std::fs with the path and the operation already in the error +use fs_err as fs; +use fs_err::os::unix::fs::symlink; + +use crate::ctx::Ctx; + +pub fn suffixed(path: &Path, suffix: &str) -> PathBuf { + let mut out = path.as_os_str().to_owned(); + out.push(suffix); + + PathBuf::from(out) +} + +// every write to the working tree goes through this module, so a dry run is held +// back in one place rather than at each call site +pub fn move_path(ctx: &Ctx, src: &Path, target: &Path) -> Result<()> { + if ctx.dry_run { + return Ok(()); + } + + ensure_parent(ctx, target)?; + + // rename cannot cross filesystems, and the store often is another one + match fs::rename(src, target) { + Ok(()) => Ok(()), + Err(rename_err) => match copy_recursive(src, target) { + Ok(()) => remove_recursive(ctx, src), + Err(copy_err) => Err(copy_err).with_context(|| format!("after {rename_err}")), + }, + } +} + +pub fn rename(ctx: &Ctx, src: &Path, target: &Path) -> Result<()> { + if ctx.dry_run { + return Ok(()); + } + + Ok(fs::rename(src, target)?) +} + +pub fn place_link(ctx: &Ctx, link_path: &Path, target: &Path) -> Result<()> { + if ctx.dry_run { + return Ok(()); + } + + // symlink under a temp name and rename over the path: the rename is atomic + let tmp = suffixed(link_path, ".ahab-tmp"); + let _ = fs::remove_file(&tmp); + + symlink(target, &tmp)?; + Ok(fs::rename(&tmp, link_path)?) +} + +pub fn remove_file(ctx: &Ctx, path: &Path) -> Result<()> { + if ctx.dry_run { + return Ok(()); + } + + Ok(fs::remove_file(path)?) +} + +// whatever is there, file, directory or symlink +fn remove_recursive(ctx: &Ctx, path: &Path) -> Result<()> { + if ctx.dry_run { + return Ok(()); + } + + if fs::symlink_metadata(path)?.is_dir() { + fs::remove_dir_all(path)?; + } else { + fs::remove_file(path)?; + } + + Ok(()) +} + +fn ensure_parent(ctx: &Ctx, target: &Path) -> Result<()> { + if ctx.dry_run { + return Ok(()); + } + + match target.parent() { + Some(parent) if !parent.as_os_str().is_empty() => Ok(fs::create_dir_all(parent)?), + _ => Ok(()), + } +} + +pub fn create_dir(ctx: &Ctx, path: &Path) -> Result<()> { + if ctx.dry_run { + return Ok(()); + } + + Ok(fs::create_dir(path)?) +} + +// created if it is missing, left as it is otherwise, which is what a caller +// making an empty __init__.py wants +pub fn touch(ctx: &Ctx, path: &Path) -> Result<()> { + if ctx.dry_run { + return Ok(()); + } + + fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(false) + .open(path)?; + + Ok(()) +} + +// fails rather than writing over a file that is already there +pub fn write_new(ctx: &Ctx, path: &Path, contents: &[u8]) -> Result<()> { + if ctx.dry_run { + return Ok(()); + } + + let mut file = fs::File::create_new(path)?; + file.write_all(contents)?; + + Ok(()) +} + +// a restored path can leave the store holding nothing but empty directories +pub fn prune_empty(ctx: &Ctx, dir: Option<&Path>, stop: &Path) { + if ctx.dry_run { + return; + } + + let mut dir = dir; + + while let Some(path) = dir { + if path == stop || !path.starts_with(stop) || fs::remove_dir(path).is_err() { + return; + } + + dir = path.parent(); + } +} + +fn copy_recursive(src: &Path, target: &Path) -> Result<()> { + let meta = fs::symlink_metadata(src)?; + + if meta.is_dir() { + fs::create_dir_all(target)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + copy_recursive(&entry.path(), &target.join(entry.file_name()))?; + } + + return Ok(()); + } + + if meta.is_symlink() { + return Ok(symlink(fs::read_link(src)?, target)?); + } + + fs::copy(src, target)?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 4db2ecc..b6e8934 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,12 +4,15 @@ mod cli; mod cmd; mod compose; mod ctx; +mod fsops; +mod output; mod scripts; use anyhow::Result; use clap::Parser; use crate::ctx::Ctx; +use crate::output::note; fn main() -> ExitCode { let args = cli::Ahab::parse(); @@ -19,8 +22,10 @@ fn main() -> ExitCode { dry_run: args.dry_run, }; + // said once here rather than by each command, so every line that follows + // reads as the plan it is if ctx.dry_run { - cli::reject_unsupported_dry_run(&args.command); + note!("dry run, nothing will be changed"); } match run(&ctx, args.command) { @@ -41,7 +46,7 @@ fn run(ctx: &Ctx, command: cli::Commands) -> Result { cli::Django::Bash => scripts::django::bash(ctx), cli::Django::Run { rest } => scripts::django::run(ctx, &rest), cli::Django::MakeCommand { app, name } => { - scripts::django::make_command(&app, &name) + scripts::django::make_command(ctx, &app, &name) } cli::Django::Makemigrations => scripts::django::makemigrations(ctx), cli::Django::Manage { rest } => scripts::django::manage(ctx, &rest), diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..7ef0543 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,11 @@ +// progress and warnings go to stderr, so what a caller pipes is only ever the +// data a command was asked for +macro_rules! note { + ($($arg:tt)*) => { eprintln!($($arg)*) }; +} + +macro_rules! warning { + ($($arg:tt)*) => { eprintln!("warning: {}", format_args!($($arg)*)) }; +} + +pub(crate) use {note, warning}; diff --git a/src/scripts/django.rs b/src/scripts/django.rs index 0170839..488e56c 100644 --- a/src/scripts/django.rs +++ b/src/scripts/django.rs @@ -1,5 +1,3 @@ -use std::fs::{File, OpenOptions, create_dir}; -use std::io::Write; use std::path::{Path, PathBuf}; use anyhow::{Result, anyhow}; @@ -7,6 +5,8 @@ use anyhow::{Result, anyhow}; use crate::cmd::{Bash, Cmd, Manage, Words}; use crate::compose::Compose; use crate::ctx::Ctx; +use crate::fsops::{create_dir, touch, write_new}; +use crate::output::note; const DEBUG_TEMPLATE: &str = r#"from django.core.management.base import BaseCommand @@ -16,7 +16,7 @@ class Command(BaseCommand): "#; -pub fn make_command(app: &PathBuf, name: &str) -> Result<()> { +pub fn make_command(ctx: &Ctx, app: &PathBuf, name: &str) -> Result<()> { let app_name = app.to_string_lossy(); let app_dir = Path::new(&app); @@ -25,32 +25,35 @@ pub fn make_command(app: &PathBuf, name: &str) -> Result<()> { return Err(anyhow!("directory {app_name} does not exist")); } - eprintln!("found app {app_name}"); + note!("found app {app_name}"); let management_dir = app_dir.join("management"); let not_management_exists = !management_dir.exists(); if not_management_exists { - create_dir(&management_dir)?; - create_file(management_dir.join("__init__.py"))?; + create_dir(ctx, &management_dir)?; + touch(ctx, &management_dir.join("__init__.py"))?; - eprintln!("created module {app_name}.management") + note!("created module {app_name}.management") }; let commands_dir = management_dir.join("commands"); let not_commands_exists = !commands_dir.exists(); if not_commands_exists { - create_dir(&commands_dir)?; - create_file(commands_dir.join("__init__.py"))?; + create_dir(ctx, &commands_dir)?; + touch(ctx, &commands_dir.join("__init__.py"))?; - eprintln!("created module {app_name}.management.commands") + note!("created module {app_name}.management.commands") }; - let mut file = safe_create_file(commands_dir.join(format!("{name}.py")))?; - file.write_all(DEBUG_TEMPLATE.as_bytes())?; + write_new( + ctx, + &commands_dir.join(format!("{name}.py")), + DEBUG_TEMPLATE.as_bytes(), + )?; - eprintln!("created command {app_name}.management.commands.{name}"); + note!("created command {app_name}.management.commands.{name}"); Ok(()) } @@ -89,17 +92,3 @@ pub fn test(ctx: &Ctx) -> Result<()> { fn service(ctx: &Ctx) -> Result { Compose::resolve(ctx)?.django() } - -fn safe_create_file(path: PathBuf) -> Result { - OpenOptions::new().write(true).create_new(true).open(path) -} - -// truncate(false) keeps an existing file's contents, which is what callers creating -// an empty __init__.py want -fn create_file(path: PathBuf) -> Result { - OpenOptions::new() - .write(true) - .create(true) - .truncate(false) - .open(path) -} diff --git a/src/scripts/link.rs b/src/scripts/link.rs index f1c402c..1ca306b 100644 --- a/src/scripts/link.rs +++ b/src/scripts/link.rs @@ -2,7 +2,6 @@ use std::cell::Cell; use std::env; use std::ffi::OsString; use std::fs; -use std::os::unix::fs::symlink; use std::path::{Path, PathBuf}; use std::process::ExitCode; @@ -11,6 +10,8 @@ use anyhow::{Context, Result, anyhow, bail}; use crate::cmd::{CheckIgnore, Cmd, ConfigGet, LsFiles, RevParse}; use crate::ctx::Ctx; +use crate::fsops::{move_path, place_link, prune_empty, remove_file, rename, suffixed}; +use crate::output::{note, warning}; const BACKUP_SUFFIX: &str = ".ahab-bak"; const LOCAL_NAMESPACE: &str = "_local"; @@ -27,7 +28,7 @@ pub fn add(ctx: &Ctx, paths: &[PathBuf], force: bool, store: Option<&Path>) -> R let mut failed = 0; for path in paths { if let Err(e) = link_one(ctx, &repo, path, force, &report) { - eprintln!("error: {e:#}"); + note!("error: {e:#}"); failed += 1; } } @@ -51,18 +52,18 @@ pub fn restore(ctx: &Ctx, paths: &[PathBuf], all: bool, store: Option<&Path>) -> }; if paths.is_empty() { - eprintln!("nothing in the store for this repository"); + note!("nothing in the store for this repository"); return Ok(()); } if let [path] = paths.as_slice() { - return restore_one(&repo, path, &report); + return restore_one(ctx, &repo, path, &report); } let mut failed = 0; for path in &paths { - if let Err(e) = restore_one(&repo, path, &report) { - eprintln!("error: {e:#}"); + if let Err(e) = restore_one(ctx, &repo, path, &report) { + note!("error: {e:#}"); failed += 1; } } @@ -73,7 +74,7 @@ pub fn restore(ctx: &Ctx, paths: &[PathBuf], all: bool, store: Option<&Path>) -> Ok(()) } -fn restore_one(repo: &Repo, path: &Path, report: &Report) -> Result<()> { +fn restore_one(ctx: &Ctx, repo: &Repo, path: &Path, report: &Report) -> Result<()> { let src = resolve(path)?; let rel = repo.relative(&src)?; let stored = repo.store.join(&rel); @@ -100,9 +101,9 @@ fn restore_one(repo: &Repo, path: &Path, report: &Report) -> Result<()> { bail!("{} is missing from the store", rel.display()); } - fs::remove_file(&src).with_context(|| format!("removing {}", src.display()))?; - move_path(&stored, &src)?; - prune_empty(stored.parent(), &repo.base); + remove_file(ctx, &src)?; + move_path(ctx, &stored, &src)?; + prune_empty(ctx, stored.parent(), &repo.base); report.line("restored", &rel); Ok(()) @@ -140,19 +141,6 @@ fn linked_paths(repo: &Repo, dir: &Path) -> Result> { Ok(found) } -// a restored path can leave the store holding nothing but empty directories -fn prune_empty(dir: Option<&Path>, stop: &Path) { - let mut dir = dir; - - while let Some(path) = dir { - if path == stop || !path.starts_with(stop) || fs::remove_dir(path).is_err() { - return; - } - - dir = path.parent(); - } -} - struct Report { store: PathBuf, named: Cell, @@ -176,10 +164,6 @@ impl Report { } } -fn warn(msg: impl std::fmt::Display) { - eprintln!("warning: {msg}"); -} - // list untracked paths not in the store, i.e. what a sandbox can still read pub fn check( ctx: &Ctx, @@ -479,7 +463,7 @@ fn link_one(ctx: &Ctx, repo: &Repo, path: &Path, force: bool, report: &Report) - )); } if !ignored(ctx, repo, &rel) { - warn(format!("{} is not gitignored", rel.display())); + warning!("{} is not gitignored", rel.display()); } let src_meta = symlink_metadata_opt(&src)?; @@ -505,14 +489,14 @@ fn link_one(ctx: &Ctx, repo: &Repo, path: &Path, force: bool, report: &Report) - // nothing in the store to adopt, so the symlink itself moves out if !target_taken { if !src.exists() { - warn(format!( + warning!( "{} is a broken symlink to {}", rel.display(), dest.display() - )); + ); } - move_path(&src, &target)?; - place_link(&src, &target)?; + move_path(ctx, &src, &target)?; + place_link(ctx, &src, &target)?; report.line("moved", &rel); return Ok(()); } @@ -521,7 +505,7 @@ fn link_one(ctx: &Ctx, repo: &Repo, path: &Path, force: bool, report: &Report) - return Err(needs_force(&target)); } - place_link(&src, &target)?; + place_link(ctx, &src, &target)?; report.line("repointed", &rel); Ok(()) } @@ -539,18 +523,17 @@ fn link_one(ctx: &Ctx, repo: &Repo, path: &Path, force: bool, report: &Report) - )); } - fs::rename(&src, &backup) - .with_context(|| format!("renaming {} to {}", src.display(), backup.display()))?; + rename(ctx, &src, &backup)?; report.line("saved", &suffixed(&rel, BACKUP_SUFFIX)); - place_link(&src, &target)?; + place_link(ctx, &src, &target)?; report.line("linked", &rel); Ok(()) } Some(_) => { - move_path(&src, &target)?; - place_link(&src, &target)?; + move_path(ctx, &src, &target)?; + place_link(ctx, &src, &target)?; report.line("moved", &rel); Ok(()) } @@ -559,7 +542,7 @@ fn link_one(ctx: &Ctx, repo: &Repo, path: &Path, force: bool, report: &Report) - if !force { return Err(needs_force(&target)); } - place_link(&src, &target)?; + place_link(ctx, &src, &target)?; report.line("linked", &rel); Ok(()) } @@ -612,7 +595,7 @@ fn repo_components(ctx: &Ctx, root: &Path) -> Result { if let Some(components) = components_from_remote(&url) { return Ok(components); } - eprintln!("could not parse git remote `{url}`, falling back to the checkout name"); + note!("could not parse git remote `{url}`, falling back to the checkout name"); } let name = root @@ -715,93 +698,6 @@ fn symlink_metadata_opt(path: &Path) -> Result> { } } -fn move_path(src: &Path, target: &Path) -> Result<()> { - ensure_parent(target)?; - - // rename cannot cross filesystems, and the store often is another one - match fs::rename(src, target) { - Ok(()) => Ok(()), - Err(rename_err) => match copy_recursive(src, target) { - Ok(()) => remove_recursive(src), - Err(copy_err) => Err(copy_err).with_context(|| { - format!( - "moving {} to {} (rename failed: {rename_err})", - src.display(), - target.display() - ) - }), - }, - } -} - -fn copy_recursive(src: &Path, target: &Path) -> Result<()> { - let meta = - fs::symlink_metadata(src).with_context(|| format!("inspecting {}", src.display()))?; - - if meta.is_dir() { - fs::create_dir_all(target) - .with_context(|| format!("creating directory {}", target.display()))?; - for entry in - fs::read_dir(src).with_context(|| format!("reading directory {}", src.display()))? - { - let entry = entry?; - copy_recursive(&entry.path(), &target.join(entry.file_name()))?; - } - return Ok(()); - } - - if meta.is_symlink() { - let dest = fs::read_link(src)?; - return symlink(&dest, target) - .with_context(|| format!("creating symlink {}", target.display())); - } - - fs::copy(src, target) - .with_context(|| format!("copying {} to {}", src.display(), target.display()))?; - Ok(()) -} - -fn remove_recursive(path: &Path) -> Result<()> { - let meta = - fs::symlink_metadata(path).with_context(|| format!("inspecting {}", path.display()))?; - - if meta.is_dir() { - fs::remove_dir_all(path) - } else { - fs::remove_file(path) - } - .with_context(|| format!("removing {}", path.display())) -} - -fn ensure_parent(target: &Path) -> Result<()> { - if let Some(parent) = target.parent() { - if !parent.as_os_str().is_empty() { - fs::create_dir_all(parent) - .with_context(|| format!("creating directory {}", parent.display()))?; - } - } - Ok(()) -} - -fn place_link(link_path: &Path, target: &Path) -> Result<()> { - // symlink under a temp name and rename over the path: the rename is atomic - let tmp = suffixed(link_path, ".ahab-tmp"); - let _ = fs::remove_file(&tmp); - - symlink(target, &tmp) - .with_context(|| format!("creating symlink {} -> {}", tmp.display(), target.display()))?; - fs::rename(&tmp, link_path) - .with_context(|| format!("replacing {} with a symlink", link_path.display()))?; - - Ok(()) -} - -fn suffixed(path: &Path, suffix: &str) -> PathBuf { - let mut out = path.as_os_str().to_owned(); - out.push(suffix); - PathBuf::from(out) -} - #[cfg(test)] mod tests { use super::{components_from_remote, sanitize}; diff --git a/src/scripts/postgres.rs b/src/scripts/postgres.rs index dbf7506..7e5bde4 100644 --- a/src/scripts/postgres.rs +++ b/src/scripts/postgres.rs @@ -1,8 +1,8 @@ use anyhow::{Context, Result, anyhow, bail}; use std::{ - fs::{self, File}, + fs::File, io::{self, Read, Write}, - path::{Path, PathBuf}, + path::Path, process::Stdio, thread, time::{Duration, Instant}, @@ -15,6 +15,8 @@ use crate::cmd::{ }; use crate::compose::Compose; use crate::ctx::Ctx; +use crate::fsops::{remove_file, rename, suffixed}; +use crate::output::note; const CUSTOM_MAGIC: &[u8] = b"PGDMP"; const TAR_MAGIC: &[u8] = b"toc.dat"; @@ -142,11 +144,11 @@ fn restore_cluster(ctx: &Ctx, db: &Database, restore: &dyn Cmd, file: &Path) -> continue; } - eprintln!("{line}"); + note!("{line}"); } if existing > 0 { - eprintln!( + note!( "left {existing} existing role{} alone", if existing == 1 { "" } else { "s" } ); @@ -159,13 +161,6 @@ fn restore_cluster(ctx: &Ctx, db: &Database, restore: &dyn Cmd, file: &Path) -> Ok(()) } -fn suffixed(path: &Path, suffix: &str) -> PathBuf { - let mut out = path.as_os_str().to_owned(); - out.push(suffix); - - PathBuf::from(out) -} - fn read_header(path: &Path) -> Result> { let mut file = File::open(path).with_context(|| format!("opening {}", path.display()))?; let mut header = vec![0; HEADER_LEN]; @@ -218,10 +213,10 @@ pub fn import(ctx: &Ctx, file: &Path) -> Result<()> { let dump = Dump::of(file)?; let db = Database::resolve(ctx)?; - eprintln!("stopping all containers"); + note!("stopping all containers"); Stop.run(ctx)?; - eprintln!("starting db container"); + note!("starting db container"); Start::service(&db.service).run(ctx)?; let remote = remote_dump(); @@ -271,7 +266,7 @@ pub fn import(ctx: &Ctx, file: &Path) -> Result<()> { }; let tool = kind.tool(); - eprintln!("restoring database with {tool}"); + note!("restoring database with {tool}"); when_ready( ctx, @@ -299,7 +294,7 @@ pub fn import(ctx: &Ctx, file: &Path) -> Result<()> { wait_until_ready(ctx, &db)?; if ctx.dry_run { - eprintln!("would restore with {tool}"); + note!("would restore with {tool}"); } else { // a directory dump was copied in whole, so pg_restore reads it from the // container; every other shape is fed in on stdin, through gunzip when it @@ -341,14 +336,14 @@ pub fn import(ctx: &Ctx, file: &Path) -> Result<()> { let _ = Rm::recursive(&remote).in_container(&db.container).run(ctx); } - eprintln!("restarting containers"); + note!("restarting containers"); Stop.run(ctx)?; Up.run(ctx)?; Ok(()) } -pub fn dump(ctx: &Ctx, file: &PathBuf, format: Format, gzip: bool) -> Result<()> { +pub fn dump(ctx: &Ctx, file: &Path, format: Format, gzip: bool) -> Result<()> { let db = Database::resolve(ctx)?; if format == Format::Directory { @@ -359,7 +354,7 @@ pub fn dump(ctx: &Ctx, file: &PathBuf, format: Format, gzip: bool) -> Result<()> return dump_directory(ctx, &db, file); } - eprintln!("dumping to local file {}", file.to_string_lossy()); + note!("dumping to local file {}", file.to_string_lossy()); // written beside the target and renamed once the dump succeeds, so a failure // cannot destroy the dump that is already there @@ -385,16 +380,11 @@ pub fn dump(ctx: &Ctx, file: &PathBuf, format: Format, gzip: bool) -> Result<()> }; if let Err(e) = dumped { - let _ = fs::remove_file(&partial); + let _ = remove_file(ctx, &partial); return Err(e); } - if ctx.dry_run { - return Ok(()); - } - - fs::rename(&partial, file) - .with_context(|| format!("renaming {} to {}", partial.display(), file.display()))?; + rename(ctx, &partial, file)?; Ok(()) } @@ -417,7 +407,7 @@ fn dump_directory(ctx: &Ctx, db: &Database, target: &Path) -> Result<()> { ); } - eprintln!("dumping to local directory {}", target.display()); + note!("dumping to local directory {}", target.display()); let remote = remote_dump(); PgDump::new(&db.user, &db.name, "d")