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")]
|
#[command(visible_alias = "reset")]
|
||||||
Wipe(WipeArgs),
|
Wipe(WipeArgs),
|
||||||
|
|
||||||
|
/// Export saved profiles to a file, or import them from one.
|
||||||
|
Transfer(TransferArgs),
|
||||||
|
|
||||||
/// Generate a shell completion script.
|
/// Generate a shell completion script.
|
||||||
Completions { shell: Shell },
|
Completions { shell: Shell },
|
||||||
|
|
||||||
@@ -182,3 +185,14 @@ pub struct WipeArgs {
|
|||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub yes: bool,
|
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 open;
|
||||||
pub mod remove;
|
pub mod remove;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
|
pub mod transfer;
|
||||||
pub mod wipe;
|
pub mod wipe;
|
||||||
|
|
||||||
use crate::cli::MappingArgs;
|
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)")]
|
#[error("--via is required: at least one hop (connection target)")]
|
||||||
NoViaHosts,
|
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}")]
|
#[error("nothing to do: {0}")]
|
||||||
NothingToDo(String),
|
NothingToDo(String),
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ fn main() {
|
|||||||
Commands::List(args) => commands::list::run(args),
|
Commands::List(args) => commands::list::run(args),
|
||||||
Commands::Remove(args) => commands::remove::run(args),
|
Commands::Remove(args) => commands::remove::run(args),
|
||||||
Commands::Wipe(args) => commands::wipe::run(args),
|
Commands::Wipe(args) => commands::wipe::run(args),
|
||||||
|
Commands::Transfer(args) => commands::transfer::run(args),
|
||||||
Commands::Completions { shell } => {
|
Commands::Completions { shell } => {
|
||||||
commands::completions::run(shell);
|
commands::completions::run(shell);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user