use std::fs;
#[cfg(feature = "alias")]
use std::path::Path;
use anyhow::Result;
use clap::ArgMatches;
use prs_lib::{Secret, Store};
use thiserror::Error;
use crate::cmd::matcher::{r#move::MoveMatcher, MainMatcher, Matcher};
#[cfg(all(feature = "tomb", target_os = "linux"))]
use crate::util::tomb;
use crate::util::{cli, error, select, sync};
pub struct Move<'a> {
cmd_matches: &'a ArgMatches,
}
impl<'a> Move<'a> {
pub fn new(cmd_matches: &'a ArgMatches) -> Self {
Self { cmd_matches }
}
pub fn invoke(&self) -> Result<()> {
let matcher_main = MainMatcher::with(self.cmd_matches).unwrap();
let matcher_move = MoveMatcher::with(self.cmd_matches).unwrap();
let store = Store::open(matcher_main.store()).map_err(Err::Store)?;
#[cfg(all(feature = "tomb", target_os = "linux"))]
let mut tomb = store.tomb(
!matcher_main.verbose(),
matcher_main.verbose(),
matcher_main.force(),
);
let sync = store.sync();
#[cfg(all(feature = "tomb", target_os = "linux"))]
tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?;
sync::ensure_ready(&sync, matcher_move.allow_dirty());
if !matcher_move.no_sync() {
sync.prepare()?;
}
let secret =
select::store_select_secret(&store, matcher_move.query()).ok_or(Err::NoneSelected)?;
let dest = matcher_move.destination();
let path = store
.normalize_secret_path(dest, secret.path.file_name().and_then(|p| p.to_str()), true)
.map_err(Err::NormalizePath)?;
let new_secret = Secret::from(&store, path.clone());
if !matcher_main.force() && path.is_file() {
eprintln!("A secret at '{}' already exists", path.display(),);
if !cli::prompt_yes("Overwrite?", Some(true), &matcher_main) {
if matcher_main.verbose() {
eprintln!("Move cancelled");
}
error::quit();
}
}
#[cfg(feature = "alias")]
{
update_secret_alias_target(&store, &secret, &new_secret)?;
update_alias_for_secret_to(&store, &secret, &new_secret);
}
fs::rename(&secret.path, path)
.map(|_| ())
.map_err(Err::Move)?;
super::remove::remove_empty_secret_dir(&secret);
if !matcher_move.no_sync() {
sync.finalize(format!("Move from {} to {}", secret.name, new_secret.name))?;
}
#[cfg(all(feature = "tomb", target_os = "linux"))]
tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?;
if !matcher_main.quiet() {
eprintln!("Secret moved");
}
Ok(())
}
}
#[cfg(feature = "alias")]
fn update_secret_alias_target(
store: &Store,
secret: &Secret,
future_secret: &Secret,
) -> Result<bool> {
if !secret.path.symlink_metadata()?.file_type().is_symlink() {
return Ok(false);
}
let target = fs::read_link(&secret.path).map_err(Err::UpdateAlias)?;
let target = secret
.path
.parent()
.unwrap()
.join(target)
.canonicalize()
.map_err(Err::UpdateAlias)?;
let target = Secret::from(store, target);
update_alias(store, &target, &secret.path, &future_secret.path)?;
Ok(true)
}
#[cfg(feature = "alias")]
fn update_alias_for_secret_to(store: &Store, secret: &Secret, new_secret: &Secret) {
for secret in super::remove::find_symlinks_to(store, secret) {
if let Err(err) = update_alias(store, new_secret, &secret.path, &secret.path) {
error::print_error(
err.context("failed to update path of alias that points to moved secret, ignoring"),
);
}
}
}
#[cfg(feature = "alias")]
fn update_alias(store: &Store, src: &Secret, symlink: &Path, future_symlink: &Path) -> Result<()> {
assert!(
symlink.symlink_metadata()?.file_type().is_symlink(),
"failed to update symlink, not a symlink"
);
fs::remove_file(symlink)
.map(|_| ())
.map_err(Err::UpdateAlias)?;
super::alias::create_alias(store, src, future_symlink, symlink)?;
Ok(())
}
#[derive(Debug, Error)]
pub enum Err {
#[error("failed to access password store")]
Store(#[source] anyhow::Error),
#[cfg(all(feature = "tomb", target_os = "linux"))]
#[error("failed to prepare password store tomb for usage")]
Tomb(#[source] anyhow::Error),
#[error("no secret selected")]
NoneSelected,
#[error("failed to normalize destination path")]
NormalizePath(#[source] anyhow::Error),
#[error("failed to move secret file")]
Move(#[source] std::io::Error),
#[cfg(feature = "alias")]
#[error("failed to update alias")]
UpdateAlias(#[source] std::io::Error),
}