Skip to main content

flams_router_backend/
server_fns.rs

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                    // remove .git
80                    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                    // TODO add other file extensions here!
107                    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                    // TODO add other file extensions here!
116                    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                // TODO add other file extensions here!
125                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()), //.map_err(|e| ServerFnError::new(e.to_string())),
622        ))
623    }
624}