use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use a7n_ipc::{
FileTransfer, HeldFiles, IpcClient, MachineId, Request, Response, SendOutcome, StatusInfo,
TransferDirection, format_size,
};
use anyhow::{Context, Result, bail};
use super::{connect_client, resolve_machine};
#[derive(Debug, clap::Subcommand)]
pub(super) enum Command {
Accept {
peer: String,
},
Reject {
peer: String,
},
Cancel { id: u64 },
}
pub(super) async fn run(command: Command) -> Result<()> {
let request = match command {
Command::Accept { peer } => Request::FilesAccept {
peer: resolve_machine(&peer).await?,
},
Command::Reject { peer } => Request::FilesReject {
peer: resolve_machine(&peer).await?,
},
Command::Cancel { id } => Request::CancelTransfer { id },
};
expect_done(connect_client().await?.request(&request).await?)
}
fn expect_done(response: Response) -> Result<()> {
match response {
Response::Done => Ok(()),
Response::Error { message, .. } => bail!("{message}"),
_ => bail!("unexpected response from daemon"),
}
}
fn peer_label(status: &StatusInfo, id: MachineId) -> String {
status
.peers
.iter()
.find(|peer| peer.id == id)
.map_or_else(|| id.to_string(), |peer| peer.name.clone())
}
fn direction_label(direction: TransferDirection) -> &'static str {
match direction {
TransferDirection::Send => "send",
TransferDirection::Receive => "receive",
}
}
pub(super) fn write_held(
output: &mut impl Write,
status: &StatusInfo,
held: &HeldFiles,
) -> io::Result<()> {
writeln!(
output,
"files waiting for confirmation: {} wants to send {} files, {} (a7n files accept {})",
peer_label(status, held.peer),
held.files,
format_size(held.total_size),
held.peer,
)
}
pub(super) fn write_transfer(
output: &mut impl Write,
status: &StatusInfo,
transfer: &FileTransfer,
) -> io::Result<()> {
write!(
output,
"transfer {} {} {}: ",
transfer.id,
direction_label(transfer.direction),
peer_label(status, transfer.peer),
)?;
write_progress(output, transfer)
}
fn write_progress(output: &mut impl Write, transfer: &FileTransfer) -> io::Result<()> {
writeln!(
output,
"{}/{} files, {} / {}",
transfer.files_done,
transfer.files_total,
format_size(transfer.bytes_done),
format_size(transfer.bytes_total),
)
}
const POLL_INTERVAL: Duration = Duration::from_millis(250);
const OFFER_WAIT: Duration = Duration::from_secs(65);
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Poll<'a> {
Progress(&'a FileTransfer),
Finished(SendOutcome),
Waiting,
}
pub(super) fn poll_send(status: &StatusInfo, offer_id: u64) -> Poll<'_> {
if let Some(finished) = status
.finished_sends
.iter()
.find(|finished| finished.offer_id == offer_id)
{
return Poll::Finished(finished.outcome);
}
status
.transfers
.iter()
.find(|transfer| transfer.offer_id == Some(offer_id))
.map_or(Poll::Waiting, Poll::Progress)
}
pub(super) async fn send(target: &str, paths: &[PathBuf], wait: bool) -> Result<()> {
let peer = resolve_machine(target).await?;
let paths = paths
.iter()
.map(|path| absolute_path(path))
.collect::<Result<Vec<_>>>()?;
let mut client = connect_client().await?;
let response = client.request(&Request::SendFiles { peer, paths }).await?;
let (offer_id, skipped_symlinks) = match response {
Response::FilesOffered {
offer_id,
skipped_symlinks,
} => (offer_id, skipped_symlinks),
Response::Error { message, .. } => bail!("{message}"),
_ => bail!("unexpected response from daemon"),
};
let mut output = io::stdout().lock();
writeln!(output, "offered files (offer {offer_id})")?;
if skipped_symlinks > 0 {
writeln!(output, "skipped {skipped_symlinks} symlinks")?;
}
if !wait {
return Ok(());
}
writeln!(output, "waiting for the peer to accept")?;
follow_send(&mut client, &mut output, offer_id).await
}
fn absolute_path(path: &Path) -> Result<PathBuf> {
std::fs::canonicalize(path).with_context(|| format!("cannot read {}", path.display()))
}
async fn follow_send(client: &mut IpcClient, output: &mut impl Write, offer_id: u64) -> Result<()> {
let mut idle_since = Instant::now();
let mut last_shown = None;
loop {
tokio::time::sleep(POLL_INTERVAL).await;
let Response::Status(status) = client.request(&Request::Status).await? else {
bail!("expected status from daemon");
};
match poll_send(&status, offer_id) {
Poll::Progress(transfer) => {
idle_since = Instant::now();
if last_shown.as_ref() != Some(transfer) {
write_progress(output, transfer)?;
last_shown = Some(transfer.clone());
}
}
Poll::Finished(SendOutcome::Completed) => {
writeln!(output, "transfer completed")?;
return Ok(());
}
Poll::Finished(SendOutcome::Failed) => bail!("transfer failed"),
Poll::Finished(SendOutcome::Cancelled) => bail!("transfer cancelled"),
Poll::Waiting if idle_since.elapsed() > OFFER_WAIT => {
bail!("the peer did not start the transfer (offer declined or expired)")
}
Poll::Waiting => {}
}
}
}