tinymist_package/registry/
http.rsuse std::path::Path;
use std::sync::{Arc, OnceLock};
use parking_lot::Mutex;
use reqwest::blocking::Response;
use reqwest::Certificate;
use tinymist_std::ImmutPath;
use typst::diag::{eco_format, EcoString, PackageResult, StrResult};
use typst::syntax::package::{PackageVersion, VersionlessPackageSpec};
use super::{
DummyNotifier, Notifier, PackageError, PackageRegistry, PackageSpec, DEFAULT_REGISTRY,
};
pub struct HttpRegistry {
package_path: Option<ImmutPath>,
package_cache_path: Option<ImmutPath>,
storage: OnceLock<PackageStorage>,
cert_path: Option<ImmutPath>,
notifier: Arc<Mutex<dyn Notifier + Send>>,
}
impl Default for HttpRegistry {
fn default() -> Self {
Self {
notifier: Arc::new(Mutex::<DummyNotifier>::default()),
cert_path: None,
package_path: None,
package_cache_path: None,
storage: OnceLock::new(),
}
}
}
impl std::ops::Deref for HttpRegistry {
type Target = PackageStorage;
fn deref(&self) -> &Self::Target {
self.storage()
}
}
impl HttpRegistry {
pub fn new(
cert_path: Option<ImmutPath>,
package_path: Option<ImmutPath>,
package_cache_path: Option<ImmutPath>,
) -> Self {
Self {
cert_path,
package_path,
package_cache_path,
..Default::default()
}
}
pub fn storage(&self) -> &PackageStorage {
self.storage.get_or_init(|| {
PackageStorage::new(
self.package_cache_path
.clone()
.or_else(|| Some(dirs::cache_dir()?.join(DEFAULT_PACKAGES_SUBDIR).into())),
self.package_path
.clone()
.or_else(|| Some(dirs::data_dir()?.join(DEFAULT_PACKAGES_SUBDIR).into())),
self.cert_path.clone(),
self.notifier.clone(),
)
})
}
pub fn local_path(&self) -> Option<ImmutPath> {
self.storage().package_path().cloned()
}
pub fn paths(&self) -> Vec<ImmutPath> {
let data_dir = self.storage().package_path().cloned();
let cache_dir = self.storage().package_cache_path().cloned();
data_dir.into_iter().chain(cache_dir).collect::<Vec<_>>()
}
pub fn test_package_list(&self, f: impl FnOnce() -> Vec<(PackageSpec, Option<EcoString>)>) {
self.storage().index.get_or_init(f);
}
}
impl PackageRegistry for HttpRegistry {
fn resolve(&self, spec: &PackageSpec) -> Result<ImmutPath, PackageError> {
self.storage().prepare_package(spec)
}
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
self.storage().download_index()
}
}
pub const DEFAULT_PACKAGES_SUBDIR: &str = "typst/packages";
pub struct PackageStorage {
package_cache_path: Option<ImmutPath>,
package_path: Option<ImmutPath>,
cert_path: Option<ImmutPath>,
index: OnceLock<Vec<(PackageSpec, Option<EcoString>)>>,
notifier: Arc<Mutex<dyn Notifier + Send>>,
}
impl PackageStorage {
pub fn new(
package_cache_path: Option<ImmutPath>,
package_path: Option<ImmutPath>,
cert_path: Option<ImmutPath>,
notifier: Arc<Mutex<dyn Notifier + Send>>,
) -> Self {
Self {
package_cache_path,
package_path,
cert_path,
notifier,
index: OnceLock::new(),
}
}
pub fn package_cache_path(&self) -> Option<&ImmutPath> {
self.package_cache_path.as_ref()
}
pub fn package_path(&self) -> Option<&ImmutPath> {
self.package_path.as_ref()
}
pub fn prepare_package(&self, spec: &PackageSpec) -> PackageResult<ImmutPath> {
let subdir = format!("{}/{}/{}", spec.namespace, spec.name, spec.version);
if let Some(packages_dir) = &self.package_path {
let dir = packages_dir.join(&subdir);
if dir.exists() {
return Ok(dir.into());
}
}
if let Some(cache_dir) = &self.package_cache_path {
let dir = cache_dir.join(&subdir);
if dir.exists() {
return Ok(dir.into());
}
if spec.namespace == "preview" {
self.download_package(spec, &dir)?;
if dir.exists() {
return Ok(dir.into());
}
}
}
Err(PackageError::NotFound(spec.clone()))
}
pub fn determine_latest_version(
&self,
spec: &VersionlessPackageSpec,
) -> StrResult<PackageVersion> {
if spec.namespace == "preview" {
self.download_index()
.iter()
.filter(|(package, _)| package.name == spec.name)
.map(|(package, _)| package.version)
.max()
.ok_or_else(|| eco_format!("failed to find package {spec}"))
} else {
let subdir = format!("{}/{}", spec.namespace, spec.name);
self.package_path
.iter()
.flat_map(|dir| std::fs::read_dir(dir.join(&subdir)).ok())
.flatten()
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter_map(|path| path.file_name()?.to_string_lossy().parse().ok())
.max()
.ok_or_else(|| eco_format!("please specify the desired version"))
}
}
pub fn cached_index(&self) -> Option<&[(PackageSpec, Option<EcoString>)]> {
self.index.get().map(Vec::as_slice)
}
pub fn download_index(&self) -> &[(PackageSpec, Option<EcoString>)] {
self.index.get_or_init(|| {
let url = format!("{DEFAULT_REGISTRY}/preview/index.json");
threaded_http(&url, self.cert_path.as_deref(), |resp| {
let reader = match resp.and_then(|r| r.error_for_status()) {
Ok(response) => response,
Err(err) => {
log::error!("Failed to fetch package index: {err} from {url}");
return vec![];
}
};
#[derive(serde::Deserialize)]
struct RemotePackageIndex {
name: EcoString,
version: PackageVersion,
description: Option<EcoString>,
}
let indices: Vec<RemotePackageIndex> = match serde_json::from_reader(reader) {
Ok(index) => index,
Err(err) => {
log::error!("Failed to parse package index: {err} from {url}");
return vec![];
}
};
indices
.into_iter()
.map(|index| {
(
PackageSpec {
namespace: "preview".into(),
name: index.name,
version: index.version,
},
index.description,
)
})
.collect::<Vec<_>>()
})
.unwrap_or_default()
})
}
pub fn download_package(&self, spec: &PackageSpec, package_dir: &Path) -> PackageResult<()> {
assert_eq!(spec.namespace, "preview");
let url = format!(
"{DEFAULT_REGISTRY}/preview/{}-{}.tar.gz",
spec.name, spec.version
);
self.notifier.lock().downloading(spec);
threaded_http(&url, self.cert_path.as_deref(), |resp| {
let reader = match resp.and_then(|r| r.error_for_status()) {
Ok(response) => response,
Err(err) if matches!(err.status().map(|s| s.as_u16()), Some(404)) => {
return Err(PackageError::NotFound(spec.clone()))
}
Err(err) => return Err(PackageError::NetworkFailed(Some(eco_format!("{err}")))),
};
let decompressed = flate2::read::GzDecoder::new(reader);
tar::Archive::new(decompressed)
.unpack(package_dir)
.map_err(|err| {
std::fs::remove_dir_all(package_dir).ok();
PackageError::MalformedArchive(Some(eco_format!("{err}")))
})
})
.ok_or_else(|| PackageError::Other(Some(eco_format!("cannot spawn http thread"))))?
}
}
pub(crate) fn threaded_http<T: Send + Sync>(
url: &str,
cert_path: Option<&Path>,
f: impl FnOnce(Result<Response, reqwest::Error>) -> T + Send + Sync,
) -> Option<T> {
std::thread::scope(|s| {
s.spawn(move || {
let client_builder = reqwest::blocking::Client::builder();
let client = if let Some(cert_path) = cert_path {
let cert = std::fs::read(cert_path)
.ok()
.and_then(|buf| Certificate::from_pem(&buf).ok());
if let Some(cert) = cert {
client_builder.add_root_certificate(cert).build().unwrap()
} else {
client_builder.build().unwrap()
}
} else {
client_builder.build().unwrap()
};
f(client.get(url).send())
})
.join()
.ok()
})
}