use std::path::Path;
use async_compression::tokio::bufread::ZstdDecoder;
use async_http_range_reader::{AsyncHttpRangeReader, CheckSupportMethod};
use async_zip::base::read::seek::ZipFileReader;
use http::HeaderMap;
use rattler_conda_types::package::{CondaArchiveType, PackageFile};
use rattler_redaction::{DEFAULT_REDACTION_STR, redact_known_secrets_from_url};
use reqwest_middleware::ClientWithMiddleware;
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
use tracing::{debug, instrument};
use url::Url;
use crate::ExtractError;
use crate::tokio::async_read::{conda_entry_prefix, get_file_from_tar_archive};
const DEFAULT_TAIL_SIZE: u64 = 64 * 1024;
#[instrument(skip_all, fields(url = %redact_known_secrets_from_url(&url, DEFAULT_REDACTION_STR).as_ref().unwrap_or(&url), path = %target_path.display()))]
pub async fn fetch_file_from_remote_sparse(
client: ClientWithMiddleware,
url: Url,
target_path: &Path,
) -> Result<Option<Vec<u8>>, ExtractError> {
let archive_type = CondaArchiveType::try_from(std::path::Path::new(url.path()))
.ok_or(ExtractError::UnsupportedArchiveType)?;
if archive_type != CondaArchiveType::Conda {
return Err(ExtractError::UnsupportedArchiveType);
}
let (reader, _headers) = AsyncHttpRangeReader::new(
client,
url,
CheckSupportMethod::NegativeRangeRequest(DEFAULT_TAIL_SIZE),
HeaderMap::default(),
)
.await?;
let buf_reader = futures::io::BufReader::new(reader.compat());
let mut zip_reader = ZipFileReader::new(buf_reader).await?;
let prefix = conda_entry_prefix(target_path);
let (index, _) = zip_reader
.file()
.entries()
.iter()
.enumerate()
.find(|(_, e)| {
e.filename()
.as_str()
.is_ok_and(|f| f.starts_with(prefix) && f.ends_with(".tar.zst"))
})
.ok_or(ExtractError::MissingComponent)?;
let entry = &zip_reader.file().entries()[index];
let offset = entry.header_offset();
let size = entry.header_size() + entry.compressed_size();
zip_reader
.inner_mut()
.get_mut()
.get_mut()
.prefetch(offset..offset + size)
.await;
let entry_reader = zip_reader.reader_without_entry(index).await?;
let tokio_reader = entry_reader.compat();
let buf_reader = tokio::io::BufReader::new(tokio_reader);
let zstd_decoder = ZstdDecoder::new(buf_reader);
let mut tar = tokio_tar::Archive::new(zstd_decoder);
let result = get_file_from_tar_archive(&mut tar, target_path).await?;
debug!(
"Requested ranges: {:?}",
zip_reader
.inner_mut()
.get_mut()
.get_mut()
.requested_ranges()
.await
);
Ok(result)
}
pub async fn fetch_package_file_sparse<P: PackageFile>(
client: ClientWithMiddleware,
url: Url,
) -> Result<P, ExtractError> {
let bytes = fetch_file_from_remote_sparse(client, url, P::package_path())
.await?
.ok_or(ExtractError::MissingComponent)?;
P::from_slice(&bytes)
.map_err(|e| ExtractError::ArchiveMemberParseError(P::package_path().to_owned(), e))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reqwest::test_server;
use std::path::PathBuf;
fn test_file() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/clobber/clobber-fd-1-0.1.0-h4616a5c_0.conda")
}
#[tokio::test]
async fn test_fetch_package_file_sparse() {
use rattler_conda_types::package::{AboutJson, IndexJson};
let url = test_server::serve_file(test_file()).await;
let client = reqwest_middleware::ClientWithMiddleware::from(reqwest::Client::new());
let index_json: IndexJson = fetch_package_file_sparse(client.clone(), url.clone())
.await
.unwrap();
insta::assert_yaml_snapshot!(index_json);
let about_json: AboutJson = fetch_package_file_sparse(client, url).await.unwrap();
insta::assert_yaml_snapshot!(about_json);
}
#[tokio::test]
async fn test_fetch_raw_file() {
let url = test_server::serve_file(test_file()).await;
let client = reqwest_middleware::ClientWithMiddleware::from(reqwest::Client::new());
let raw = fetch_file_from_remote_sparse(client, url, Path::new("info/index.json"))
.await
.unwrap()
.expect("file should exist in archive");
assert!(!raw.is_empty());
}
#[tokio::test]
async fn test_fetch_pkg_file_sparse() {
let url = test_server::serve_file(test_file()).await;
let client = reqwest_middleware::ClientWithMiddleware::from(reqwest::Client::new());
let raw = fetch_file_from_remote_sparse(client, url, Path::new("clobber"))
.await
.unwrap()
.expect("file should exist in pkg section");
let content = String::from_utf8(raw).unwrap();
insta::assert_snapshot!(content, @"clobber-fd-1");
}
}