Skip to main content

flams_math_archives/backend/
sandbox.rs

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    /// # Errors
108    /// # Panics
109    #[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                                // SAFETY: sandboxes have only local archives
131                                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                            // SAFETY: we know target is in some mathhub directory,
141                            // so it has a parent
142                            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    /// # Panics
175    #[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    /// # Panics
272    #[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        // TODO this can be massively optimized
280        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    //#[deprecated(note = "needs refactoring: should register with manager, but can't")]
324    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}