use clap::Parser;
use qssh::{QsshConfig, PqAlgorithm, QsshClient, security_tiers::SecurityTier};
use qssh::sftp_client::SftpClient;
use std::path::Path;
use tokio::fs;
#[derive(Parser, Debug)]
#[clap(name = "qscp")]
#[clap(about = "Quantum-Secure Copy - Transfer files with post-quantum security")]
#[clap(version = env!("CARGO_PKG_VERSION"))]
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"))
.target(env_logger::Target::Stderr)
.init();
} else {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn"))
.target(env_logger::Target::Stderr)
.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);
}
}
fn make_config(host: &str, port: u16, username: String, qkd: bool) -> QsshConfig {
QsshConfig {
server: format!("{}:{}", host, port),
username,
password: None,
port_forwards: vec![],
use_qkd: qkd,
qkd_endpoint: None,
qkd_cert_path: None,
qkd_key_path: None,
qkd_ca_path: None,
pq_algorithm: PqAlgorithm::Falcon512,
kex_algorithm: qssh::KexAlgorithm::default(),
key_rotation_interval: 3600,
security_tier: SecurityTier::default(),
quantum_native: true,
}
}
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 = make_config(&host, args.port, username, args.qkd);
let mut client = QsshClient::new(config);
client.connect().await?;
let transport = client.transport()
.ok_or("Not connected")?
.clone();
let mut sftp = SftpClient::connect(transport).await?;
for source in &args.source {
let path = Path::new(source);
if path.is_file() {
upload_file(&mut sftp, path, &remote_path, args.progress).await?;
} else if path.is_dir() && args.recursive {
upload_directory(&mut sftp, &client, path, &remote_path, args.progress).await?;
} else if path.is_dir() {
eprintln!("Skipping {}: is a directory (use -r for recursive)", source);
} else {
eprintln!("Skipping {}: not a regular file", source);
}
}
sftp.close().await?;
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 = make_config(&host, args.port, username, args.qkd);
let mut client = QsshClient::new(config);
client.connect().await?;
let transport = client.transport()
.ok_or("Not connected")?
.clone();
let mut sftp = SftpClient::connect(transport).await?;
download_file(&mut sftp, &remote_path, &args.destination, args.progress).await?;
sftp.close().await?;
client.disconnect().await?;
Ok(())
}
async fn upload_file(
sftp: &mut SftpClient,
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 {
eprintln!("Uploading {} ({} bytes)", local_path.display(), file_size);
}
let contents = fs::read(local_path).await?;
let dest = if remote_path.ends_with('/') {
let name = local_path.file_name()
.ok_or("No filename")?
.to_string_lossy();
format!("{}{}", remote_path, name)
} else {
remote_path.to_string()
};
sftp.upload(&contents, &dest).await?;
if show_progress {
eprintln!("Upload complete: {} -> {} ({} bytes)", local_path.display(), dest, file_size);
}
Ok(())
}
async fn download_file(
sftp: &mut SftpClient,
remote_path: &str,
local_path: &str,
show_progress: bool,
) -> Result<(), Box<dyn std::error::Error>> {
if show_progress {
eprintln!("Downloading {} to {}", remote_path, local_path);
}
let contents = sftp.download(remote_path).await?;
fs::write(local_path, &contents).await?;
if show_progress {
eprintln!("Download complete: {} ({} bytes)", local_path, contents.len());
}
Ok(())
}
async fn upload_directory(
sftp: &mut SftpClient,
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(sftp, &path, &remote_file, show_progress).await?;
} else if path.is_dir() {
Box::pin(upload_directory(sftp, 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()))
}