refactor: put every filesystem write behind one module
This commit is contained in:
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
165
src/fsops.rs
Normal file
165
src/fsops.rs
Normal file
@@ -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(())
|
||||
}
|
||||
@@ -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<ExitCode> {
|
||||
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),
|
||||
|
||||
11
src/output.rs
Normal file
11
src/output.rs
Normal file
@@ -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};
|
||||
@@ -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<String> {
|
||||
Compose::resolve(ctx)?.django()
|
||||
}
|
||||
|
||||
fn safe_create_file(path: PathBuf) -> Result<File, std::io::Error> {
|
||||
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<File, std::io::Error> {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(path)
|
||||
}
|
||||
|
||||
@@ -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<Vec<PathBuf>> {
|
||||
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<bool>,
|
||||
@@ -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<PathBuf> {
|
||||
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<Option<fs::Metadata>> {
|
||||
}
|
||||
}
|
||||
|
||||
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};
|
||||
|
||||
@@ -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<Vec<u8>> {
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user