1mod ignore_regex;
2mod iter;
3pub mod manager;
4pub mod source_files;
5
6use std::path::{Path, PathBuf};
7
8use either::Either;
9use flams_ontology::{
10 archive_json::{ArchiveIndex, Institution},
11 content::modules::OpenModule,
12 file_states::FileStateSummary,
13 languages::Language,
14 narration::documents::UncheckedDocument,
15 uris::{
16 ArchiveId, ArchiveURI, ArchiveURIRef, ArchiveURITrait, DocumentURI, Name, NameStep,
17 PathURITrait, URIOrRefTrait, URIRefTrait,
18 },
19 DocumentRange, Unchecked,
20};
21use flams_utils::{
22 change_listener::ChangeSender,
23 prelude::{TreeChild, TreeLike},
24 vecmap::{VecMap, VecSet},
25 CSS,
26};
27use ignore_regex::IgnoreSource;
28use iter::ArchiveIterator;
29use manager::MaybeQuads;
30use rayon::iter::{IntoParallelIterator, ParallelIterator};
31use source_files::{FileStates, SourceDir};
32use spliter::ParallelSpliterator;
33use tracing::instrument;
34
35use crate::{
36 building::{BuildArtifact, BuildResultArtifact},
37 formats::{BuildTargetId, OMDocResult, SourceFormatId},
38};
39
40use super::{docfile::PreDocFile, rdf::RDFStore, BackendChange};
41
42#[derive(Debug)]
43pub(super) struct RepositoryData {
44 pub(super) uri: ArchiveURI,
45 pub(super) attributes: VecMap<Box<str>, Box<str>>,
46 pub(super) formats: VecSet<SourceFormatId>,
47 pub(super) dependencies: Box<[ArchiveId]>,
48 pub(super) institutions: Box<[Institution]>,
49 pub(super) index: Box<[ArchiveIndex]>,
50}
51
52#[cfg(feature = "zip")]
70mod zip {
71 use std::path::PathBuf;
72
73 use tokio::io::AsyncWriteExt;
74
75 pub(super) struct ZipStream {
76 handle: tokio::task::JoinHandle<()>,
77 stream: tokio_util::io::ReaderStream<tokio::io::ReadHalf<tokio::io::SimplexStream>>,
78 }
79 impl ZipStream {
80 pub(super) fn new(p: PathBuf) -> Self {
81 let (reader, writer) = tokio::io::simplex(1024);
82 let stream = tokio_util::io::ReaderStream::new(reader);
83 let handle = tokio::task::spawn(Self::zip(p, writer));
84 Self { handle, stream }
85 }
86 async fn zip(p: PathBuf, writer: tokio::io::WriteHalf<tokio::io::SimplexStream>) {
87 let comp = async_compression::tokio::write::GzipEncoder::with_quality(
88 writer,
89 async_compression::Level::Best,
90 );
91 let mut tar = tokio_tar::Builder::new(comp);
92 let _ = tar.append_dir_all(".", &p).await;
93 let mut comp = match tar.into_inner().await {
94 Ok(r) => r,
95 Err(e) => {
96 tracing::error!("Failed to zip: {e}");
97 return;
98 }
99 };
100 let _ = comp.shutdown().await;
102 tracing::info!("Finished zipping {}", p.display());
103 }
104 }
105 impl Drop for ZipStream {
106 fn drop(&mut self) {
107 tracing::info!("Dropping");
108 self.handle.abort();
109 }
110 }
111 impl futures::Stream for ZipStream {
112 type Item = std::io::Result<tokio_util::bytes::Bytes>;
113 #[inline]
114 fn poll_next(
115 self: std::pin::Pin<&mut Self>,
116 cx: &mut std::task::Context<'_>,
117 ) -> std::task::Poll<Option<Self::Item>> {
118 unsafe { self.map_unchecked_mut(|f| &mut f.stream).poll_next(cx) }
119 }
120 #[inline]
121 fn size_hint(&self) -> (usize, Option<usize>) {
122 self.stream.size_hint()
123 }
124 }
125
126 pub(super) trait ZipExt {
127 async fn unpack_with_callback<P: AsRef<std::path::Path>>(
128 &mut self,
129 dst: P,
130 cont: impl FnMut(&std::path::Path),
131 ) -> tokio::io::Result<()>;
132 }
133 impl<R: tokio::io::AsyncRead + Unpin> ZipExt for tokio_tar::Archive<R> {
134 async fn unpack_with_callback<P: AsRef<std::path::Path>>(
135 &mut self,
136 dst: P,
137 mut cont: impl FnMut(&std::path::Path),
138 ) -> tokio::io::Result<()> {
139 use rustc_hash::FxHashSet;
140 use std::pin::Pin;
141 use tokio::fs;
142 use tokio_stream::StreamExt;
143 let mut entries = self.entries()?;
144 let mut pinned = Pin::new(&mut entries);
145 let dst = dst.as_ref();
146
147 if fs::symlink_metadata(dst).await.is_err() {
148 fs::create_dir_all(&dst).await?;
149 }
150
151 let dst = fs::canonicalize(dst).await?;
152
153 let mut targets = FxHashSet::default();
154
155 let mut directories = Vec::new();
156 while let Some(entry) = pinned.next().await {
157 let mut file = entry?;
158 if file.header().entry_type() == tokio_tar::EntryType::Directory {
159 directories.push(file);
160 } else {
161 if let Ok(p) = file.path() {
162 cont(&p)
163 }
164 file.unpack_in_raw(&dst, &mut targets).await?;
165 }
166 }
167
168 directories.sort_by(|a, b| b.path_bytes().cmp(&a.path_bytes()));
169 for mut dir in directories {
170 dir.unpack_in_raw(&dst, &mut targets).await?;
171 }
172
173 Ok(())
174 }
175 }
176}
177
178#[derive(Debug)]
179pub struct LocalArchive {
180 pub(super) data: RepositoryData,
181 pub(super) out_path: std::sync::Arc<Path>,
182 pub(super) ignore: IgnoreSource,
183 pub(super) file_state: parking_lot::RwLock<SourceDir>,
184 #[cfg(feature = "gitlab")]
185 pub(super) is_managed: std::sync::OnceLock<Option<git_url_parse::GitUrl>>,
186 }
189impl LocalArchive {
190 #[inline]
191 #[must_use]
192 pub fn out_dir_of(p: &Path) -> PathBuf {
193 p.join(".flams")
194 }
195
196 #[cfg(feature = "zip")]
197 pub async fn unzip_from_remote(
199 id: ArchiveId,
200 url: &str,
201 cont: impl FnMut(&Path),
202 ) -> Result<(), ()> {
203 use flams_utils::PathExt;
204 use futures::TryStreamExt;
205 use zip::ZipExt;
206 let resp = match reqwest::get(url).await {
207 Ok(r) => r,
208 Err(e) => {
209 tracing::error!("Error contacting remote: {e}");
210 return Err(());
211 }
212 };
213 let status = resp.status().as_u16();
214 if (400..=599).contains(&status) {
215 let text = resp.text().await;
216 tracing::error!("Error response from remote: {text:?}");
217 return Err(());
218 }
219 let stream = resp.bytes_stream().map_err(std::io::Error::other);
220 let stream = tokio_util::io::StreamReader::new(stream);
221 let decomp = async_compression::tokio::bufread::GzipDecoder::new(stream);
222 let dest = crate::settings::Settings::get()
223 .temp_dir()
224 .join(flams_utils::hashstr("download", &id));
225
226 let mut tar = tokio_tar::Archive::new(decomp);
227 if let Err(e) = tar.unpack_with_callback(&dest, cont).await {
228 tracing::error!("Error unpacking stream: {e}");
229 let _ = tokio::fs::remove_dir_all(dest).await;
230 return Err(());
231 };
232 let mh = flams_utils::unwrap!(crate::settings::Settings::get().mathhubs.first());
233 let mhdest = mh.join(id.as_ref());
234 if let Err(e) = tokio::fs::create_dir_all(&mhdest).await {
235 tracing::error!("Error moving to MathHub: {e}");
236 return Err(());
237 }
238 if mhdest.exists() {
239 let _ = tokio::fs::remove_dir_all(&mhdest).await;
240 }
241 match tokio::task::spawn_blocking(move || dest.rename_safe(&mhdest)).await {
242 Ok(Ok(())) => Ok(()),
243 Err(e) => {
244 tracing::error!("Error moving to MathHub: {e}");
245 Err(())
246 }
247 Ok(Err(e)) => {
248 tracing::error!("Error moving to MathHub: {e:#}");
249 Err(())
250 }
251 }
252 }
253
254 #[cfg(feature = "zip")]
255 pub fn zip(&self) -> impl futures::Stream<Item = std::io::Result<tokio_util::bytes::Bytes>> {
256 let dir_path = flams_utils::unwrap!(self.out_path.parent()).to_path_buf();
257 zip::ZipStream::new(dir_path)
258 }
259
260 #[cfg(not(feature = "gitlab"))]
261 #[inline]
262 pub const fn is_managed(&self) -> Option<&str> {
263 None
264 }
265
266 #[cfg(feature = "gitlab")]
267 pub fn is_managed(&self) -> Option<&git_url_parse::GitUrl> {
268 let gl = crate::settings::Settings::get().gitlab_url.as_ref()?;
269 self.is_managed
270 .get_or_init(|| {
271 let Ok(repo) = flams_git::repos::GitRepo::open(self.path()) else {
272 return None;
273 };
274 gl.host_str().and_then(|s| repo.is_managed(s))
275 })
276 .as_ref()
277 }
278
279 #[inline]
280 #[must_use]
281 pub fn source_dir_of(p: &Path) -> PathBuf {
282 p.join("source")
283 }
284
285 #[inline]
286 #[must_use]
287 pub fn path(&self) -> &Path {
288 self.out_path.parent().unwrap_or_else(|| unreachable!())
289 }
290
291 #[inline]
292 pub fn file_state(&self) -> FileStates {
293 self.file_state.read().state().clone()
294 }
295
296 #[inline]
297 pub fn state_summary(&self) -> FileStateSummary {
298 self.file_state.read().state().summarize()
299 }
300
301 #[inline]
302 #[must_use]
303 pub fn out_dir(&self) -> &Path {
304 &self.out_path
305 } #[inline]
308 #[must_use]
309 pub fn source_dir(&self) -> PathBuf {
310 Self::source_dir_of(self.path())
311 }
312
313 #[inline]
314 #[must_use]
315 pub fn is_meta(&self) -> bool {
316 self.data.uri.archive_id().is_meta()
317 }
318
319 #[inline]
320 #[must_use]
321 pub fn uri(&self) -> ArchiveURIRef {
322 self.data.uri.archive_uri()
323 }
324
325 #[inline]
326 #[must_use]
327 pub fn id(&self) -> &ArchiveId {
328 self.data.uri.archive_id()
329 }
330
331 #[inline]
332 #[must_use]
333 pub fn formats(&self) -> &[SourceFormatId] {
334 self.data.formats.0.as_slice()
335 }
336
337 #[inline]
338 #[must_use]
339 pub const fn attributes(&self) -> &VecMap<Box<str>, Box<str>> {
340 &self.data.attributes
341 }
342
343 #[inline]
344 #[must_use]
345 pub const fn dependencies(&self) -> &[ArchiveId] {
346 &self.data.dependencies
347 }
348
349 #[inline]
350 pub fn with_sources<R>(&self, f: impl FnOnce(&SourceDir) -> R) -> R {
351 f(&self.file_state.read())
352 }
353
354 pub(crate) fn update_sources(&self, sender: &ChangeSender<BackendChange>) {
355 let mut state = self.file_state.write();
356 state.update(
357 self.uri(),
358 self.path(),
359 sender,
360 &self.ignore,
361 self.formats(),
362 );
363 }
364
365 fn load_module(&self, path: Option<&Name>, name: &NameStep) -> Option<OpenModule<Unchecked>> {
366 let out = path.map_or_else(
367 || self.out_dir().join(".modules"),
368 |n| {
369 n.steps()
370 .iter()
371 .fold(self.out_dir().to_path_buf(), |p, n| p.join(n.as_ref()))
372 .join(".modules")
373 },
374 );
375
376 let out = Self::escape_module_name(&out, name);
377 macro_rules! err {
379 ($e:expr) => {
380 match $e {
381 Ok(e) => e,
382 Err(e) => {
383 tracing::error!("Error loading {}: {e}", out.display());
384 return None;
385 }
386 }
387 };
388 }
389 if out.exists() {
390 let file = err!(std::fs::File::open(&out));
391 let file = std::io::BufReader::new(file);
392 Some(err!(bincode::serde::decode_from_reader(
393 file,
394 bincode::config::standard()
395 )))
396 } else {
398 None
399 }
400 }
401
402 fn submit_triples(
403 &self,
404 in_doc: &DocumentURI,
405 rel_path: &str,
406 relational: &RDFStore,
407 load: bool,
408 iter: impl Iterator<Item = flams_ontology::rdf::Triple>,
409 ) {
410 let out = rel_path
411 .split('/')
412 .fold(self.out_dir().to_path_buf(), |p, s| p.join(s));
413 let _ = std::fs::create_dir_all(&out);
414 let out = out.join("index.ttl");
415 relational.export(iter, &out, in_doc);
416 if load {
417 relational.load(&out, in_doc.to_iri());
418 }
419 }
420
421 pub(super) fn get_filepath(
422 &self,
423 path: Option<&Name>,
424 name: &NameStep,
425 language: Language,
426 filename: &str,
427 ) -> Option<PathBuf> {
428 let out = path.map_or_else(
429 || self.out_dir().to_path_buf(),
430 |n| {
431 n.steps()
432 .iter()
433 .fold(self.out_dir().to_path_buf(), |p, n| p.join(n.as_ref()))
434 },
435 );
436 let name = name.as_ref();
437
438 for d in std::fs::read_dir(&out).ok()? {
439 let Ok(dir) = d else { continue };
440 let Ok(m) = dir.metadata() else { continue };
441 if !m.is_dir() {
442 continue;
443 }
444 let dname = dir.file_name();
445 let Some(d) = dname.to_str() else { continue };
446 if !d.starts_with(name) {
447 continue;
448 }
449 let rest = &d[name.len()..];
450 if !rest.is_empty() && !rest.starts_with('.') {
451 continue;
452 }
453 let rest = rest.strip_prefix('.').unwrap_or(rest);
454 if rest.contains('.') {
455 let lang: &'static str = language.into();
456 if !rest.starts_with(lang) {
457 continue;
458 }
459 }
460 let p = dir.path().join(filename);
461 if p.exists() {
462 return Some(p);
463 }
464 }
465 None
466 }
467
468 fn load_document(
469 &self,
470 path: Option<&Name>,
471 name: &NameStep,
472 language: Language,
473 ) -> Option<UncheckedDocument> {
474 self.get_filepath(path, name, language, "doc")
475 .and_then(|p| PreDocFile::read_from_file(&p))
476 }
477
478 pub fn load_html_body(
479 &self,
480 path: Option<&Name>,
481 name: &NameStep,
482 language: Language,
483 full: bool,
484 ) -> Option<(Vec<CSS>, String)> {
485 self.get_filepath(path, name, language, "ftml")
486 .and_then(|p| OMDocResult::load_html_body(&p, full))
487 }
488
489 #[cfg(feature = "tokio")]
490 pub fn load_html_body_async<'a>(
491 &self,
492 path: Option<&'a Name>,
493 name: &'a NameStep,
494 language: Language,
495 full: bool,
496 ) -> Option<impl std::future::Future<Output = Option<(Vec<CSS>, String)>> + 'a> {
497 let p = self.get_filepath(path, name, language, "ftml")?;
498 Some(OMDocResult::load_html_body_async(p, full))
499 }
500
501 #[cfg(feature = "tokio")]
502 pub fn load_html_full_async<'a>(
503 &self,
504 path: Option<&'a Name>,
505 name: &'a NameStep,
506 language: Language,
507 ) -> Option<impl std::future::Future<Output = Option<String>> + 'a> {
508 let p = self.get_filepath(path, name, language, "ftml")?;
509 Some(OMDocResult::load_html_full_async(p))
510 }
511
512 pub fn load_html_full(
513 &self,
514 path: Option<&Name>,
515 name: &NameStep,
516 language: Language,
517 ) -> Option<String> {
518 let p = self.get_filepath(path, name, language, "ftml")?;
519 OMDocResult::load_html_full(p)
520 }
521
522 pub fn load_html_fragment(
523 &self,
524 path: Option<&Name>,
525 name: &NameStep,
526 language: Language,
527 range: DocumentRange,
528 ) -> Option<(Vec<CSS>, String)> {
529 self.get_filepath(path, name, language, "ftml")
530 .and_then(|p| OMDocResult::load_html_fragment(&p, range))
531 }
532 pub fn load_reference<T: flams_ontology::Resourcable>(
533 &self,
534 path: Option<&Name>,
535 name: &NameStep,
536 language: Language,
537 range: DocumentRange,
538 ) -> eyre::Result<T> {
539 let Some(p) = self.get_filepath(path, name, language, "ftml") else {
540 return Err(eyre::eyre!("File not found"));
541 };
542 OMDocResult::load_reference(&p, range)
543 }
544
545 #[cfg(feature = "tokio")]
546 pub fn load_html_fragment_async<'a>(
547 &self,
548 path: Option<&'a Name>,
549 name: &'a NameStep,
550 language: Language,
551 range: DocumentRange,
552 ) -> Option<impl std::future::Future<Output = Option<(Vec<CSS>, String)>> + 'a> {
553 let p = self.get_filepath(path, name, language, "ftml")?;
554 Some(OMDocResult::load_html_fragment_async(p, range))
555 }
556
557 pub fn load<D: BuildArtifact>(&self, relative_path: &str) -> Result<D, std::io::Error> {
559 let p = self
560 .out_dir()
561 .join(relative_path)
562 .join(D::get_type_id().name());
563 if p.exists() {
564 D::load(&p)
565 } else {
566 Err(std::io::ErrorKind::NotFound.into())
567 }
568 }
569
570 fn escape_module_name(in_path: &Path, name: &NameStep) -> PathBuf {
571 static REPLACER: flams_utils::escaping::Escaper<u8, 1> =
572 flams_utils::escaping::Escaper([(b'*', "__AST__")]);
573 in_path.join(REPLACER.escape(name).to_string())
574 }
575
576 #[allow(clippy::cast_possible_truncation)]
577 #[allow(clippy::cognitive_complexity)]
578 fn save_omdoc_result(&self, top: &Path, result: &OMDocResult) {
579 macro_rules! err {
580 ($e:expr) => {
581 match $e {
582 Ok(r) => r,
583 Err(e) => {
584 tracing::error!("Failed to save {}: {}", top.display(), e);
585 return;
586 }
587 }
588 };
589 }
590 macro_rules! er {
591 ($e:expr) => {
592 if let Err(e) = $e {
593 tracing::error!("Failed to save {}: {}", top.display(), e);
594 return;
595 }
596 };
597 }
598 let p = top.join("ftml");
599 result.write(&p);
600 let OMDocResult {
601 document,
602 modules,
603 html,
604 } = result;
605 let p = top.join("doc");
606 let file = err!(std::fs::File::create(&p));
607 let mut buf = std::io::BufWriter::new(file);
608
609 er!(bincode::serde::encode_into_std_write(
610 document,
611 &mut buf,
612 bincode::config::standard()
613 ));
614 #[cfg(feature = "tantivy")]
617 {
618 let p = top.join("tantivy");
619 let file = err!(std::fs::File::create(&p));
620 let mut buf = std::io::BufWriter::new(file);
621 let ret = document.all_searches(&html.html);
622 er!(bincode::serde::encode_into_std_write(
623 ret,
624 &mut buf,
625 bincode::config::standard()
626 ));
627 }
628
629 for m in modules {
630 let path = m.uri.path();
631 let name = m.uri.name();
632 let out = path.map_or_else(
634 || self.out_dir().join(".modules"),
635 |n| {
636 n.steps()
637 .iter()
638 .fold(self.out_dir().to_path_buf(), |p, n| p.join(n.as_ref()))
639 .join(".modules")
640 },
641 );
642 err!(std::fs::create_dir_all(&out));
644 let out = Self::escape_module_name(&out, name.first_name());
645 let file = err!(std::fs::File::create(&out));
646 let mut buf = std::io::BufWriter::new(file);
647 er!(bincode::serde::encode_into_std_write(
649 m,
650 &mut buf,
651 bincode::config::standard()
652 ));
653 }
654 }
655
656 pub fn get_log(&self, relative_path: &str, target: BuildTargetId) -> PathBuf {
657 self.out_dir()
658 .join(relative_path)
659 .join(target.name())
660 .with_extension("log")
661 }
662
663 #[allow(clippy::cognitive_complexity)]
664 pub fn save(
665 &self,
666 relative_path: &str,
667 log: Either<String, PathBuf>,
668 from: BuildTargetId,
669 result: Option<BuildResultArtifact>,
670 ) {
671 macro_rules! err {
672 ($e:expr) => {
673 if let Err(e) = $e {
674 tracing::error!("Failed to save [{}]{}: {}", self.id(), relative_path, e);
675 return;
676 }
677 };
678 }
679 let top = self.out_dir().join(relative_path);
680 err!(std::fs::create_dir_all(&top));
681 let logfile = top.join(from.name()).with_extension("log");
682 match log {
683 Either::Left(s) => {
684 err!(std::fs::write(&logfile, s));
685 }
686 Either::Right(f) => {
687 err!(std::fs::rename(&f, &logfile));
688 }
689 }
690 match result {
691 Some(BuildResultArtifact::File(t, f)) => {
692 let p = top.join(t.name());
693 err!(std::fs::rename(&f, &p));
694 }
695 Some(BuildResultArtifact::Data(d)) => {
696 if let Some(e) = d.as_any().downcast_ref::<OMDocResult>() {
697 self.save_omdoc_result(&top, e);
698 return;
699 }
700 let p = top.join(d.get_type().name());
701 err!(d.write(&p));
702 }
703 None | Some(BuildResultArtifact::None) => (),
704 }
705 }
706}
707
708#[non_exhaustive]
709pub enum Archive {
710 Local(LocalArchive),
711}
712impl std::fmt::Debug for Archive {
713 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
714 match self {
715 Self::Local(a) => a.id().fmt(f),
716 }
717 }
718}
719impl Archive {
720 #[inline]
721 pub fn get_log(&self, relative_path: &str, target: BuildTargetId) -> PathBuf {
722 match self {
723 Self::Local(a) => a.get_log(relative_path, target),
724 }
725 }
726
727 #[inline]
728 pub fn with_sources<R>(&self, f: impl FnOnce(&SourceDir) -> R) -> R {
729 match self {
730 Self::Local(a) => a.with_sources(f),
731 }
732 }
733
734 pub fn submit_triples(
735 &self,
736 in_doc: &DocumentURI,
737 rel_path: &str,
738 relational: &RDFStore,
739 load: bool,
740 iter: impl Iterator<Item = flams_ontology::rdf::Triple>,
741 ) {
742 match self {
743 Self::Local(a) => a.submit_triples(in_doc, rel_path, relational, load, iter),
744 }
745 }
746
747 #[inline]
748 #[must_use]
749 const fn data(&self) -> &RepositoryData {
750 match self {
751 Self::Local(a) => &a.data,
752 }
753 }
754
755 #[inline]
756 #[must_use]
757 pub fn uri(&self) -> ArchiveURIRef {
758 self.data().uri.archive_uri()
759 }
760 #[inline]
761 #[must_use]
762 pub fn id(&self) -> &ArchiveId {
763 self.data().uri.archive_id()
764 }
765
766 #[inline]
767 #[must_use]
768 pub fn formats(&self) -> &[SourceFormatId] {
769 self.data().formats.0.as_slice()
770 }
771
772 #[inline]
773 #[must_use]
774 pub const fn attributes(&self) -> &VecMap<Box<str>, Box<str>> {
775 &self.data().attributes
776 }
777
778 #[inline]
779 #[must_use]
780 pub const fn dependencies(&self) -> &[ArchiveId] {
781 &self.data().dependencies
782 }
783
784 pub fn load_html_body(
785 &self,
786 path: Option<&Name>,
787 name: &NameStep,
788 language: Language,
789 full: bool,
790 ) -> Option<(Vec<CSS>, String)> {
791 match self {
792 Self::Local(a) => a.load_html_body(path, name, language, full),
793 }
794 }
795
796 #[cfg(feature = "tokio")]
797 pub fn load_html_body_async<'a>(
798 &self,
799 path: Option<&'a Name>,
800 name: &'a NameStep,
801 language: Language,
802 full: bool,
803 ) -> Option<impl std::future::Future<Output = Option<(Vec<CSS>, String)>> + 'a> {
804 match self {
805 Self::Local(a) => a.load_html_body_async(path, name, language, full),
806 }
807 }
808 #[cfg(feature = "tokio")]
809 pub fn load_html_full_async<'a>(
810 &self,
811 path: Option<&'a Name>,
812 name: &'a NameStep,
813 language: Language,
814 ) -> Option<impl std::future::Future<Output = Option<String>> + 'a> {
815 match self {
816 Self::Local(a) => a.load_html_full_async(path, name, language),
817 }
818 }
819 pub fn load_html_full(
820 &self,
821 path: Option<&Name>,
822 name: &NameStep,
823 language: Language,
824 ) -> Option<String> {
825 match self {
826 Self::Local(a) => a.load_html_full(path, name, language),
827 }
828 }
829
830 pub fn load_html_fragment(
831 &self,
832 path: Option<&Name>,
833 name: &NameStep,
834 language: Language,
835 range: DocumentRange,
836 ) -> Option<(Vec<CSS>, String)> {
837 match self {
838 Self::Local(a) => a.load_html_fragment(path, name, language, range),
839 }
840 }
841
842 pub fn load_reference<T: flams_ontology::Resourcable>(
843 &self,
844 path: Option<&Name>,
845 name: &NameStep,
846 language: Language,
847 range: DocumentRange,
848 ) -> eyre::Result<T> {
849 match self {
850 Self::Local(a) => a.load_reference(path, name, language, range),
851 }
852 }
853
854 #[cfg(feature = "tokio")]
855 pub fn load_html_fragment_async<'a>(
856 &self,
857 path: Option<&'a Name>,
858 name: &'a NameStep,
859 language: Language,
860 range: DocumentRange,
861 ) -> Option<impl std::future::Future<Output = Option<(Vec<CSS>, String)>> + 'a> {
862 match self {
863 Self::Local(a) => a.load_html_fragment_async(path, name, language, range),
864 }
865 }
866
867 fn load_document(
868 &self,
869 path: Option<&Name>,
870 name: &NameStep,
871 language: Language,
872 ) -> Option<UncheckedDocument> {
873 match self {
874 Self::Local(a) => a.load_document(path, name, language),
875 }
876 }
877 fn load_module(&self, path: Option<&Name>, name: &NameStep) -> Option<OpenModule<Unchecked>> {
878 match self {
879 Self::Local(a) => a.load_module(path, name),
880 }
881 }
882
883 #[inline]
885 pub fn load<D: BuildArtifact>(&self, relative_path: &str) -> Result<D, std::io::Error> {
886 match self {
887 Self::Local(a) => a.load(relative_path),
888 }
889 }
890
891 pub fn save(
892 &self,
893 relative_path: &str,
894 log: Either<String, PathBuf>,
895 from: BuildTargetId,
896 result: Option<BuildResultArtifact>,
897 ) {
898 match self {
899 Self::Local(a) => a.save(relative_path, log, from, result),
900 }
901 }
902}
903
904#[derive(Debug, Default)]
905pub struct ArchiveTree {
906 pub archives: Vec<Archive>,
907 pub groups: Vec<ArchiveOrGroup>,
908 pub index: (VecSet<Institution>, VecSet<ArchiveIndex>),
909}
910
911#[derive(Debug)]
912pub enum ArchiveOrGroup {
913 Archive(ArchiveId),
914 Group(ArchiveGroup),
915}
916
917impl ArchiveOrGroup {
918 #[inline]
919 #[must_use]
920 pub const fn id(&self) -> &ArchiveId {
921 match self {
922 Self::Archive(id) => id,
923 Self::Group(g) => &g.id,
924 }
925 }
926}
927
928#[derive(Debug)]
929pub struct ArchiveGroup {
930 pub id: ArchiveId,
931 pub children: Vec<ArchiveOrGroup>,
932 pub state: FileStates,
933}
934
935impl TreeLike for ArchiveTree {
936 type RefIter<'a> = std::slice::Iter<'a, ArchiveOrGroup>;
937 type Child<'a> = &'a ArchiveOrGroup;
938 fn children(&self) -> Option<Self::RefIter<'_>> {
939 Some(self.groups.iter())
940 }
941}
942
943impl TreeLike for &ArchiveGroup {
944 type RefIter<'a>
945 = std::slice::Iter<'a, ArchiveOrGroup>
946 where
947 Self: 'a;
948 type Child<'a>
949 = &'a ArchiveOrGroup
950 where
951 Self: 'a;
952 fn children(&self) -> Option<Self::RefIter<'_>> {
953 Some(self.children.iter())
954 }
955}
956
957impl TreeChild<ArchiveTree> for &ArchiveOrGroup {
958 fn children<'a>(&self) -> Option<<ArchiveTree as TreeLike>::RefIter<'a>>
959 where
960 Self: 'a,
961 {
962 if let ArchiveOrGroup::Group(a) = self {
963 Some(a.children.iter())
964 } else {
965 None
966 }
967 }
968}
969
970impl TreeChild<&ArchiveGroup> for &ArchiveOrGroup {
971 fn children<'a>(&self) -> Option<std::slice::Iter<'a, ArchiveOrGroup>>
972 where
973 Self: 'a,
974 {
975 if let ArchiveOrGroup::Group(a) = self {
976 Some(a.children.iter())
977 } else {
978 None
979 }
980 }
981}
982
983impl ArchiveTree {
984 #[must_use]
985 pub fn find(&self, id: &ArchiveId) -> Option<&ArchiveOrGroup> {
986 let mut steps = id.steps().peekable();
987 let mut curr = &self.groups;
988 while let Some(step) = steps.next() {
989 let e = curr.iter().find(|e| e.id().last_name() == step)?;
990 if steps.peek().is_none() {
994 return Some(e);
995 } if let ArchiveOrGroup::Group(g) = e {
997 curr = &g.children;
999 } else {
1000 return None;
1001 }
1002 }
1003 None
1004 }
1005
1006 #[must_use]
1007 pub fn get(&self, id: &ArchiveId) -> Option<&Archive> {
1008 self.archives.iter().find(|a| a.uri().archive_id() == id)
1009 }
1012
1013 pub fn state(&self) -> FileStates {
1014 let mut r = FileStates::default();
1015 for aog in &self.groups {
1016 match aog {
1017 ArchiveOrGroup::Archive(a) => {
1018 if let Some(Archive::Local(a)) = self.get(a) {
1019 r.merge_all(&a.file_state.read().state);
1020 }
1021 }
1022 ArchiveOrGroup::Group(g) => r.merge_all(&g.state),
1023 }
1024 }
1025 r
1026 }
1027
1028 #[instrument(level = "info",
1029 target = "archives",
1030 name = "Loading archives",
1031 fields(path = %path.display()),
1032 skip_all
1033 )]
1034 pub(crate) fn load(
1035 &mut self,
1036 path: &Path,
1037 sender: &ChangeSender<BackendChange>,
1038 f: impl MaybeQuads,
1039 ) {
1040 tracing::info!(target:"archives","Searching for archives");
1041 let old = std::mem::take(self);
1042 let old_new_f = parking_lot::Mutex::new((old, Self::default(), f));
1043
1044 ArchiveIterator::new(path)
1045 .par_split()
1046 .into_par_iter()
1047 .for_each(|a| {
1048 a.update_sources(sender);
1049 let mut lock = old_new_f.lock();
1050 let (old, new, f) = &mut *lock;
1051 if old.remove_from_list(a.id()).is_none() {
1052 sender.lazy_send(|| BackendChange::NewArchive(URIRefTrait::owned(a.uri())));
1053 }
1054 new.insert(Archive::Local(a), f);
1055 drop(lock);
1056 });
1058 let (_old, new, _) = old_new_f.into_inner();
1059 *self = new;
1061 for a in &self.archives {
1062 for i in &a.data().institutions {
1063 self.index.0.insert_clone(i);
1064 }
1065 for doc in &a.data().index {
1066 self.index.1.insert_clone(doc);
1067 }
1068 }
1069 }
1071
1072 #[inline]
1073 fn remove_from_list(&mut self, id: &ArchiveId) -> Option<Archive> {
1074 if let Ok(i) = self
1075 .archives
1076 .binary_search_by_key(&id, |a: &Archive| a.id())
1077 {
1078 Some(self.archives.remove(i))
1079 } else {
1080 None
1081 }
1082 }
1083
1084 fn _remove(&mut self, id: &ArchiveId) -> Option<Archive> {
1085 let mut curr = &mut self.groups;
1086 let mut steps = id.steps();
1087 while let Some(step) = steps.next() {
1088 let Ok(i) = curr.binary_search_by_key(&step, |v| v.id().last_name()) else {
1089 return None;
1090 };
1091 if matches!(curr[i], ArchiveOrGroup::Group(_)) {
1092 let ArchiveOrGroup::Group(g) = &mut curr[i] else {
1093 unreachable!()
1094 };
1095 curr = &mut g.children;
1096 continue;
1097 }
1098 if steps.next().is_some() {
1099 return None;
1100 }
1101 let ArchiveOrGroup::Archive(a) = curr.remove(i) else {
1102 unreachable!()
1103 };
1104 let Ok(i) = self
1105 .archives
1106 .binary_search_by_key(&&a, |a: &Archive| a.id())
1107 else {
1108 unreachable!()
1109 };
1110 return Some(self.archives.remove(i));
1111 }
1112 None
1113 }
1114
1115 #[allow(clippy::needless_pass_by_ref_mut)]
1116 #[allow(irrefutable_let_patterns)]
1117 fn insert(&mut self, archive: Archive, _f: &mut impl MaybeQuads) {
1118 let id = archive.id().clone();
1119 let steps = if let Some((group, _)) = id.as_ref().rsplit_once('/') {
1120 group.split('/')
1121 } else {
1122 match self
1123 .archives
1124 .binary_search_by_key(&&id, |a: &Archive| a.id())
1125 {
1126 Ok(i) => self.archives[i] = archive,
1127 Err(i) => self.archives.insert(i, archive),
1128 };
1129 match self
1130 .groups
1131 .binary_search_by_key(&id.as_ref(), |v| v.id().last_name())
1132 {
1133 Ok(i) => self.groups[i] = ArchiveOrGroup::Archive(id),
1134 Err(i) => self.groups.insert(i, ArchiveOrGroup::Archive(id)),
1135 }
1136 return;
1137 };
1138 let mut curr = &mut self.groups;
1139 let mut curr_name = String::new();
1140 for step in steps {
1141 if curr_name.is_empty() {
1142 curr_name = step.to_string();
1143 } else {
1144 curr_name = format!("{curr_name}/{step}");
1145 }
1146 match curr.binary_search_by_key(&step, |v| v.id().last_name()) {
1147 Ok(i) => {
1148 let ArchiveOrGroup::Group(g) = &mut curr[i]
1149 else {
1151 unreachable!()
1152 };
1153 if let Archive::Local(a) = &archive {
1154 g.state.merge_all(a.file_state.read().state());
1155 }
1156 curr = &mut g.children;
1157 }
1158 Err(i) => {
1159 let mut state = FileStates::default();
1160 if let Archive::Local(a) = &archive {
1161 state.merge_all(a.file_state.read().state());
1162 }
1163 let g = ArchiveGroup {
1164 id: ArchiveId::new(&curr_name),
1165 children: Vec::new(),
1166 state,
1167 };
1168 curr.insert(i, ArchiveOrGroup::Group(g));
1169 let ArchiveOrGroup::Group(g) = &mut curr[i] else {
1170 unreachable!()
1171 };
1172 curr = &mut g.children;
1173 }
1174 }
1175 }
1176
1177 match self
1178 .archives
1179 .binary_search_by_key(&&id, |a: &Archive| a.id())
1180 {
1181 Ok(i) => self.archives[i] = archive,
1182 Err(i) => self.archives.insert(i, archive),
1183 };
1184 match curr.binary_search_by_key(&id.last_name(), |v| v.id().last_name()) {
1185 Ok(i) => curr[i] = ArchiveOrGroup::Archive(id),
1186 Err(i) => curr.insert(i, ArchiveOrGroup::Archive(id)),
1187 }
1188 }
1189}