1use std::{
2 hint::unreachable_unchecked,
3 path::{Path, PathBuf},
4};
5
6use ftml_ontology::{
7 narrative::{DocumentRange, documents::Document},
8 utils::{Css, RefTree},
9};
10use ftml_uris::{ArchiveId, DocumentUri, UriPath, UriWithArchive};
11
12use crate::{
13 Archive, LocalArchive, MathArchive,
14 backend::{GlobalBackend, LocalBackend},
15 manager::{ArchiveGroup, ArchiveManager, ArchiveOrGroup, ArchiveTree},
16 utils::{
17 AsyncEngine,
18 errors::{ArtifactSaveError, BackendError, FileError},
19 path_ext::{PathExt, RelPath},
20 },
21};
22
23#[derive(Debug, Clone)]
24pub enum SandboxedRepository {
25 Copy(ArchiveId),
26 Git {
27 id: ArchiveId,
28 branch: Box<str>,
29 commit: flams_backend_types::git::Commit,
30 remote: Box<str>,
31 },
32}
33
34impl SandboxedRepository {
35 #[inline]
36 #[must_use]
37 pub const fn id(&self) -> &ArchiveId {
38 match self {
39 Self::Copy(id) | Self::Git { id, .. } => id,
40 }
41 }
42}
43
44#[derive(Debug)]
45pub(super) struct SandboxedBackendI {
46 path: Box<Path>,
47 span: tracing::Span,
48 pub(super) repos: parking_lot::RwLock<Vec<SandboxedRepository>>,
49 manager: ArchiveManager,
50}
51
52#[derive(Debug, Clone)]
53pub struct SandboxedBackend(pub(super) triomphe::Arc<SandboxedBackendI>);
54impl Drop for SandboxedBackendI {
55 fn drop(&mut self) {
56 let _ = std::fs::remove_dir_all(&self.path);
57 }
58}
59
60impl SandboxedBackend {
61 pub fn load_all(&self) {
62 self.0.manager.load(&[&self.0.path]);
63 }
64 #[inline]
65 #[must_use]
66 pub fn root(&self) -> &Path {
67 &self.0.path
68 }
69
70 #[inline]
71 #[must_use]
72 pub fn get_repos(&self) -> Vec<SandboxedRepository> {
73 self.0.repos.read().clone()
74 }
75
76 #[inline]
77 pub fn with_repos<R>(&self, f: impl FnOnce(&[SandboxedRepository]) -> R) -> R {
78 let inner = self.0.repos.read();
79 f(inner.as_slice())
80 }
81
82 #[inline]
83 #[must_use]
84 pub fn path_for(&self, id: &ArchiveId) -> PathBuf {
85 self.0.path.join(id.as_ref())
86 }
87
88 pub fn new(name: &str, temp_dir: &Path) -> Self {
89 let p = temp_dir.join(name);
90 let i = SandboxedBackendI {
91 span: tracing::info_span!(target:"sandbox","sandbox",path=%p.display()),
92 path: p.into(),
93 repos: parking_lot::RwLock::new(Vec::new()),
94 #[cfg(not(feature = "rocksdb"))]
95 manager: ArchiveManager::default(),
96 #[cfg(feature = "rocksdb")]
97 manager: ArchiveManager::new(&temp_dir.join(".rdf")),
98 };
99 Self(triomphe::Arc::new(i))
100 }
101
102 #[inline]
103 pub fn clear(&self) {
104 self.0.repos.write().clear();
105 }
106
107 #[tracing::instrument(level = "info",
110 parent = &self.0.span,
111 target = "sandbox",
112 name = "migrating",
113 fields(path = %self.0.path.display()),
114 skip_all
115 )]
116 pub fn migrate<A: AsyncEngine>(&self) -> Result<usize, FileError> {
117 let mut count = 0;
118 let cnt = &mut count;
119 self.0.manager.reinit::<Result<(), FileError>>(
120 move |sandbox| {
121 GlobalBackend.reinit::<Result<(), FileError>>(
122 |_| {
123 sandbox.top.clear();
124 let Some(main) = crate::mathhub::mathhubs().first() else {
125 unreachable!()
126 };
127 for a in std::mem::take(&mut sandbox.archives) {
128 *cnt += 1;
129 let Archive::Local(a) = a else {
130 unsafe { unreachable_unchecked() }
132 };
133 let source = a.path();
134 let target = main.join(a.id().as_ref());
135
136 if let Some(p) = target.parent() {
137 std::fs::create_dir_all(p)
138 .map_err(|e| FileError::Creation(p.to_path_buf(), e))?;
139 }
140 let safe_target = unsafe { target.parent().unwrap_unchecked() }.join(
143 format!(".{}.tmp", target.file_name().expect("weird fs").display()),
144 );
145 if safe_target.exists() {
146 std::fs::remove_dir_all(&safe_target)
147 .map_err(|e| FileError::Rename(safe_target.clone(), e))?;
148 }
149 source.rename_safe(&safe_target)?;
150 if target.exists() {
151 std::fs::remove_dir_all(&target)
152 .map_err(|e| FileError::Rename(target.clone(), e))?;
153 }
154 std::fs::rename(&safe_target, target)
155 .map_err(|e| FileError::Rename(safe_target.clone(), e))?;
156 }
157 Ok(())
158 },
159 crate::mathhub::mathhubs(),
160 )
161 },
162 &[&*self.0.path],
163 )?;
164
165 #[cfg(feature = "rdf")]
166 A::background(|| {
167 GlobalBackend
168 .triple_store()
169 .load_archives(&GlobalBackend.all_archives());
170 });
171 Ok(count)
172 }
173
174 #[tracing::instrument(level = "info",
176 parent = &self.0.span,
177 target = "sandbox",
178 name = "adding",
179 fields(repository = ?sb),
180 skip_all
181 )]
182 pub fn add(&self, sb: SandboxedRepository, then: impl FnOnce()) {
183 let mut repos = self.0.repos.write();
184 let id = sb.id();
185 if let Some(i) = repos.iter().position(|r| r.id() == id) {
186 repos.remove(i);
187 }
188 self.require_meta_infs(
189 id,
190 &mut repos,
191 |_, _| {},
192 |_, _, _| {
193 tracing::error!("A group with id {id} already exists!");
194 },
195 || {},
196 );
197 let id = sb.id().clone();
198 repos.push(sb);
199 drop(repos);
200 then();
201 let manifest = LocalArchive::manifest_of(&self.0.path.join(id.as_ref()))
202 .expect("archive does not exist");
203 self.0.manager.load_one(&manifest, RelPath::from_id(&id));
204 }
205
206 fn require_meta_infs(
207 &self,
208 id: &ArchiveId,
209 repos: &mut Vec<SandboxedRepository>,
210 then: impl FnOnce(&LocalArchive, &mut Vec<SandboxedRepository>),
211 group: impl FnOnce(&ArchiveGroup, &ArchiveTree, &mut Vec<SandboxedRepository>),
212 else_: impl FnOnce(),
213 ) {
214 if repos.iter().any(|r| r.id() == id) {
215 return;
216 }
217 GlobalBackend.with_tree(move |t| {
218 let mut steps = id.steps();
219 let Some(mut current) = steps.next() else {
220 tracing::error!("empty archive ID");
221 return;
222 };
223 let mut ls = &t.top;
224 loop {
225 let Some(a) = ls.iter().find(|a| a.id().last() == current) else {
226 else_();
227 return;
228 };
229 match a {
230 ArchiveOrGroup::Archive(_) => {
231 if steps.next().is_some() {
232 else_();
233 return;
234 }
235 let Some(Archive::Local(a)) = t.get(id) else {
236 else_();
237 return;
238 };
239 then(a, repos);
240 return;
241 }
242 ArchiveOrGroup::Group(g) => {
243 let Some(next) = steps.next() else {
244 group(g, t, repos);
245 return;
246 };
247 if let Some(ArchiveOrGroup::Archive(a)) =
248 g.children.iter().find(|a| a.id().is_meta())
249 && !repos.iter().any(|r| r.id() == a)
250 {
251 let Some(Archive::Local(a)) = t.get(a) else {
252 else_();
253 return;
254 };
255 repos.push(SandboxedRepository::Copy(a.id().clone()));
256 if self.copy_archive(a).is_ok()
257 && let Some(manifest) =
258 LocalArchive::manifest_of(&self.0.path.join(a.id().as_ref()))
259 {
260 self.0.manager.load_one(&manifest, RelPath::from_id(a.id()));
261 }
262 }
263 current = next;
264 ls = &g.children;
265 }
266 }
267 }
268 });
269 }
270
271 #[tracing::instrument(level = "info",
273 parent = &self.0.span,
274 target = "sandbox",
275 name = "require",
276 skip(self)
277 )]
278 pub fn require(&self, id: &ArchiveId, load: bool) {
279 let mut repos = self.0.repos.write();
281 self.require_meta_infs(
282 id,
283 &mut repos,
284 |a, repos| {
285 if !repos.iter().any(|r| r.id() == id) {
286 repos.push(SandboxedRepository::Copy(id.clone()));
287 if let Err(e) = self.copy_archive(a) {
288 tracing::error!("Error copying {id}: {e}");
289 }
290 }
291 },
292 |g, t, repos| {
293 for a in g.dfs() {
294 if let ArchiveOrGroup::Archive(id) = a
295 && let Some(Archive::Local(a)) = t.get(id)
296 && !repos.iter().any(|r| r.id() == id)
297 {
298 repos.push(SandboxedRepository::Copy(id.clone()));
299 if let Err(e) = self.copy_archive(a) {
300 tracing::error!("Error copying {id}: {e}");
301 }
302 if let Some(manifest) =
303 LocalArchive::manifest_of(&self.0.path.join(id.as_ref()))
304 {
305 let _ = self.0.manager.load_one(&manifest, RelPath::from_id(id));
306 }
307 }
308 }
309 },
310 || tracing::error!("could not find archive {id}"),
311 );
312 drop(repos);
313
314 if load {
315 let Some(manifest) = LocalArchive::manifest_of(&self.0.path.join(id.as_ref())) else {
316 tracing::error!("Error loading manifest of archive {id}");
317 panic!("archive does not exist")
318 };
319 let _ = self.0.manager.load_one(&manifest, RelPath::from_id(id));
320 }
321 }
322
323 pub fn maybe_copy(&self, archive: &LocalArchive) {
325 if !self.0.repos.read().iter().any(|a| a.id() == archive.id()) {
326 self.0
327 .repos
328 .write()
329 .push(SandboxedRepository::Copy(archive.id().clone()));
330 let _ = self.copy_archive(archive);
331 }
332 }
333
334 fn copy_archive(&self, a: &LocalArchive) -> Result<(), FileError> {
335 let path = a.path();
336 let target = self.0.path.join(a.id().as_ref());
337 if target.exists() {
338 return Err(FileError::AlreadyExists);
339 }
340 tracing::info!("copying archive {} to {}", a.id(), target.display());
341 path.copy_dir_all(&target)
342 }
343}
344
345impl LocalBackend for SandboxedBackend {
346 type ArchiveIter<'a> =
347 std::iter::Chain<std::slice::Iter<'a, Archive>, std::slice::Iter<'a, Archive>>;
348
349 fn save(
350 &self,
351 in_doc: &ftml_uris::DocumentUri,
352 rel_path: Option<&UriPath>,
353 log: crate::artifacts::FileOrString,
354 from: crate::formats::BuildTargetId,
355 result: Option<Box<dyn crate::artifacts::Artifact>>,
356 ) -> std::result::Result<(), crate::utils::errors::ArtifactSaveError> {
357 #[cfg(feature = "cached")]
358 {
359 if let Some(r) = result.as_ref() {
360 if let Some(r) = r.as_any().downcast_ref::<crate::artifacts::ContentUpdate>() {
361 if let Some(d) = &r.document {
362 self.0.manager.documents.remove(&d.uri);
363 }
364 for m in &r.modules {
365 self.0.manager.modules.remove(&m.uri);
366 }
367 } else if let Some(r) = r.as_any().downcast_ref::<crate::artifacts::ContentResult>()
368 {
369 self.0.manager.documents.remove(&r.document.uri);
370 for m in &r.modules {
371 self.0.manager.modules.remove(&m.uri);
372 }
373 }
374 }
375 }
376 self.0
377 .manager
378 .with_buildable_archive(in_doc.archive_id(), |a| {
379 let Some(a) = a else {
380 return Err(ArtifactSaveError::NoArchive);
381 };
382 a.save(
383 in_doc,
384 rel_path,
385 log,
386 from,
387 result,
388 #[cfg(feature = "rdf")]
389 self.0.manager.triple_store(),
390 #[cfg(feature = "rdf")]
391 false,
392 )
393 })
394 }
395
396 fn get_document(&self, uri: &DocumentUri) -> Result<Document, BackendError> {
397 self.0
398 .manager
399 .get_document(uri)
400 .or_else(|_| GlobalBackend.get_document(uri))
401 }
402
403 #[allow(clippy::future_not_send)]
404 fn get_document_async<A: AsyncEngine>(
405 &self,
406 uri: &DocumentUri,
407 ) -> impl Future<Output = Result<Document, BackendError>> + Send + use<A>
408 where
409 Self: Sized,
410 {
411 let mgr = self.0.manager.get_document_async::<A>(uri);
412 let uri = uri.clone();
413 async move {
414 if let Ok(d) = mgr.await {
415 return Ok(d);
416 }
417 GlobalBackend.get_document_async::<A>(&uri).await
418 }
419 }
420
421 fn get_module(
422 &self,
423 uri: &ftml_uris::ModuleUri,
424 ) -> Result<ftml_ontology::domain::modules::ModuleLike, crate::utils::errors::BackendError>
425 {
426 self.0
427 .manager
428 .get_module(uri)
429 .or_else(|_| GlobalBackend.get_module(uri))
430 }
431
432 fn get_module_async<A: AsyncEngine>(
433 &self,
434 uri: &ftml_uris::ModuleUri,
435 ) -> impl Future<Output = Result<ftml_ontology::domain::modules::ModuleLike, BackendError>>
436 + Send
437 + use<A>
438 where
439 Self: Sized,
440 {
441 let uri = uri.clone();
442 let mgr = self.0.manager.get_module_async::<A>(&uri);
443 async move {
444 if let Ok(d) = mgr.await {
445 return Ok(d);
446 }
447 GlobalBackend.get_module_async::<A>(&uri).await
448 }
449 }
450
451 fn with_archive_or_group<R>(
452 &self,
453 id: &ArchiveId,
454 f: impl FnOnce(Option<&ArchiveOrGroup>) -> R,
455 ) -> R
456 where
457 Self: Sized,
458 {
459 match self.0.manager.with_archive_or_group(id, |a| {
460 if a.is_some() {
461 either::Left(f(a))
462 } else {
463 either::Right(f)
464 }
465 }) {
466 either::Left(v) => v,
467 either::Right(f) => GlobalBackend.with_archive_or_group(id, f),
468 }
469 }
470
471 fn with_archive<R>(&self, id: &ArchiveId, f: impl FnOnce(Option<&Archive>) -> R) -> R
472 where
473 Self: Sized,
474 {
475 match self.0.manager.with_archive(id, |a| {
476 if a.is_some() {
477 either::Left(f(a))
478 } else {
479 either::Right(f)
480 }
481 }) {
482 either::Left(v) => v,
483 either::Right(f) => GlobalBackend.with_archive(id, f),
484 }
485 }
486
487 fn with_archives<R>(&self, f: impl FnOnce(Self::ArchiveIter<'_>) -> R) -> R
488 where
489 Self: Sized,
490 {
491 self.0
492 .manager
493 .with_archives(|t1| GlobalBackend.with_archives(|t2| f(t1.iter().chain(t2.iter()))))
494 }
495
496 fn get_html_full(&self, uri: &DocumentUri) -> Result<Box<str>, BackendError> {
497 self.0
498 .manager
499 .get_html_full(uri)
500 .or_else(|_| GlobalBackend.get_html_full(uri))
501 }
502
503 fn get_html_body(&self, uri: &DocumentUri) -> Result<(Box<[Css]>, Box<str>), BackendError> {
504 self.0
505 .manager
506 .get_html_body(uri)
507 .or_else(|_| GlobalBackend.get_html_body(uri))
508 }
509
510 fn get_html_body_async<A: AsyncEngine>(
511 &self,
512 uri: &ftml_uris::DocumentUri,
513 ) -> impl Future<Output = Result<(Box<[ftml_ontology::utils::Css]>, Box<str>), BackendError>>
514 + Send
515 + use<A>
516 where
517 Self: Sized,
518 {
519 let mgr = self.0.manager.get_html_body_async::<A>(uri);
520 let uri = uri.clone();
521 async move {
522 if let Ok(d) = mgr.await {
523 return Ok(d);
524 }
525 GlobalBackend.get_html_body_async::<A>(&uri).await
526 }
527 }
528
529 fn get_html_body_inner(
530 &self,
531 uri: &DocumentUri,
532 ) -> Result<(Box<[Css]>, Box<str>), BackendError> {
533 self.0
534 .manager
535 .get_html_body_inner(uri)
536 .or_else(|_| GlobalBackend.get_html_body_inner(uri))
537 }
538
539 fn get_html_body_inner_async<A: AsyncEngine>(
540 &self,
541 uri: &ftml_uris::DocumentUri,
542 ) -> impl Future<Output = Result<(Box<[ftml_ontology::utils::Css]>, Box<str>), BackendError>>
543 + Send
544 + use<A>
545 where
546 Self: Sized,
547 {
548 let mgr = self.0.manager.get_html_body_inner_async::<A>(uri);
549 let uri = uri.clone();
550 async move {
551 if let Ok(d) = mgr.await {
552 return Ok(d);
553 }
554 GlobalBackend.get_html_body_inner_async::<A>(&uri).await
555 }
556 }
557
558 fn get_html_fragment(
559 &self,
560 uri: &DocumentUri,
561 range: DocumentRange,
562 ) -> Result<(Box<[Css]>, Box<str>), BackendError> {
563 self.0
564 .manager
565 .get_html_fragment(uri, range)
566 .or_else(|_| GlobalBackend.get_html_fragment(uri, range))
567 }
568
569 fn get_html_fragment_async<A: AsyncEngine>(
570 &self,
571 uri: &ftml_uris::DocumentUri,
572 range: ftml_ontology::narrative::DocumentRange,
573 ) -> impl Future<Output = Result<(Box<[ftml_ontology::utils::Css]>, Box<str>), BackendError>>
574 + Send
575 + use<A> {
576 let mgr = self.0.manager.get_html_fragment_async::<A>(uri, range);
577 let uri = uri.clone();
578 async move {
579 if let Ok(d) = mgr.await {
580 return Ok(d);
581 }
582 GlobalBackend
583 .get_html_fragment_async::<A>(&uri, range)
584 .await
585 }
586 }
587
588 fn get_reference<T: bincode::Decode<()>>(
589 &self,
590 rf: &ftml_ontology::narrative::DocDataRef<T>,
591 ) -> Result<T, BackendError>
592 where
593 Self: Sized,
594 {
595 self.0
596 .manager
597 .get_reference(rf)
598 .or_else(|_| GlobalBackend.get_reference(rf))
599 }
600
601 #[cfg(feature = "rdf")]
602 #[inline]
603 fn get_notations<E: AsyncEngine>(
604 &self,
605 uri: &ftml_uris::SymbolUri,
606 ) -> impl Iterator<
607 Item = (
608 ftml_uris::DocumentElementUri,
609 ftml_ontology::narrative::elements::Notation,
610 ),
611 >
612 where
613 Self: Sized,
614 {
615 GlobalBackend.get_notations::<E>(uri)
616 }
617
618 #[cfg(feature = "rdf")]
619 #[inline]
620 fn get_var_notations<E: AsyncEngine>(
621 &self,
622 uri: &ftml_uris::DocumentElementUri,
623 ) -> impl Iterator<
624 Item = (
625 ftml_uris::DocumentElementUri,
626 ftml_ontology::narrative::elements::Notation,
627 ),
628 >
629 where
630 Self: Sized,
631 {
632 GlobalBackend.get_var_notations::<E>(uri)
633 }
634}