use clap::Parser;
use qssh::{QsshConfig, PqAlgorithm, QsshClient};
use std::path::{Path, PathBuf};
use tokio::fs;
#[derive(Parser, Debug)]
#[clap(name = "qscp")]
#[clap(about = "Quantum-Secure Copy - Transfer files with post-quantum security")]
#[clap(version = "0.1.0")]
struct Args {
source: Vec<String>,
destination: String,
#[clap(short = 'P', long, default_value = "22222")]
port: u16,
#[clap(long)]
qkd: bool,
#[clap(short, long)]
recursive: bool,
#[clap(short, long)]
preserve: bool,
#[clap(short, long)]
verbose: bool,
#[clap(long)]
progress: bool,
}
#[tokio::main]
async fn main() {
let args = Args::parse();
if args.verbose {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init();
} else {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
}
let result = if args.destination.contains(':') {
upload_files(&args).await
} else if args.source.iter().any(|s| s.contains(':')) {
download_files(&args).await
} else {
eprintln!("Error: Either source or destination must be remote (user@host:path)");
std::process::exit(1);
};
if let Err(e) = result {
eprintln!("Transfer failed: {}", e);
std::process::exit(1);
}
}
async fn upload_files(args: &Args) -> Result<(), Box<dyn std::error::Error>> {
let (remote, remote_path) = parse_remote(&args.destination)?;
let (username, host) = parse_user_host(&remote)?;
let config = QsshConfig {
server: format!("{}:{}", host, args.port),
username,
password: None,
port_forwards: vec![],
use_qkd: args.qkd,
qkd_endpoint: None,
qkd_cert_path: None,
qkd_key_path: None,
qkd_ca_path: None,
pq_algorithm: PqAlgorithm::Falcon512,
key_rotation_interval: 3600,
};
let mut client = QsshClient::new(config);
client.connect().await?;
for source in &args.source {
let path = Path::new(source);
if path.is_file() {
upload_file(&client, path, &remote_path, args.progress).await?;
} else if path.is_dir() && args.recursive {
upload_directory(&client, path, &remote_path, args.progress).await?;
} else {
eprintln!("Skipping {}: not a regular file", source);
}
}
client.disconnect().await?;
Ok(())
}
async fn download_files(args: &Args) -> Result<(), Box<dyn std::error::Error>> {
let remote_source = args.source.iter()
.find(|s| s.contains(':'))
.ok_or("No remote source found")?;
let (remote, remote_path) = parse_remote(remote_source)?;
let (username, host) = parse_user_host(&remote)?;
let config = QsshConfig {
server: format!("{}:{}", host, args.port),
username,
password: None,
port_forwards: vec![],
use_qkd: args.qkd,
qkd_endpoint: None,
qkd_cert_path: None,
qkd_key_path: None,
qkd_ca_path: None,
pq_algorithm: PqAlgorithm::Falcon512,
key_rotation_interval: 3600,
};
let mut client = QsshClient::new(config);
client.connect().await?;
download_file(&client, &remote_path, &args.destination, args.progress).await?;
client.disconnect().await?;
Ok(())
}
async fn upload_file(
client: &QsshClient,
local_path: &Path,
remote_path: &str,
show_progress: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let metadata = fs::metadata(local_path).await?;
let file_size = metadata.len();
if show_progress {
println!("Uploading {} ({} bytes)", local_path.display(), file_size);
}
let contents = fs::read(local_path).await?;
use base64::Engine;
let remote_cmd = format!(
"cat > {} << 'EOF'\n{}\nEOF",
remote_path,
base64::engine::general_purpose::STANDARD.encode(&contents)
);
client.exec(&remote_cmd).await?;
if show_progress {
println!("Upload complete: {}", local_path.display());
}
Ok(())
}
async fn download_file(
client: &QsshClient,
remote_path: &str,
local_path: &str,
show_progress: bool,
) -> Result<(), Box<dyn std::error::Error>> {
if show_progress {
println!("Downloading {} to {}", remote_path, local_path);
}
let output = client.exec(&format!("base64 {}", remote_path)).await?;
use base64::Engine;
let contents = base64::engine::general_purpose::STANDARD.decode(output.trim())?;
fs::write(local_path, contents).await?;
if show_progress {
println!("Download complete: {}", local_path);
}
Ok(())
}
async fn upload_directory(
client: &QsshClient,
local_path: &Path,
remote_path: &str,
show_progress: bool,
) -> Result<(), Box<dyn std::error::Error>> {
client.exec(&format!("mkdir -p {}", remote_path)).await?;
let mut entries = fs::read_dir(local_path).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
let name = entry.file_name();
let remote_file = format!("{}/{}", remote_path, name.to_string_lossy());
if path.is_file() {
upload_file(client, &path, &remote_file, show_progress).await?;
} else if path.is_dir() {
Box::pin(upload_directory(client, &path, &remote_file, show_progress)).await?;
}
}
Ok(())
}
fn parse_remote(spec: &str) -> Result<(String, String), Box<dyn std::error::Error>> {
let parts: Vec<&str> = spec.rsplitn(2, ':').collect();
if parts.len() != 2 {
return Err("Invalid remote specification".into());
}
Ok((parts[1].to_string(), parts[0].to_string()))
}
fn parse_user_host(remote: &str) -> Result<(String, String), Box<dyn std::error::Error>> {
let parts: Vec<&str> = remote.split('@').collect();
if parts.len() != 2 {
return Err("Invalid user@host format".into());
}
Ok((parts[0].to_string(), parts[1].to_string()))
}