1use crate::FileStates;
2use flams_backend_types::{
3 archive_json::{ArchiveIndex, Institution},
4 archives::{ArchiveData, ArchiveGroupData, DirectoryData, FileData},
5};
6use ftml_uris::{
7 ArchiveId, IsDomainUri, IsNarrativeUri, Language, ModuleUri, Uri, UriWithArchive, UriWithPath,
8 components::UriComponents,
9};
10use leptos::prelude::*;
11use std::path::Path;
12
13#[server(prefix = "/api/backend", endpoint = "group_entries")]
14pub async fn group_entries(
15 r#in: Option<ArchiveId>,
16) -> Result<(Vec<ArchiveGroupData>, Vec<ArchiveData>), ServerFnError<String>> {
17 server::group_entries(r#in).await
18}
19
20#[server(prefix = "/api/backend", endpoint = "archive_entries")]
21pub async fn archive_entries(
22 archive: ArchiveId,
23 path: Option<String>,
24) -> Result<(Vec<DirectoryData>, Vec<FileData>), ServerFnError<String>> {
25 server::archive_entries(archive, path).await
26}
27
28#[server(prefix = "/api/backend", endpoint = "archive_modules")]
29pub async fn archive_modules(
30 archive: ArchiveId,
31 path: Option<String>,
32) -> Result<(Vec<DirectoryData>, Vec<ModuleUri>), ServerFnError<String>> {
33 server::archive_modules(archive, path).await
34}
35
36#[server(prefix = "/api/backend", endpoint = "archive_dependencies")]
37pub async fn archive_dependencies(
38 archives: Vec<ArchiveId>,
39) -> Result<Vec<ArchiveId>, ServerFnError<String>> {
40 server::archive_dependencies(archives).await
41}
42
43#[server(prefix = "/api/backend", endpoint = "build_status")]
44pub async fn build_status(
45 archive: Option<ArchiveId>,
46 path: Option<String>,
47) -> Result<FileStates, ServerFnError<String>> {
48 server::build_status(archive, path).await
49}
50
51ftml_uris::compfun! {
52 #[server(prefix = "/api/backend", endpoint = "source_file",
53 input=server_fn::codec::GetUrl,
54 output=server_fn::codec::Json)]
55 #[allow(clippy::many_single_char_names)]
56 #[allow(clippy::too_many_arguments)]
57 pub async fn source_file(
58 uri: Uri
59 ) -> Result<String, ServerFnError<String>> {
60 use flams_math_archives::{backend::LocalBackend, LocalArchive,MathArchive};
61 use flams_web_utils::not_found;
62 fn get_root(
63 id: &ArchiveId,
64 and_then: impl FnOnce(&LocalArchive, String) -> Result<String, String>,
65 ) -> Result<String, String> {
66 use flams_git::GitUrlExt;
67 flams_math_archives::backend::GlobalBackend.with_local_archive(id, |a| {
68 let Some(a) = a else {
69 not_found!("Archive {id} not found")
70 };
71 let repo = flams_git::repos::GitRepo::open(a.path())
72 .map_err(|_| format!("No git remote for {id} found"))?;
73 let url = repo
74 .get_origin_url()
75 .map_err(|_| format!("No git remote for {id} found"))?;
76 let https = url.into_https();
77 let mut url = https.to_string();
78 if https.git_suffix {
79 url.pop();
81 url.pop();
82 url.pop();
83 url.pop();
84 }
85 and_then(a, url)
86 })
87 }
88 fn get_source(id: &ArchiveId, path: Option<&str>) -> Result<String, String> {
89 get_root(id, |_, s| Ok(s)).map(|mut s| {
90 s.push_str("/-/tree/main/source/");
91 if let Some(p) = path {
92 s.push_str(p);
93 }
94 s
95 })
96 }
97 fn get_source_of_file<'a>(
98 id: &ArchiveId,
99 path: Option<&str>,
100 last: Option<&'a str>,
101 mut name: &'a str,
102 lang: Option<Language>,
103 ) -> Result<String, String> {
104 fn find(path: &Path, base: &mut String, name: &str, lang: Option<Language>) -> bool {
105 if let Some(lang) = lang {
106 let filename = format!("{name}.{lang}.tex");
108 let p = path.join(&filename);
109 if p.exists() {
110 base.push('/');
111 base.push_str(&filename);
112 return true;
113 }
114 } else {
115 let filename = format!("{name}.en.tex");
117 let p = path.join(&filename);
118 if p.exists() {
119 base.push('/');
120 base.push_str(&filename);
121 return true;
122 }
123 }
124 let filename = format!("{name}.tex");
126 let p = path.join(&filename);
127 p.exists() && {
128 base.push('/');
129 base.push_str(&filename);
130 true
131 }
132 }
133 get_root(id, |a, mut base| {
134 base.push_str("/-/blob/main/source");
135 let mut source_path = a.source_dir();
136 if let Some(path) = path {
137 for s in path.split('/') {
138 source_path = source_path.join(s);
139 base.push('/');
140 base.push_str(s);
141 }
142 }
143 if let Some(last) = last {
144 let np = source_path.join(last);
145 let mut nb = format!("{base}/{last}");
146 if find(&np, &mut nb, name, lang) {
147 return Ok(base);
148 }
149 name = last;
150 }
151 if find(&source_path, &mut base, name, lang) {
152 Ok(base)
153 } else {
154 not_found!("No source file found")
155 }
156 })
157 }
158
159 tokio::task::spawn_blocking(move || {
160 let Result::<UriComponents, _>::Ok(comps) = uri else {
161 return Err("invalid uri components".to_string());
162 };
163 let uri = match comps.parse(flams_router_base::uris::get_uri) {
164 Ok(uri) => uri,
165 Err(e) => return Err(format!("Invalid uri: {e}")),
166 };
167
168 match uri {
169 uri @ Uri::Base(_) => Err(format!("BaseUri can not have a source path: {uri}")),
170 Uri::Archive(a) => get_root(a.archive_id(), |_, s| Ok(s)),
171 Uri::Path(uri) => match uri.path() {
172 None => get_root(uri.archive_id(), |_, s| Ok(s)),
173 Some(p) => get_source(uri.archive_id(), Some(&p.to_string())),
174 },
175 Uri::Document(doc) => {
176 let path_str = doc.path().map(ToString::to_string);
177 get_source_of_file(
178 doc.archive_id(),
179 path_str.as_deref(),
180 None,
181 doc.document_name().as_ref(),
182 Some(doc.language()),
183 )
184 }
185 Uri::DocumentElement(n) => {
186 let doc = n.document_uri();
187 let path_str = doc.path().map(ToString::to_string);
188 get_source_of_file(
189 doc.archive_id(),
190 path_str.as_deref(),
191 None,
192 doc.document_name().as_ref(),
193 Some(doc.language()),
194 )
195 }
196 Uri::Module(module) => {
197 let (path, last) = if let Some(p) = module.path() {
198 let ps = p.to_string();
199 if let Some((p, l)) = ps.rsplit_once('/') {
200 (Some(p.to_string()), Some(l.to_string()))
201 } else {
202 (None, Some(ps))
203 }
204 } else {
205 (None, None)
206 };
207
208 get_source_of_file(
209 module.archive_id(),
210 path.as_deref(),
211 last.as_deref(),
212 module.module_name().first(),
213 None,
214 )
215 }
216 Uri::Symbol(symbol) => {
217 let (path, last) = if let Some(p) = symbol.path() {
218 let ps = p.to_string();
219 if let Some((p, l)) = ps.rsplit_once('/') {
220 (Some(p.to_string()), Some(l.to_string()))
221 } else {
222 (None, Some(ps))
223 }
224 } else {
225 (None, None)
226 };
227
228 get_source_of_file(
229 symbol.archive_id(),
230 path.as_deref(),
231 last.as_deref(),
232 symbol.module_name().first(),
233 None,
234 )
235 }
236 }
237 })
238 .await
239 .map_err(|e| e.to_string())?
240 .map_err(|s: String| s.into())
241 }
242}
243
244#[server(prefix="/api/backend",endpoint="download",
245 input=server_fn::codec::GetUrl,
246 output=server_fn::codec::Streaming
247)]
248pub async fn archive_stream(
249 id: ArchiveId,
250) -> Result<leptos::server_fn::codec::ByteStream<ServerFnError<String>>, ServerFnError<String>> {
251 server::archive_stream(id).await
252}
253
254#[server(
255 prefix="/api",
256 endpoint="index",
257 output=server_fn::codec::Json
258)]
259pub async fn index() -> Result<(Vec<Institution>, Vec<ArchiveIndex>), ServerFnError<String>> {
260 Ok(
261 flams_math_archives::manager::ArchiveManager::index_async::<flams_system::TokioEngine>(
262 || flams_system::settings::Settings::get().external_url(),
263 )
264 .await,
265 )
266}
267
268#[cfg(feature = "ssr")]
269mod server {
270 use flams_backend_types::archives::{ArchiveData, ArchiveGroupData, DirectoryData, FileData};
271 use flams_math_archives::{
272 Archive, BuildableArchive, LocallyBuilt, MathArchive,
273 backend::{GlobalBackend, LocalBackend},
274 manager::ArchiveOrGroup as AoG,
275 source_files::{SourceEntry, SourceEntryRef},
276 sparql,
277 utils::path_ext::RelPath,
278 };
279 use flams_router_base::LoginState;
280 use flams_system::LocalArchiveExt;
281 use flams_utils::vecmap::VecSet;
282 use flams_web_utils::blocking_server_fn;
283 use ftml_uris::{ArchiveId, ArchiveUri, FtmlUri, ModuleUri, UriName, UriPath, UriWithArchive};
284 use leptos::prelude::*;
285
286 use crate::FileStates;
287
288 pub async fn group_entries(
289 id: Option<ArchiveId>,
290 ) -> Result<(Vec<ArchiveGroupData>, Vec<ArchiveData>), ServerFnError<String>> {
291 let login = LoginState::get_server();
292 blocking_server_fn(move || {
293 let allowed = matches!(
294 login,
295 LoginState::Admin
296 | LoginState::NoAccounts
297 | LoginState::User { is_admin: true, .. }
298 );
299 GlobalBackend.with_tree(|tree| {
300 let v = match id {
301 None => &tree.top,
302 Some(id) => match tree.get_group_or_archive(&id) {
303 Some(AoG::Group(g)) => &g.children,
304 _ => return Err(format!("Archive Group {id} not found").into()),
305 },
306 };
307 let mut groups = Vec::new();
308 let mut archives = Vec::new();
309 for a in v {
310 match a {
311 AoG::Archive(id) => {
312 let (summary, git) = if !allowed
313 && flams_system::settings::Settings::get().gitlab_url.is_none()
314 {
315 (None, None)
316 } else {
317 tree.get(id)
318 .map(|a| {
319 if let Archive::Local(a) = a {
320 (
321 if allowed {
322 Some(a.state_summary())
323 } else {
324 None
325 },
326 a.is_managed().map(ToString::to_string),
327 )
328 } else {
329 (None, None)
330 }
331 })
332 .unwrap_or_default()
333 };
334 archives.push(ArchiveData {
335 id: id.clone(),
336 summary,
337 git,
338 });
339 }
340 AoG::Group(g) => {
341 let summary = if allowed {
342 Some(g.state.summarize())
343 } else {
344 None
345 };
346 groups.push(ArchiveGroupData {
347 id: g.id.clone(),
348 summary,
349 });
350 }
351 }
352 }
353 Ok((groups, archives))
354 })
355 })
356 .await
357 }
358
359 pub async fn archive_modules(
360 archive: ArchiveId,
361 path: Option<String>,
362 ) -> Result<(Vec<DirectoryData>, Vec<ModuleUri>), ServerFnError<String>> {
363 let login = LoginState::get_server();
364 blocking_server_fn(move || {
365 let allowed = matches!(
366 login,
367 LoginState::Admin
368 | LoginState::NoAccounts
369 | LoginState::User { is_admin: true, .. }
370 );
371 GlobalBackend.with_local_archive(&archive, |a| {
372 let Some(a) = a else {
373 return Err(format!("Archive {archive} not found"));
374 };
375 a.with_sources(|d| {
376 let d = match &path {
377 None => d,
378 Some(p) => match d.find(RelPath::new(p)) {
379 Some(SourceEntryRef::Dir(d)) => d,
380 _ => {
381 return Err(format!(
382 "Directory {p} not found in archive {archive}"
383 ));
384 }
385 },
386 };
387 let mut ds = Vec::new();
388 for d in &d.children {
389 if let SourceEntry::Dir(d) = d {
390 ds.push(DirectoryData {
391 rel_path: d
392 .relative_path
393 .as_ref()
394 .map(UriPath::to_string)
395 .unwrap_or_default(),
396 summary: if allowed {
397 Some(d.state.summarize())
398 } else {
399 None
400 },
401 });
402 }
403 }
404 let mut mods = Vec::new();
405 let out = path.as_ref().map_or_else(
406 || a.out_dir().join(".modules"),
407 |p| a.out_dir().join(p).join(".modules"),
408 );
409 let uripath = path.as_ref().and_then(|p| p.parse::<UriPath>().ok());
410 let path_uri = a.uri().clone() / uripath;
411 if let Ok(dir) = std::fs::read_dir(out) {
412 for f in dir {
413 if let Ok(f) = f
414 && let Ok(name) = f.file_name().into_string()
415 && let Ok(name) = name.replace("__AST__", "*").parse::<UriName>()
416 {
417 mods.push(path_uri.clone() | name);
418 }
419 }
420 }
421
422 Ok((ds, mods))
423 })
424 })
425 })
426 .await
427 }
428
429 pub async fn archive_entries(
430 archive: ArchiveId,
431 path: Option<String>,
432 ) -> Result<(Vec<DirectoryData>, Vec<FileData>), ServerFnError<String>> {
433 use either::Either;
434 let login = LoginState::get_server();
435
436 blocking_server_fn(move || {
437 let allowed = matches!(
438 login,
439 LoginState::Admin
440 | LoginState::NoAccounts
441 | LoginState::User { is_admin: true, .. }
442 );
443 GlobalBackend.with_local_archive(&archive, |a| {
444 let Some(a) = a else {
445 return Err(format!("Archive {archive} not found").into());
446 };
447 a.with_sources(|d| {
448 let d = match path {
449 None => d,
450 Some(p) => match d.find(RelPath::new(&p)) {
451 Some(SourceEntryRef::Dir(d)) => d,
452 _ => {
453 return Err(format!(
454 "Directory {p} not found in archive {archive}"
455 )
456 .into());
457 }
458 },
459 };
460 let mut ds = Vec::new();
461 let mut fs = Vec::new();
462 for d in &d.children {
463 match d {
464 SourceEntry::Dir(d) => ds.push(DirectoryData {
465 rel_path: d
466 .relative_path
467 .as_ref()
468 .map(UriPath::to_string)
469 .unwrap_or_default(),
470 summary: if allowed {
471 Some(d.state.summarize())
472 } else {
473 None
474 },
475 }),
476 SourceEntry::File(f) => fs.push(FileData {
477 rel_path: f.relative_path.to_string(),
478 format: f.format.to_string(),
479 }),
480 }
481 }
482 Ok((ds, fs))
483 })
484 })
485 })
486 .await
487 }
488
489 pub async fn archive_dependencies(
490 archives: Vec<ArchiveId>,
491 ) -> Result<Vec<ArchiveId>, ServerFnError<String>> {
492 use flams_system::TokioEngine;
493 let mut archives: VecSet<_> = archives.into_iter().collect();
494 blocking_server_fn(move || {
495 let mut ret = VecSet::new();
496 let mut dones = VecSet::new();
497 while let Some(archive) = archives.0.pop() {
498 if dones.0.contains(&archive) {
499 continue;
500 }
501 dones.insert(archive.clone());
502 let Some(iri) = GlobalBackend.with_tree(|tree| {
503 let mut steps = archive.steps();
504 if let Some(mut n) = steps.next() {
505 let mut curr = tree.top.as_slice();
506 while let Some(g) = curr.iter().find_map(|a| match a {
507 AoG::Group(g) if g.id.last() == n => Some(g),
508 _ => None,
509 }) {
510 curr = g.children.as_slice();
511 if let Some(a) = curr.iter().find_map(|a| match a {
512 AoG::Archive(a) if a.is_meta() => Some(a),
513 _ => None,
514 }) {
515 if !ret.0.contains(a) {
516 ret.insert(a.clone());
517 archives.insert(a.clone());
518 }
519 }
520 if let Some(m) = steps.next() {
521 n = m;
522 } else {
523 break;
524 }
525 }
526 }
527 tree.get(&archive).map(|a| a.uri().to_iri())
528 }) else {
529 return Err(format!("Archive {archive} not found"));
530 };
531 let res = GlobalBackend
532 .triple_store()
533 .query_str::<TokioEngine>(format!(
534 "SELECT DISTINCT ?a WHERE {{
535 <{}> ulo:contains ?d.
536 ?d rdf:type ulo:document .
537 ?d ulo:contains* ?x.
538 ?x (dc:requires|ulo:imports|dc:hasPart) ?m.
539 ?e ulo:contains? ?m.
540 ?e rdf:type ulo:document.
541 ?a ulo:contains ?e.
542 }}",
543 iri.as_str()
544 ))
545 .map_err(|e| e.to_string())?;
546 for i in res.into_uris::<ArchiveUri>().collect::<Vec<_>>() {
547 let id = i.archive_id();
548 if !ret.0.contains(id) {
549 archives.insert(id.clone());
550 ret.insert(id.clone());
551 }
552 }
553 }
554 Ok(ret.0)
555 })
556 .await
557 }
558 pub async fn build_status(
559 archive: Option<ArchiveId>,
560 path: Option<String>,
561 ) -> Result<FileStates, ServerFnError<String>> {
562 use either::Either;
563 use flams_math_archives::Archive;
564 use flams_math_archives::backend::LocalBackend;
565 use flams_math_archives::manager::ArchiveOrGroup as AoG;
566 let login = LoginState::get_server();
567
568 blocking_server_fn(move || {
569 let allowed = matches!(
570 login,
571 LoginState::Admin
572 | LoginState::NoAccounts
573 | LoginState::User { is_admin: true, .. }
574 );
575 if !allowed {
576 return Err("Not logged in".to_string().into());
577 }
578 path.map_or_else(
579 || {
580 if let Some(archive) = archive.as_ref() {
581 GlobalBackend.with_tree(|tree| match tree.get_group_or_archive(archive) {
582 None => Err(format!("Archive {archive} not found").into()),
583 Some(AoG::Archive(id)) => {
584 let Some(Archive::Local(archive)) = tree.get(id) else {
585 return Err(format!("Archive {archive} not found").into());
586 };
587 Ok(archive.file_state().into())
588 }
589 Some(AoG::Group(g)) => Ok(g.state.clone().into()),
590 })
591 } else {
592 Ok(GlobalBackend.with_tree(|tree| tree.state()).into())
593 }
594 },
595 |path| {
596 let Some(archive) = archive.as_ref() else {
597 return Err("path without archive".to_string().into());
598 };
599 GlobalBackend.with_local_archive(&archive, |a| {
600 let Some(a) = a else {
601 return Err(format!("Archive {archive} not found").into());
602 };
603 a.with_sources(|d| match d.find(RelPath::new(&path)) {
604 Some(SourceEntryRef::Dir(d)) => Ok(d.state.clone().into()),
605 Some(SourceEntryRef::File(f)) => Ok((&*f.target_state).into()),
606 None => {
607 Err(format!("Directory {path} not found in archive {archive}")
608 .into())
609 }
610 })
611 })
612 },
613 )
614 })
615 .await
616 }
617 pub async fn archive_stream(
618 id: ArchiveId,
619 ) -> Result<leptos::server_fn::codec::ByteStream<ServerFnError<String>>, ServerFnError<String>>
620 {
621 use futures::TryStreamExt;
622 let stream = GlobalBackend
623 .with_local_archive(&id, |a| a.map(flams_system::zip::zip))
624 .ok_or_else(|| format!("No archive with id {id} found!"))?;
625 Ok(leptos::server_fn::codec::ByteStream::new(
626 stream.map_err(|e| e.to_string().into()), ))
628 }
629}