use std::{fs, io::Write, os::unix::fs::OpenOptionsExt, path::Path};
use async_trait::async_trait;
use futures_util::StreamExt;
use reqwest::Client;
use sha2::{Digest, Sha256};
use crate::domain::errors::{AgentError, AgentResult, ErrorCode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DownloadReceipt {
pub(crate) bytes: u64,
pub(crate) sha256: String,
}
#[async_trait]
pub(crate) trait Downloader: Send + Sync {
async fn download(
&self,
url: &str,
destination: &Path,
limit: u64,
) -> AgentResult<DownloadReceipt>;
}
#[derive(Clone)]
pub(crate) struct ReqwestDownloader {
client: Client,
}
impl ReqwestDownloader {
pub(crate) fn new() -> Self {
Self {
client: Client::new(),
}
}
}
#[async_trait]
impl Downloader for ReqwestDownloader {
async fn download(
&self,
url: &str,
destination: &Path,
limit: u64,
) -> AgentResult<DownloadReceipt> {
let mut created_destination = false;
let result = async {
let response = self
.client
.get(url)
.send()
.await
.map_err(|_| download_failed())?;
if !response.status().is_success()
|| response
.content_length()
.is_some_and(|length| length > limit)
{
return Err(download_failed());
}
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(destination)
.map_err(|_| download_failed())?;
created_destination = true;
let mut total = 0_u64;
let mut hash = Sha256::new();
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|_| download_failed())?;
total = total
.checked_add(chunk.len() as u64)
.filter(|total| *total <= limit)
.ok_or_else(download_failed)?;
file.write_all(&chunk).map_err(|_| download_failed())?;
hash.update(&chunk);
}
file.sync_all().map_err(|_| download_failed())?;
Ok(DownloadReceipt {
bytes: total,
sha256: hex_digest(&hash.finalize()),
})
}
.await;
if result.is_err() && created_destination {
let _ = fs::remove_file(destination);
}
result
}
}
fn download_failed() -> AgentError {
AgentError::new(ErrorCode::SkillDownloadFailed, "managed download failed")
}
fn hex_digest(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}