use super::error::{Fault, ReadFailureKind, ReadStage};
use super::wire::ReadOnlySftp;
use std::cell::Cell;
use std::future::{poll_fn, Future};
use std::path::Path;
use std::pin::pin;
use std::task::Poll;
use std::time::Duration;
use strop_core::worker::CancelToken;
use tokio::io::{AsyncRead, AsyncWrite};
pub(super) const DEADLINE: Duration = Duration::from_secs(60);
const CANCEL_POLL: Duration = Duration::from_millis(50);
pub(super) async fn run(
stdin: impl AsyncWrite + Unpin,
stdout: impl AsyncRead + Unpin,
path: &Path,
token: &CancelToken,
) -> Result<String, Fault> {
let stage = Cell::new(ReadStage::Connect);
let mut work = pin!(transfer(stdin, stdout, path, token, &stage));
let mut cancel = pin!(cancellation(token));
let raced = poll_fn(|cx| {
if let Poll::Ready(result) = work.as_mut().poll(cx) {
return Poll::Ready(result);
}
if cancel.as_mut().poll(cx).is_ready() {
return Poll::Ready(Err(Fault::cancelled(stage.get())));
}
Poll::Pending
});
match tokio::time::timeout(DEADLINE, raced).await {
Ok(result) => result,
Err(_elapsed) => Err(Fault::deadline(stage.get())),
}
}
async fn cancellation(token: &CancelToken) {
loop {
tokio::time::sleep(CANCEL_POLL).await;
if token.is_cancelled() {
return;
}
}
}
async fn transfer(
stdin: impl AsyncWrite + Unpin,
stdout: impl AsyncRead + Unpin,
path: &Path,
token: &CancelToken,
stage: &Cell<ReadStage>,
) -> Result<String, Fault> {
stage.set(ReadStage::Connect);
let mut client = ReadOnlySftp::connect(stdin, stdout).await?;
stage.set(ReadStage::Open);
let handle = client.open(path).await?;
stage.set(ReadStage::Inspect);
let length = client.inspect(&handle).await?;
stage.set(ReadStage::Transfer);
let snapshot = client.read(&handle, length).await?;
stage.set(ReadStage::Validate);
let text = match String::from_utf8(snapshot) {
Ok(text) => text,
Err(error) => {
let at = error.utf8_error().valid_up_to();
return Err(Fault::new(
ReadStage::Validate,
ReadFailureKind::InvalidUtf8,
format!("invalid UTF-8 at byte {at}"),
));
}
};
stage.set(ReadStage::Teardown);
client.close(handle).await?;
drop(client); if token.is_cancelled() {
return Err(Fault::cancelled(ReadStage::Teardown));
}
Ok(text)
}