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/<name>.toml, aggregated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
14
src/cli.rs
14
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<String>,
|
||||
|
||||
/// Import profiles from a file.
|
||||
#[arg(short, long, value_name = "PATH")]
|
||||
pub import: Option<String>,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
51
src/commands/transfer.rs
Normal file
51
src/commands/transfer.rs
Normal file
@@ -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<Profile>,
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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(())
|
||||
|
||||
Reference in New Issue
Block a user