use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Mutex;
use typst::diag::{PackageError, PackageResult};
use typst::syntax::package::PackageSpec;
use typst_kit::packages::FsPackages;
pub struct Packages {
data: Option<FsPackages>,
cache: Option<FsPackages>,
#[cfg(feature = "download")]
universe: Option<typst_kit::packages::UniversePackages>,
roots: Mutex<HashMap<PackageSpec, PackageResult<PathBuf>>>,
}
impl Packages {
pub fn new(data: Option<PathBuf>, cache: Option<PathBuf>) -> Self {
Self {
data: data.map(FsPackages::new).or_else(FsPackages::system_data),
cache: cache.map(FsPackages::new).or_else(FsPackages::system_cache),
#[cfg(feature = "download")]
universe: None,
roots: Mutex::new(HashMap::new()),
}
}
#[cfg(feature = "download")]
pub fn download(mut self, user_agent: impl Into<String>) -> Self {
self.universe = Some(typst_kit::packages::UniversePackages::new(download::Http {
user_agent: user_agent.into(),
}));
self
}
pub fn root(&self, spec: &PackageSpec) -> PackageResult<PathBuf> {
let mut roots = self.roots.lock().expect("package lock poisoned");
roots
.entry(spec.clone())
.or_insert_with(|| self.obtain(spec))
.clone()
}
fn obtain(&self, spec: &PackageSpec) -> PackageResult<PathBuf> {
for packages in [&self.data, &self.cache].into_iter().flatten() {
if let Some(root) = packages.obtain(spec) {
return Ok(root.path().to_path_buf());
}
}
#[cfg(feature = "download")]
if let (Some(universe), Some(cache)) = (&self.universe, &self.cache) {
if spec.namespace == typst_kit::packages::UniversePackages::NAMESPACE {
let mut archive = universe.package(spec)?;
cache.store(spec, |dir| {
archive.unpack(dir).map_err(|error| {
PackageError::MalformedArchive(Some(error.to_string().into()))
})
})?;
if let Some(root) = cache.obtain(spec) {
return Ok(root.path().to_path_buf());
}
}
}
Err(PackageError::NotFound(spec.clone()))
}
}
#[cfg(feature = "download")]
mod download {
use std::any::Any;
use std::io::{self, Read};
pub(super) struct Http {
pub(super) user_agent: String,
}
impl typst_kit::downloader::Downloader for Http {
fn stream(&self, _: &dyn Any, url: &str) -> io::Result<(Option<usize>, Box<dyn Read>)> {
let agent = ureq::AgentBuilder::new()
.user_agent(&self.user_agent)
.try_proxy_from_env(true)
.build();
let response = agent.get(url).call().map_err(|error| match error {
ureq::Error::Status(404, _) => io::Error::new(io::ErrorKind::NotFound, error),
error => io::Error::other(error),
})?;
let length = response
.header("Content-Length")
.and_then(|value| value.parse().ok());
Ok((length, Box::new(response.into_reader())))
}
}
}