use std::path::PathBuf;
use std::sync::{
Arc,
atomic::{AtomicU64, Ordering},
};
use a7n_core::files::ValidManifest;
use a7n_proto::limits;
use quinn::{Connection, RecvStream};
use tokio::sync::{mpsc, oneshot, watch};
use tokio::time::timeout;
use super::{Event, Failure, Progress, Result, staging};
use crate::daemon::transfer::{TransferError, read};
mod verify;
pub(super) use verify::apply;
const FREE_SPACE_MARGIN: u64 = 256 * 1024 * 1024;
pub(super) fn check_space(
directory: &std::path::Path,
total: u64,
resumed: u64,
available_space: impl FnOnce(&std::path::Path) -> std::io::Result<u64>,
) -> Result<()> {
let Ok(available) = available_space(directory) else {
crate::log::event!("kind=file_receive_space_query_failed");
return Ok(());
};
let needed = total.saturating_sub(resumed).checked_add(FREE_SPACE_MARGIN);
if needed.is_none_or(|needed| available < needed) {
return Err(Failure::NoSpace);
}
Ok(())
}
pub(super) struct Receive {
pub id: u64,
pub manifest: ValidManifest,
pub stage: PathBuf,
pub destination: PathBuf,
pub receiver: oneshot::Receiver<(RecvStream, Vec<u8>)>,
pub cancelled: watch::Receiver<Failure>,
pub progress: Arc<Progress>,
pub events: mpsc::Sender<Event>,
pub connection: Connection,
pub reserved: Arc<AtomicU64>,
}
pub(super) struct Reservation<'a> {
reserved: &'a AtomicU64,
bytes: u64,
pub(super) others: u64,
}
impl<'a> Reservation<'a> {
pub(super) fn new(reserved: &'a AtomicU64, bytes: u64) -> Self {
let others = reserved.fetch_add(bytes, Ordering::Relaxed);
Self {
reserved,
bytes,
others,
}
}
}
impl Drop for Reservation<'_> {
fn drop(&mut self) {
self.reserved.fetch_sub(self.bytes, Ordering::Relaxed);
}
}
fn space_left(path: &std::path::Path, reserved: u64) -> std::io::Result<u64> {
a7n_platform::transfer_fs::available_space(path).map(|space| space.saturating_sub(reserved))
}
pub(super) async fn run(mut job: Receive) -> Result<Vec<PathBuf>> {
let mut cancelled = job.cancelled.clone();
let result = tokio::select! {
biased;
_ = cancelled.changed() => Err(*cancelled.borrow()),
result = download(&mut job) => result,
};
if result.is_err() && !matches!(result, Err(Failure::LinkLost | Failure::NoSpace)) {
let _ = std::fs::remove_dir_all(&job.stage);
}
if result == Err(Failure::Invalid) {
job.connection.close(crate::link::CLOSE_NORMAL, b"");
}
result
}
async fn download(job: &mut Receive) -> Result<Vec<PathBuf>> {
let Receive {
id,
manifest,
stage,
destination: receive_dir,
receiver,
progress,
events,
reserved,
..
} = job;
let id = *id;
if !stage.try_exists().map_err(|_| Failure::Io)? {
check_space(receive_dir, progress.bytes_total, 0, |path| {
space_left(path, reserved.load(Ordering::Relaxed))
})?;
}
let mut partial = staging::prepare(stage, manifest)?;
let resumed = partial
.files
.iter()
.flatten()
.map(|file| file.written)
.sum();
let reservation = Reservation::new(reserved, progress.bytes_total.saturating_sub(resumed));
check_space(receive_dir, progress.bytes_total, resumed, |path| {
space_left(path, reservation.others)
})?;
progress.bytes.store(resumed, Ordering::Relaxed);
events
.send(Event::FilesReady {
id,
resume: partial.resume.clone(),
})
.await
.map_err(|_| Failure::LinkLost)?;
let (mut stream, mut buf) = timeout(limits::FRAME_READ_TIMEOUT, receiver)
.await
.map_err(|_| Failure::LinkLost)?
.map_err(|_| Failure::LinkLost)?;
loop {
let frame = timeout(limits::FRAME_READ_TIMEOUT, read(&mut stream, &mut buf))
.await
.map_err(|_| Failure::LinkLost)?
.map_err(|reason| {
if reason == TransferError::Frame {
Failure::Invalid
} else {
Failure::LinkLost
}
})?
.ok_or(Failure::LinkLost)?;
if apply(&mut partial, manifest, frame, progress)? {
break;
}
}
if timeout(limits::FRAME_READ_TIMEOUT, read(&mut stream, &mut buf))
.await
.map_err(|_| Failure::LinkLost)?
.map_err(|_| Failure::LinkLost)?
.is_some()
{
return Err(Failure::Invalid);
}
drop(partial);
staging::publish(stage, receive_dir, manifest)
}