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) => 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()), //.map_err(|e| ServerFnError::new(e.to_string())),
627        ))
628    }
629}