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) => uri.path().map_or_else(
172 || get_root(uri.archive_id(), |_, s| Ok(s)),
173 |p| get_source(uri.archive_id(), Some(p.as_ref()))
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) = module.path().map_or((None,None),|p| {
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 });
205
206 get_source_of_file(
207 module.archive_id(),
208 path.as_deref(),
209 last.as_deref(),
210 module.module_name().first(),
211 None,
212 )
213 }
214 Uri::Symbol(symbol) => {
215 let (path, last) = symbol.path().map_or((None,None),|p| {
216 let ps = p.to_string();
217 if let Some((p, l)) = ps.rsplit_once('/') {
218 (Some(p.to_string()), Some(l.to_string()))
219 } else {
220 (None, Some(ps))
221 }
222 });
223
224 get_source_of_file(
225 symbol.archive_id(),
226 path.as_deref(),
227 last.as_deref(),
228 symbol.module_name().first(),
229 None,
230 )
231 }
232 }
233 })
234 .await
235 .map_err(|e| e.to_string())?
236 .map_err(|s: String| s.into())
237 }
238}
239
240#[server(prefix="/api/backend",endpoint="download",
241 input=server_fn::codec::GetUrl,
242 output=server_fn::codec::Streaming
243)]
244pub async fn archive_stream(
245 id: ArchiveId,
246) -> Result<leptos::server_fn::codec::ByteStream<ServerFnError<String>>, ServerFnError<String>> {
247 server::archive_stream(&id)
248}
249
250#[server(
251 prefix="/api",
252 endpoint="index",
253 output=server_fn::codec::Json
254)]
255pub async fn index() -> Result<(Vec<Institution>, Vec<ArchiveIndex>), ServerFnError<String>> {
256 Ok(
257 flams_math_archives::manager::ArchiveManager::index_async::<flams_system::TokioEngine>(
258 || flams_system::settings::Settings::get().external_url(),
259 )
260 .await,
261 )
262}
263
264#[cfg(feature = "ssr")]
265mod server {
266 use flams_backend_types::archives::{ArchiveData, ArchiveGroupData, DirectoryData, FileData};
267 use flams_math_archives::{
268 Archive, BuildableArchive, LocallyBuilt, MathArchive,
269 backend::{GlobalBackend, LocalBackend},
270 manager::ArchiveOrGroup as AoG,
271 source_files::{SourceEntry, SourceEntryRef},
272 utils::path_ext::RelPath,
273 };
274 use flams_router_base::LoginState;
275 use flams_system::LocalArchiveExt;
276 use flams_utils::vecmap::VecSet;
277 use flams_web_utils::blocking_server_fn;
278 use ftml_uris::{ArchiveId, ArchiveUri, FtmlUri, ModuleUri, UriName, UriPath, UriWithArchive};
279 use leptos::prelude::*;
280
281 use crate::FileStates;
282
283 pub async fn group_entries(
284 id: Option<ArchiveId>,
285 ) -> Result<(Vec<ArchiveGroupData>, Vec<ArchiveData>), ServerFnError<String>> {
286 let login = LoginState::get_server();
287 blocking_server_fn(move || {
288 let allowed = matches!(
289 login,
290 LoginState::Admin
291 | LoginState::NoAccounts
292 | LoginState::User { is_admin: true, .. }
293 );
294 GlobalBackend.with_tree(|tree| {
295 let v = match id {
296 None => &tree.top,
297 Some(id) => match tree.get_group_or_archive(&id) {
298 Some(AoG::Group(g)) => &g.children,
299 _ => return Err(format!("Archive Group {id} not found")),
300 },
301 };
302 let mut groups = Vec::new();
303 let mut archives = Vec::new();
304 for a in v {
305 match a {
306 AoG::Archive(id) => {
307 let (summary, git) = if !allowed
308 && flams_system::settings::Settings::get().gitlab_url.is_none()
309 {
310 (None, None)
311 } else {
312 tree.get(id)
313 .map(|a| {
314 if let Archive::Local(a) = a {
315 (
316 if allowed {
317 Some(a.state_summary())
318 } else {
319 None
320 },
321 a.is_managed().map(ToString::to_string),
322 )
323 } else {
324 (None, None)
325 }
326 })
327 .unwrap_or_default()
328 };
329 archives.push(ArchiveData {
330 id: id.clone(),
331 summary,
332 git,
333 });
334 }
335 AoG::Group(g) => {
336 let summary = if allowed {
337 Some(g.state.summarize())
338 } else {
339 None
340 };
341 groups.push(ArchiveGroupData {
342 id: g.id.clone(),
343 summary,
344 });
345 }
346 }
347 }
348 Ok((groups, archives))
349 })
350 })
351 .await
352 }
353
354 pub async fn archive_modules(
355 archive: ArchiveId,
356 path: Option<String>,
357 ) -> Result<(Vec<DirectoryData>, Vec<ModuleUri>), ServerFnError<String>> {
358 let login = LoginState::get_server();
359 blocking_server_fn(move || {
360 let allowed = matches!(
361 login,
362 LoginState::Admin
363 | LoginState::NoAccounts
364 | LoginState::User { is_admin: true, .. }
365 );
366 GlobalBackend.with_local_archive(&archive, |a| {
367 let Some(a) = a else {
368 return Err(format!("Archive {archive} not found"));
369 };
370 a.with_sources(|d| {
371 let d = match &path {
372 None => d,
373 Some(p) => match d.find(RelPath::new(p)) {
374 Some(SourceEntryRef::Dir(d)) => d,
375 _ => {
376 return Err(format!(
377 "Directory {p} not found in archive {archive}"
378 ));
379 }
380 },
381 };
382 let mut ds = Vec::new();
383 for d in &d.children {
384 if let SourceEntry::Dir(d) = d {
385 ds.push(DirectoryData {
386 rel_path: d
387 .relative_path
388 .as_ref()
389 .map(UriPath::to_string)
390 .unwrap_or_default(),
391 summary: if allowed {
392 Some(d.state.summarize())
393 } else {
394 None
395 },
396 });
397 }
398 }
399 let mut mods = Vec::new();
400 let out = path.as_ref().map_or_else(
401 || a.out_dir().join(".modules"),
402 |p| a.out_dir().join(p).join(".modules"),
403 );
404 let uripath = path.as_ref().and_then(|p| p.parse::<UriPath>().ok());
405 let path_uri = a.uri().clone() / uripath;
406 if let Ok(dir) = std::fs::read_dir(out) {
407 for f in dir {
408 if let Ok(f) = f
409 && let Ok(name) = f.file_name().into_string()
410 && let Ok(name) = name.replace("__AST__", "*").parse::<UriName>()
411 {
412 mods.push(path_uri.clone() | name);
413 }
414 }
415 }
416
417 Ok((ds, mods))
418 })
419 })
420 })
421 .await
422 }
423
424 pub async fn archive_entries(
425 archive: ArchiveId,
426 path: Option<String>,
427 ) -> Result<(Vec<DirectoryData>, Vec<FileData>), ServerFnError<String>> {
428 let login = LoginState::get_server();
429
430 blocking_server_fn(move || {
431 let allowed = matches!(
432 login,
433 LoginState::Admin
434 | LoginState::NoAccounts
435 | LoginState::User { is_admin: true, .. }
436 );
437 GlobalBackend.with_local_archive(&archive, |a| {
438 let Some(a) = a else {
439 return Err(format!("Archive {archive} not found"));
440 };
441 a.with_sources(|d| {
442 let d = match path {
443 None => d,
444 Some(p) => match d.find(RelPath::new(&p)) {
445 Some(SourceEntryRef::Dir(d)) => d,
446 _ => {
447 return Err(format!(
448 "Directory {p} not found in archive {archive}"
449 ));
450 }
451 },
452 };
453 let mut ds = Vec::new();
454 let mut fs = Vec::new();
455 for d in &d.children {
456 match d {
457 SourceEntry::Dir(d) => ds.push(DirectoryData {
458 rel_path: d
459 .relative_path
460 .as_ref()
461 .map(UriPath::to_string)
462 .unwrap_or_default(),
463 summary: if allowed {
464 Some(d.state.summarize())
465 } else {
466 None
467 },
468 }),
469 SourceEntry::File(f) => fs.push(FileData {
470 rel_path: f.relative_path.to_string(),
471 format: f.format.to_string(),
472 }),
473 }
474 }
475 Ok((ds, fs))
476 })
477 })
478 })
479 .await
480 }
481
482 pub async fn archive_dependencies(
483 archives: Vec<ArchiveId>,
484 ) -> Result<Vec<ArchiveId>, ServerFnError<String>> {
485 use flams_system::TokioEngine;
486 let mut archives: VecSet<_> = archives.into_iter().collect();
487 blocking_server_fn(move || {
488 let mut ret = VecSet::new();
489 let mut dones = VecSet::new();
490 while let Some(archive) = archives.0.pop() {
491 if dones.0.contains(&archive) {
492 continue;
493 }
494 dones.insert(archive.clone());
495 let Some(iri) = GlobalBackend.with_tree(|tree| {
496 let mut steps = archive.steps();
497 if let Some(mut n) = steps.next() {
498 let mut curr = tree.top.as_slice();
499 while let Some(g) = curr.iter().find_map(|a| match a {
500 AoG::Group(g) if g.id.last() == n => Some(g),
501 _ => None,
502 }) {
503 curr = g.children.as_slice();
504 if let Some(a) = curr.iter().find_map(|a| match a {
505 AoG::Archive(a) if a.is_meta() => Some(a),
506 _ => None,
507 }) && !ret.0.contains(a)
508 {
509 ret.insert(a.clone());
510 archives.insert(a.clone());
511 }
512 if let Some(m) = steps.next() {
513 n = m;
514 } else {
515 break;
516 }
517 }
518 }
519 tree.get(&archive).map(|a| a.uri().to_iri())
520 }) else {
521 return Err(format!("Archive {archive} not found"));
522 };
523 let res = GlobalBackend
524 .triple_store()
525 .query_str::<TokioEngine>(format!(
526 "SELECT DISTINCT ?a WHERE {{
527 <{}> ulo:contains ?d.
528 ?d rdf:type ulo:document .
529 ?d ulo:contains* ?x.
530 ?x (dc:requires|ulo:imports|dc:hasPart) ?m.
531 ?e ulo:contains? ?m.
532 ?e rdf:type ulo:document.
533 ?a ulo:contains ?e.
534 }}",
535 iri.as_str()
536 ))
537 .map_err(|e| e.to_string())?;
538 for i in res.into_uris::<ArchiveUri>() {
539 let id = i.archive_id();
540 if !ret.0.contains(id) {
541 archives.insert(id.clone());
542 ret.insert(id.clone());
543 }
544 }
545 }
546 Ok(ret.0)
547 })
548 .await
549 }
550 pub async fn build_status(
551 archive: Option<ArchiveId>,
552 path: Option<String>,
553 ) -> Result<FileStates, ServerFnError<String>> {
554 use flams_math_archives::Archive;
555 use flams_math_archives::backend::LocalBackend;
556 use flams_math_archives::manager::ArchiveOrGroup as AoG;
557 let login = LoginState::get_server();
558
559 blocking_server_fn(move || {
560 let allowed = matches!(
561 login,
562 LoginState::Admin
563 | LoginState::NoAccounts
564 | LoginState::User { is_admin: true, .. }
565 );
566 if !allowed {
567 return Err("Not logged in".to_string());
568 }
569 path.map_or_else(
570 || {
571 archive.as_ref().map_or_else(
572 || {
573 Ok(GlobalBackend
574 .with_tree(flams_math_archives::manager::ArchiveTree::state)
575 .into())
576 },
577 |archive| {
578 GlobalBackend.with_tree(|tree| {
579 match tree.get_group_or_archive(archive) {
580 None => Err(format!("Archive {archive} not found")),
581 Some(AoG::Archive(id)) => {
582 let Some(Archive::Local(archive)) = tree.get(id) else {
583 return Err(format!("Archive {archive} not found"));
584 };
585 Ok(archive.file_state().into())
586 }
587 Some(AoG::Group(g)) => Ok(g.state.clone().into()),
588 }
589 })
590 },
591 )
592 },
593 |path| {
594 let Some(archive) = archive.as_ref() else {
595 return Err("path without archive".to_string());
596 };
597 GlobalBackend.with_local_archive(archive, |a| {
598 let Some(a) = a else {
599 return Err(format!("Archive {archive} not found"));
600 };
601 a.with_sources(|d| match d.find(RelPath::new(&path)) {
602 Some(SourceEntryRef::Dir(d)) => Ok(d.state.clone().into()),
603 Some(SourceEntryRef::File(f)) => Ok((&*f.target_state).into()),
604 None => Err(format!("Directory {path} not found in archive {archive}")),
605 })
606 })
607 },
608 )
609 })
610 .await
611 }
612 pub fn archive_stream(
613 id: &ArchiveId,
614 ) -> Result<leptos::server_fn::codec::ByteStream<ServerFnError<String>>, ServerFnError<String>>
615 {
616 use futures::TryStreamExt;
617 let stream = GlobalBackend
618 .with_local_archive(id, |a| a.map(flams_system::zip::zip))
619 .ok_or_else(|| format!("No archive with id {id} found!"))?;
620 Ok(leptos::server_fn::codec::ByteStream::new(
621 stream.map_err(|e| e.to_string().into()), ))
623 }
624}