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)]
|
||||
pub struct TransferArgs {
|
||||
/// Export all saved profiles to a file.
|
||||
#[arg(short, long, value_name = "PATH")]
|
||||
/// Name of the profile to export; every saved profile if omitted. Not used for import.
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Export saved profiles to a file.
|
||||
#[arg(short, long, value_name = "PATH.toml")]
|
||||
pub export: Option<String>,
|
||||
|
||||
/// Import profiles from a file.
|
||||
#[arg(short, long, value_name = "PATH")]
|
||||
#[arg(short, long, value_name = "PATH.toml")]
|
||||
pub import: Option<String>,
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::error::{PortholeError, Result};
|
||||
use crate::profile::{self, Profile};
|
||||
use crate::{atomic, ui};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
struct TransferFile {
|
||||
@@ -19,16 +19,32 @@ pub fn run(args: TransferArgs) -> Result<()> {
|
||||
match (&args.export, &args.import) {
|
||||
(Some(_), Some(_)) => Err(PortholeError::TransferConflictingMode),
|
||||
(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),
|
||||
}
|
||||
}
|
||||
|
||||
fn export(path: &str) -> Result<()> {
|
||||
let file = TransferFile { profiles: profile::list_all()? };
|
||||
fn export(path: &str, name: Option<&str>) -> Result<()> {
|
||||
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)?;
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -47,5 +63,60 @@ fn import(path: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
ui::ok(&format!("Imported {} profile(s) from '{path}'.", file.profiles.len()));
|
||||
warn_about_missing_identities(&file.profiles);
|
||||
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")]
|
||||
TransferConflictingMode,
|
||||
|
||||
#[error("a profile name only applies to -e/--export, not -i/--import")]
|
||||
TransferNameWithImport,
|
||||
|
||||
#[error("nothing to do: {0}")]
|
||||
NothingToDo(String),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user