use std::io::Read;
use std::time::Duration;
use super::{InstallError, PackageSpec};
pub const DEFAULT_REGISTRY: &str = "https://packages.typst.org/preview";
pub const REGISTRY_URL_ENV: &str = "FERROCV_REGISTRY_URL";
const FETCH_TIMEOUT: Duration = Duration::from_secs(30);
pub const MAX_TARBALL_BYTES: u64 = 16 * 1024 * 1024;
fn registry_root() -> String {
match std::env::var(REGISTRY_URL_ENV) {
Ok(v) if !v.is_empty() => v,
_ => DEFAULT_REGISTRY.to_owned(),
}
}
pub fn tarball_url(spec: &PackageSpec) -> String {
debug_assert_eq!(
spec.namespace, "preview",
"tarball_url currently only supports the @preview namespace; \
widen DEFAULT_REGISTRY before relaxing parse_spec"
);
let root = registry_root();
let root = root.trim_end_matches('/');
format!(
"{root}/{name}-{version}.tar.gz",
name = spec.name,
version = spec.version,
)
}
pub fn fetch_tarball(spec: &PackageSpec) -> Result<Vec<u8>, InstallError> {
fetch_tarball_from(&tarball_url(spec))
}
pub fn fetch_tarball_from(url: &str) -> Result<Vec<u8>, InstallError> {
let agent = ureq::Agent::config_builder()
.timeout_global(Some(FETCH_TIMEOUT))
.http_status_as_error(false)
.build()
.new_agent();
let mut response = agent.get(url).call().map_err(|e| InstallError::Http {
url: url.to_owned(),
reason: e.to_string(),
})?;
let status = response.status().as_u16();
if !(200..300).contains(&status) {
return Err(InstallError::HttpStatus {
url: url.to_owned(),
status,
});
}
let mut reader = response
.body_mut()
.as_reader()
.take(MAX_TARBALL_BYTES.saturating_add(1));
let mut buf = Vec::new();
reader
.read_to_end(&mut buf)
.map_err(|source| InstallError::Io {
context: format!("read tarball body from {url}"),
source,
})?;
if (buf.len() as u64) > MAX_TARBALL_BYTES {
return Err(InstallError::Io {
context: format!("tarball body exceeded {MAX_TARBALL_BYTES} bytes"),
source: std::io::Error::other("tarball body too large"),
});
}
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::install::spec::parse_spec;
#[test]
fn url_matches_registry_convention() {
let prior = std::env::var(REGISTRY_URL_ENV).ok();
unsafe { std::env::remove_var(REGISTRY_URL_ENV) };
let spec = parse_spec("@preview/basic-resume:0.2.8").unwrap();
assert_eq!(
tarball_url(&spec),
"https://packages.typst.org/preview/basic-resume-0.2.8.tar.gz"
);
if let Some(v) = prior {
unsafe { std::env::set_var(REGISTRY_URL_ENV, v) };
}
}
}