Fix transfer rough edges: value-name hint, identity warnings, single-profile export
- --help now shows <PATH.toml> instead of the format-agnostic <PATH>. - Export warns (naming the affected profiles) that identity files aren't included, only their local paths; import warns per-profile when an identity path doesn't resolve on the importing machine, expanding a leading ~/ the same way ssh.rs does so that check isn't a false positive for tilde paths. - transfer now takes an optional profile-name positional (same shape as OpenArgs.name) so -e/--export can target a single profile instead of always dumping every saved one; combining it with -i/--import is rejected with a new TransferNameWithImport error. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -188,11 +188,14 @@ pub struct WipeArgs {
|
|||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct TransferArgs {
|
pub struct TransferArgs {
|
||||||
/// Export all saved profiles to a file.
|
/// Name of the profile to export; every saved profile if omitted. Not used for import.
|
||||||
#[arg(short, long, value_name = "PATH")]
|
pub name: Option<String>,
|
||||||
|
|
||||||
|
/// Export saved profiles to a file.
|
||||||
|
#[arg(short, long, value_name = "PATH.toml")]
|
||||||
pub export: Option<String>,
|
pub export: Option<String>,
|
||||||
|
|
||||||
/// Import profiles from a file.
|
/// Import profiles from a file.
|
||||||
#[arg(short, long, value_name = "PATH")]
|
#[arg(short, long, value_name = "PATH.toml")]
|
||||||
pub import: Option<String>,
|
pub import: Option<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use crate::error::{PortholeError, Result};
|
|||||||
use crate::profile::{self, Profile};
|
use crate::profile::{self, Profile};
|
||||||
use crate::{atomic, ui};
|
use crate::{atomic, ui};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize)]
|
#[derive(Default, Serialize, Deserialize)]
|
||||||
struct TransferFile {
|
struct TransferFile {
|
||||||
@@ -19,16 +19,32 @@ pub fn run(args: TransferArgs) -> Result<()> {
|
|||||||
match (&args.export, &args.import) {
|
match (&args.export, &args.import) {
|
||||||
(Some(_), Some(_)) => Err(PortholeError::TransferConflictingMode),
|
(Some(_), Some(_)) => Err(PortholeError::TransferConflictingMode),
|
||||||
(None, None) => Err(PortholeError::TransferNoMode),
|
(None, None) => Err(PortholeError::TransferNoMode),
|
||||||
(Some(path), None) => export(path),
|
(None, Some(_)) if args.name.is_some() => Err(PortholeError::TransferNameWithImport),
|
||||||
|
(Some(path), None) => export(path, args.name.as_deref()),
|
||||||
(None, Some(path)) => import(path),
|
(None, Some(path)) => import(path),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn export(path: &str) -> Result<()> {
|
fn export(path: &str, name: Option<&str>) -> Result<()> {
|
||||||
let file = TransferFile { profiles: profile::list_all()? };
|
let profiles = match name {
|
||||||
|
Some(n) => {
|
||||||
|
let n = profile::normalize(n);
|
||||||
|
vec![profile::load(&n)?]
|
||||||
|
}
|
||||||
|
None => profile::list_all()?,
|
||||||
|
};
|
||||||
|
|
||||||
|
warn_about_identities(&profiles);
|
||||||
|
|
||||||
|
let count = profiles.len();
|
||||||
|
let file = TransferFile { profiles };
|
||||||
let text = toml::to_string_pretty(&file)?;
|
let text = toml::to_string_pretty(&file)?;
|
||||||
atomic::write(Path::new(path), text.as_bytes())?;
|
atomic::write(Path::new(path), text.as_bytes())?;
|
||||||
ui::ok(&format!("Exported {} profile(s) to '{path}'.", file.profiles.len()));
|
|
||||||
|
match name {
|
||||||
|
Some(n) => ui::ok(&format!("Exported profile '{n}' to '{path}'.")),
|
||||||
|
None => ui::ok(&format!("Exported {count} profile(s) to '{path}'.")),
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,5 +63,60 @@ fn import(path: &str) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ui::ok(&format!("Imported {} profile(s) from '{path}'.", file.profiles.len()));
|
ui::ok(&format!("Imported {} profile(s) from '{path}'.", file.profiles.len()));
|
||||||
|
warn_about_missing_identities(&file.profiles);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Identity files are never included in the export, only the path - warn so
|
||||||
|
/// that doesn't come as a surprise on the importing end.
|
||||||
|
fn warn_about_identities(profiles: &[Profile]) {
|
||||||
|
let names: Vec<&str> = profiles.iter().filter(|p| p.identity.is_some()).map(|p| p.name.as_str()).collect();
|
||||||
|
if !names.is_empty() {
|
||||||
|
ui::warn(&format!(
|
||||||
|
"identity files are not included in the export ({}) - copy them to the importing machine yourself",
|
||||||
|
names.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// After import, flag any profile whose identity path doesn't resolve on
|
||||||
|
/// this machine - the most likely sign of a not-yet-copied key file.
|
||||||
|
fn warn_about_missing_identities(profiles: &[Profile]) {
|
||||||
|
for p in profiles {
|
||||||
|
if let Some(identity) = &p.identity {
|
||||||
|
if !expand_home(identity).is_file() {
|
||||||
|
ui::warn(&format!(
|
||||||
|
"'{}': identity file '{identity}' not found on this machine - fix it with \
|
||||||
|
'porthole edit {} -i <path>' before opening",
|
||||||
|
p.name, p.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expands a leading `~/` the same way `ssh` itself does at spawn time
|
||||||
|
/// (`ssh.rs`) - without this, a valid `~/...` identity path would be
|
||||||
|
/// misreported as missing since `Path::is_file` never expands `~` on its own.
|
||||||
|
fn expand_home(path: &str) -> PathBuf {
|
||||||
|
match path.strip_prefix("~/").zip(dirs::home_dir()) {
|
||||||
|
Some((rest, home)) => home.join(rest),
|
||||||
|
None => PathBuf::from(path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expands_leading_tilde() {
|
||||||
|
let home = dirs::home_dir().unwrap();
|
||||||
|
assert_eq!(expand_home("~/.ssh/id"), home.join(".ssh/id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leaves_absolute_path_untouched() {
|
||||||
|
assert_eq!(expand_home("/etc/ssh/id"), PathBuf::from("/etc/ssh/id"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ pub enum PortholeError {
|
|||||||
#[error("only one of -i/--import, -e/--export may be given")]
|
#[error("only one of -i/--import, -e/--export may be given")]
|
||||||
TransferConflictingMode,
|
TransferConflictingMode,
|
||||||
|
|
||||||
|
#[error("a profile name only applies to -e/--export, not -i/--import")]
|
||||||
|
TransferNameWithImport,
|
||||||
|
|
||||||
#[error("nothing to do: {0}")]
|
#[error("nothing to do: {0}")]
|
||||||
NothingToDo(String),
|
NothingToDo(String),
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user