From 125d72c9187a7aee1a7f21cad63ed18143f3022c Mon Sep 17 00:00:00 2001 From: Overlord Date: Thu, 13 Aug 2026 22:46:57 +0200 Subject: [PATCH] Add transfer subcommand for bulk profile export/import -e/--export writes every saved profile to a single TOML file as a [[profile]] array; -i/--import reads one back and saves each entry, failing on the first name collision rather than silently overwriting. Reuses Profile's existing Serialize/Deserialize impl directly, so the file format is just the same shape already written to ~/.config/porthole/profiles/.toml, aggregated. Co-Authored-By: Claude Sonnet 5 --- src/cli.rs | 14 +++++++++++ src/commands/mod.rs | 1 + src/commands/transfer.rs | 51 ++++++++++++++++++++++++++++++++++++++++ src/error.rs | 6 +++++ src/main.rs | 1 + 5 files changed, 73 insertions(+) create mode 100644 src/commands/transfer.rs diff --git a/src/cli.rs b/src/cli.rs index 13f33d3..104517e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -41,6 +41,9 @@ pub enum Commands { #[command(visible_alias = "reset")] Wipe(WipeArgs), + /// Export saved profiles to a file, or import them from one. + Transfer(TransferArgs), + /// Generate a shell completion script. Completions { shell: Shell }, @@ -182,3 +185,14 @@ pub struct WipeArgs { #[arg(short, long)] pub yes: bool, } + +#[derive(Args)] +pub struct TransferArgs { + /// Export all saved profiles to a file. + #[arg(short, long, value_name = "PATH")] + pub export: Option, + + /// Import profiles from a file. + #[arg(short, long, value_name = "PATH")] + pub import: Option, +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 32743b8..93340b9 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,6 +6,7 @@ pub mod list; pub mod open; pub mod remove; pub mod status; +pub mod transfer; pub mod wipe; use crate::cli::MappingArgs; diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs new file mode 100644 index 0000000..692a36e --- /dev/null +++ b/src/commands/transfer.rs @@ -0,0 +1,51 @@ +//! Bulk profile backup/restore - a flat TOML array of the same `Profile` +//! records `profile::save`/`load` already read and write, so it round-trips +//! through the exact same serialization with nothing profile-specific here. + +use crate::cli::TransferArgs; +use crate::error::{PortholeError, Result}; +use crate::profile::{self, Profile}; +use crate::{atomic, ui}; +use serde::{Deserialize, Serialize}; +use std::path::Path; + +#[derive(Default, Serialize, Deserialize)] +struct TransferFile { + #[serde(rename = "profile", default)] + profiles: Vec, +} + +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(path)) => import(path), + } +} + +fn export(path: &str) -> Result<()> { + let file = TransferFile { profiles: profile::list_all()? }; + 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())); + Ok(()) +} + +fn import(path: &str) -> Result<()> { + let text = std::fs::read_to_string(path)?; + let file: TransferFile = toml::from_str(&text)?; + + for p in &file.profiles { + profile::require_valid_name(&p.name)?; + if profile::exists(&p.name) { + return Err(PortholeError::AlreadyExists(p.name.clone())); + } + } + for p in &file.profiles { + profile::save(p)?; + } + + ui::ok(&format!("Imported {} profile(s) from '{path}'.", file.profiles.len())); + Ok(()) +} diff --git a/src/error.rs b/src/error.rs index db64960..49ff4d1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -29,6 +29,12 @@ pub enum PortholeError { #[error("--via is required: at least one hop (connection target)")] NoViaHosts, + #[error("exactly one of -i/--import, -e/--export is required")] + TransferNoMode, + + #[error("only one of -i/--import, -e/--export may be given")] + TransferConflictingMode, + #[error("nothing to do: {0}")] NothingToDo(String), diff --git a/src/main.rs b/src/main.rs index c3fb680..22d9f43 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,6 +37,7 @@ fn main() { Commands::List(args) => commands::list::run(args), Commands::Remove(args) => commands::remove::run(args), Commands::Wipe(args) => commands::wipe::run(args), + Commands::Transfer(args) => commands::transfer::run(args), Commands::Completions { shell } => { commands::completions::run(shell); Ok(())