tinymist_package/registry/
http.rs1use std::path::Path;
4use std::sync::{Arc, OnceLock};
5
6use parking_lot::Mutex;
7use reqwest::Certificate;
8use reqwest::blocking::Response;
9use tinymist_std::ImmutPath;
10use typst::diag::{EcoString, PackageResult, StrResult, eco_format};
11use typst::syntax::package::{PackageVersion, VersionlessPackageSpec};
12
13use super::{
14 DEFAULT_REGISTRY, DummyNotifier, Notifier, PackageError, PackageRegistry, PackageSpec,
15};
16
17pub struct HttpRegistry {
19 package_path: Option<ImmutPath>,
21 package_cache_path: Option<ImmutPath>,
24 storage: OnceLock<PackageStorage>,
26 cert_path: Option<ImmutPath>,
28 notifier: Arc<Mutex<dyn Notifier + Send>>,
30 }
32
33impl Default for HttpRegistry {
34 fn default() -> Self {
35 Self {
36 notifier: Arc::new(Mutex::<DummyNotifier>::default()),
37 cert_path: None,
38 package_path: None,
39 package_cache_path: None,
40
41 storage: OnceLock::new(),
42 }
44 }
45}
46
47impl std::ops::Deref for HttpRegistry {
48 type Target = PackageStorage;
49
50 fn deref(&self) -> &Self::Target {
51 self.storage()
52 }
53}
54
55impl HttpRegistry {
56 pub fn new(
58 cert_path: Option<ImmutPath>,
59 package_path: Option<ImmutPath>,
60 package_cache_path: Option<ImmutPath>,
61 ) -> Self {
62 Self {
63 cert_path,
64 package_path,
65 package_cache_path,
66 ..Default::default()
67 }
68 }
69
70 pub fn storage(&self) -> &PackageStorage {
72 self.storage.get_or_init(|| {
73 PackageStorage::new(
74 self.package_cache_path
75 .clone()
76 .or_else(|| Some(dirs::cache_dir()?.join(DEFAULT_PACKAGES_SUBDIR).into())),
77 self.package_path
78 .clone()
79 .or_else(|| Some(dirs::data_dir()?.join(DEFAULT_PACKAGES_SUBDIR).into())),
80 self.cert_path.clone(),
81 self.notifier.clone(),
82 )
83 })
84 }
85
86 pub fn local_path(&self) -> Option<ImmutPath> {
88 self.storage().package_path().cloned()
89 }
90
91 pub fn paths(&self) -> Vec<ImmutPath> {
93 let data_dir = self.storage().package_path().cloned();
94 let cache_dir = self.storage().package_cache_path().cloned();
95 data_dir.into_iter().chain(cache_dir).collect::<Vec<_>>()
96 }
97
98 pub fn test_package_list(&self, f: impl FnOnce() -> Vec<(PackageSpec, Option<EcoString>)>) {
100 self.storage().index.get_or_init(f);
101 }
102}
103
104impl PackageRegistry for HttpRegistry {
105 fn resolve(&self, spec: &PackageSpec) -> Result<ImmutPath, PackageError> {
106 self.storage().prepare_package(spec)
107 }
108
109 fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
110 self.storage().download_index()
111 }
112}
113
114pub const DEFAULT_PACKAGES_SUBDIR: &str = "typst/packages";
117
118pub struct PackageStorage {
121 package_cache_path: Option<ImmutPath>,
123 package_path: Option<ImmutPath>,
125 cert_path: Option<ImmutPath>,
127 index: OnceLock<Vec<(PackageSpec, Option<EcoString>)>>,
129 notifier: Arc<Mutex<dyn Notifier + Send>>,
130}
131
132impl PackageStorage {
133 pub fn new(
137 package_cache_path: Option<ImmutPath>,
138 package_path: Option<ImmutPath>,
139 cert_path: Option<ImmutPath>,
140 notifier: Arc<Mutex<dyn Notifier + Send>>,
141 ) -> Self {
142 Self {
143 package_cache_path,
144 package_path,
145 cert_path,
146 notifier,
147 index: OnceLock::new(),
148 }
149 }
150
151 pub fn package_cache_path(&self) -> Option<&ImmutPath> {
154 self.package_cache_path.as_ref()
155 }
156
157 pub fn package_path(&self) -> Option<&ImmutPath> {
159 self.package_path.as_ref()
160 }
161
162 pub fn prepare_package(&self, spec: &PackageSpec) -> PackageResult<ImmutPath> {
164 let subdir = format!("{}/{}/{}", spec.namespace, spec.name, spec.version);
165
166 if let Some(packages_dir) = &self.package_path {
167 let dir = packages_dir.join(&subdir);
168 if dir.exists() {
169 return Ok(dir.into());
170 }
171 }
172
173 if let Some(cache_dir) = &self.package_cache_path {
174 let dir = cache_dir.join(&subdir);
175 if dir.exists() {
176 return Ok(dir.into());
177 }
178
179 if spec.namespace == "preview" {
181 self.download_package(spec, &dir)?;
182 if dir.exists() {
183 return Ok(dir.into());
184 }
185 }
186 }
187
188 Err(PackageError::NotFound(spec.clone()))
189 }
190
191 pub fn determine_latest_version(
193 &self,
194 spec: &VersionlessPackageSpec,
195 ) -> StrResult<PackageVersion> {
196 if spec.namespace == "preview" {
197 self.download_index()
200 .iter()
201 .filter(|(package, _)| package.name == spec.name)
202 .map(|(package, _)| package.version)
203 .max()
204 .ok_or_else(|| eco_format!("failed to find package {spec}"))
205 } else {
206 let subdir = format!("{}/{}", spec.namespace, spec.name);
210 self.package_path
211 .iter()
212 .flat_map(|dir| std::fs::read_dir(dir.join(&subdir)).ok())
213 .flatten()
214 .filter_map(|entry| entry.ok())
215 .map(|entry| entry.path())
216 .filter_map(|path| path.file_name()?.to_string_lossy().parse().ok())
217 .max()
218 .ok_or_else(|| eco_format!("please specify the desired version"))
219 }
220 }
221
222 pub fn cached_index(&self) -> Option<&[(PackageSpec, Option<EcoString>)]> {
224 self.index.get().map(Vec::as_slice)
225 }
226
227 pub fn download_index(&self) -> &[(PackageSpec, Option<EcoString>)] {
229 self.index.get_or_init(|| {
230 let url = format!("{DEFAULT_REGISTRY}/preview/index.json");
231
232 threaded_http(&url, self.cert_path.as_deref(), |resp| {
233 let reader = match resp.and_then(|r| r.error_for_status()) {
234 Ok(response) => response,
235 Err(err) => {
236 log::error!("Failed to fetch package index: {err} from {url}");
238 return vec![];
239 }
240 };
241
242 #[derive(serde::Deserialize)]
243 struct RemotePackageIndex {
244 name: EcoString,
245 version: PackageVersion,
246 description: Option<EcoString>,
247 }
248
249 let indices: Vec<RemotePackageIndex> = match serde_json::from_reader(reader) {
250 Ok(index) => index,
251 Err(err) => {
252 log::error!("Failed to parse package index: {err} from {url}");
253 return vec![];
254 }
255 };
256
257 indices
258 .into_iter()
259 .map(|index| {
260 (
261 PackageSpec {
262 namespace: "preview".into(),
263 name: index.name,
264 version: index.version,
265 },
266 index.description,
267 )
268 })
269 .collect::<Vec<_>>()
270 })
271 .unwrap_or_default()
272 })
273 }
274
275 pub fn download_package(&self, spec: &PackageSpec, package_dir: &Path) -> PackageResult<()> {
280 assert_eq!(spec.namespace, "preview");
281
282 let url = format!(
283 "{DEFAULT_REGISTRY}/preview/{}-{}.tar.gz",
284 spec.name, spec.version
285 );
286
287 self.notifier.lock().downloading(spec);
288 threaded_http(&url, self.cert_path.as_deref(), |resp| {
289 let reader = match resp.and_then(|r| r.error_for_status()) {
290 Ok(response) => response,
291 Err(err) if matches!(err.status().map(|s| s.as_u16()), Some(404)) => {
292 return Err(PackageError::NotFound(spec.clone()));
293 }
294 Err(err) => return Err(PackageError::NetworkFailed(Some(eco_format!("{err}")))),
295 };
296
297 let decompressed = flate2::read::GzDecoder::new(reader);
298 tar::Archive::new(decompressed)
299 .unpack(package_dir)
300 .map_err(|err| {
301 std::fs::remove_dir_all(package_dir).ok();
302 PackageError::MalformedArchive(Some(eco_format!("{err}")))
303 })
304 })
305 .ok_or_else(|| PackageError::Other(Some(eco_format!("cannot spawn http thread"))))?
306 }
307}
308
309pub(crate) fn threaded_http<T: Send + Sync>(
310 url: &str,
311 cert_path: Option<&Path>,
312 f: impl FnOnce(Result<Response, reqwest::Error>) -> T + Send + Sync,
313) -> Option<T> {
314 std::thread::scope(|s| {
315 s.spawn(move || {
316 let client_builder = reqwest::blocking::Client::builder();
317
318 let client = if let Some(cert_path) = cert_path {
319 let cert = std::fs::read(cert_path)
320 .ok()
321 .and_then(|buf| Certificate::from_pem(&buf).ok());
322 if let Some(cert) = cert {
323 client_builder.add_root_certificate(cert).build().unwrap()
324 } else {
325 client_builder.build().unwrap()
326 }
327 } else {
328 client_builder.build().unwrap()
329 };
330
331 f(client.get(url).send())
332 })
333 .join()
334 .ok()
335 })
336}