flams_router_backend/
server_fns.rs

1use std::path::Path;
2
3use flams_ontology::{
4    archive_json::{
5        ArchiveData, ArchiveGroupData, ArchiveIndex, DirectoryData, FileData, Institution,
6    },
7    languages::Language,
8    uris::{ArchiveId, ArchiveURITrait, NarrativeURITrait, PathURITrait, URI, URIWithLanguage},
9};
10use flams_router_base::uris::URIComponents;
11use leptos::prelude::*;
12
13use crate::FileStates;
14
15#[server(prefix = "/api/backend", endpoint = "group_entries")]
16pub async fn group_entries(
17    r#in: Option<ArchiveId>,
18) -> Result<(Vec<ArchiveGroupData>, Vec<ArchiveData>), ServerFnError<String>> {
19    server::group_entries(r#in).await
20}
21
22#[server(prefix = "/api/backend", endpoint = "archive_entries")]
23pub async fn archive_entries(
24    archive: ArchiveId,
25    path: Option<String>,
26) -> Result<(Vec<DirectoryData>, Vec<FileData>), ServerFnError<String>> {
27    server::archive_entries(archive, path).await
28}
29
30#[server(prefix = "/api/backend", endpoint = "archive_dependencies")]
31pub async fn archive_dependencies(
32    archives: Vec<ArchiveId>,
33) -> Result<Vec<ArchiveId>, ServerFnError<String>> {
34    server::archive_dependencies(archives).await
35}
36
37#[server(prefix = "/api/backend", endpoint = "build_status")]
38pub async fn build_status(
39    archive: Option<ArchiveId>,
40    path: Option<String>,
41) -> Result<FileStates, ServerFnError<String>> {
42    server::build_status(archive, path).await
43}
44
45#[server(prefix = "/api/backend", endpoint = "source_file",
46    input=server_fn::codec::GetUrl,
47    output=server_fn::codec::Json)]
48#[allow(clippy::many_single_char_names)]
49#[allow(clippy::too_many_arguments)]
50pub async fn source_file(
51    uri: Option<URI>,
52    rp: Option<String>,
53    a: Option<ArchiveId>,
54    p: Option<String>,
55    l: Option<Language>,
56    d: Option<String>,
57    e: Option<String>,
58    m: Option<String>,
59    s: Option<String>,
60) -> Result<String, ServerFnError<String>> {
61    use flams_system::backend::{Backend, archives::LocalArchive};
62    use flams_web_utils::not_found;
63    fn get_root(
64        id: &ArchiveId,
65        and_then: impl FnOnce(&LocalArchive, String) -> Result<String, String>,
66    ) -> Result<String, String> {
67        use flams_git::GitUrlExt;
68        flams_system::backend::GlobalBackend::get().with_local_archive(id, |a| {
69            let Some(a) = a else {
70                not_found!("Archive {id} not found")
71            };
72            let repo = flams_git::repos::GitRepo::open(a.path())
73                .map_err(|_| format!("No git remote for {id} found"))?;
74            let url = repo
75                .get_origin_url()
76                .map_err(|_| format!("No git remote for {id} found"))?;
77            let https = url.into_https();
78            let mut url = https.to_string();
79            if https.git_suffix {
80                // remove .git
81                url.pop();
82                url.pop();
83                url.pop();
84                url.pop();
85            }
86            and_then(a, url)
87        })
88    }
89    fn get_source(id: &ArchiveId, path: Option<&str>) -> Result<String, String> {
90        get_root(id, |_, s| Ok(s)).map(|mut s| {
91            s.push_str("/-/tree/main/source/");
92            if let Some(p) = path {
93                s.push_str(p);
94            }
95            s
96        })
97    }
98    fn get_source_of_file<'a>(
99        id: &ArchiveId,
100        path: Option<&str>,
101        last: Option<&'a str>,
102        mut name: &'a str,
103        lang: Option<Language>,
104    ) -> Result<String, String> {
105        fn find(path: &Path, base: &mut String, name: &str, lang: Option<Language>) -> bool {
106            if let Some(lang) = lang {
107                // TODO add other file extensions here!
108                let filename = format!("{name}.{lang}.tex");
109                let p = path.join(&filename);
110                if p.exists() {
111                    base.push('/');
112                    base.push_str(&filename);
113                    return true;
114                }
115            } else {
116                // TODO add other file extensions here!
117                let filename = format!("{name}.en.tex");
118                let p = path.join(&filename);
119                if p.exists() {
120                    base.push('/');
121                    base.push_str(&filename);
122                    return true;
123                }
124            }
125            // TODO add other file extensions here!
126            let filename = format!("{name}.tex");
127            let p = path.join(&filename);
128            p.exists() && {
129                base.push('/');
130                base.push_str(&filename);
131                true
132            }
133        }
134        get_root(id, |a, mut base| {
135            base.push_str("/-/blob/main/source");
136            let mut source_path = a.source_dir();
137            if let Some(path) = path {
138                for s in path.split('/') {
139                    source_path = source_path.join(s);
140                    base.push('/');
141                    base.push_str(s);
142                }
143            }
144            if let Some(last) = last {
145                let np = source_path.join(last);
146                let mut nb = format!("{base}/{last}");
147                if find(&np, &mut nb, name, lang) {
148                    return Ok(base);
149                }
150                name = last;
151            }
152            if find(&source_path, &mut base, name, lang) {
153                Ok(base)
154            } else {
155                not_found!("No source file found")
156            }
157        })
158    }
159
160    tokio::task::spawn_blocking(move || {
161        let Result::<URIComponents, _>::Ok(comps) = (uri, rp, a, p, l, d, e, m, s).try_into()
162        else {
163            return Err("invalid uri components".to_string());
164        };
165        let Some(uri) = comps.parse() else {
166            return Err("invalid uri".to_string());
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::Narrative(n) => {
176                let doc = n.document();
177                let path_str = doc.path().map(ToString::to_string);
178                get_source_of_file(
179                    doc.archive_id(),
180                    path_str.as_deref(),
181                    None,
182                    doc.name().first_name().as_ref(),
183                    Some(doc.language()),
184                )
185            }
186            URI::Content(module) => {
187                let (path, last) = if let Some(p) = module.path() {
188                    let ps = p.to_string();
189                    if let Some((p, l)) = ps.rsplit_once('/') {
190                        (Some(p.to_string()), Some(l.to_string()))
191                    } else {
192                        (None, Some(ps))
193                    }
194                } else {
195                    (None, None)
196                };
197
198                get_source_of_file(
199                    module.archive_id(),
200                    path.as_deref(),
201                    last.as_deref(),
202                    module.name().first_name().as_ref(),
203                    None,
204                )
205            }
206        }
207    })
208    .await
209    .map_err(|e| e.to_string())?
210    .map_err(|s: String| s.into())
211}
212
213#[server(prefix="/api/backend",endpoint="download",
214  input=server_fn::codec::GetUrl,
215  output=server_fn::codec::Streaming
216)]
217pub async fn archive_stream(
218    id: ArchiveId,
219) -> Result<leptos::server_fn::codec::ByteStream<ServerFnError<String>>, ServerFnError<String>> {
220    server::archive_stream(id).await
221}
222
223#[server(
224  prefix="/api",
225  endpoint="index",
226  output=server_fn::codec::Json
227)]
228pub async fn index() -> Result<(Vec<Institution>, Vec<ArchiveIndex>), ServerFnError<String>> {
229    use flams_system::backend::GlobalBackend;
230    flams_web_utils::blocking_server_fn(|| {
231        let (a, b) = GlobalBackend::get().with_archive_tree(|t| t.index.clone());
232        Ok((a.0, b.0))
233    })
234    .await
235}
236
237#[cfg(feature = "ssr")]
238mod server {
239    use flams_ontology::{
240        archive_json::{ArchiveData, ArchiveGroupData, DirectoryData, FileData},
241        uris::{ArchiveId, ArchiveURI, ArchiveURITrait, URIOrRefTrait},
242    };
243    use flams_router_base::LoginState;
244    use flams_system::backend::{
245        Backend, GlobalBackend,
246        archives::{Archive, ArchiveOrGroup as AoG},
247    };
248    use flams_utils::vecmap::VecSet;
249    use flams_web_utils::blocking_server_fn;
250    use leptos::prelude::*;
251
252    use crate::FileStates;
253
254    pub async fn group_entries(
255        id: Option<ArchiveId>,
256    ) -> Result<(Vec<ArchiveGroupData>, Vec<ArchiveData>), ServerFnError<String>> {
257        let login = LoginState::get_server();
258        blocking_server_fn(move || {
259            let allowed = matches!(
260                login,
261                LoginState::Admin
262                    | LoginState::NoAccounts
263                    | LoginState::User { is_admin: true, .. }
264            );
265            flams_system::backend::GlobalBackend::get().with_archive_tree(|tree| {
266                let v = match id {
267                    None => &tree.groups,
268                    Some(id) => match tree.find(&id) {
269                        Some(AoG::Group(g)) => &g.children,
270                        _ => return Err(format!("Archive Group {id} not found").into()),
271                    },
272                };
273                let mut groups = Vec::new();
274                let mut archives = Vec::new();
275                for a in v {
276                    match a {
277                        AoG::Archive(id) => {
278                            let (summary, git) = if !allowed
279                                && flams_system::settings::Settings::get().gitlab_url.is_none()
280                            {
281                                (None, None)
282                            } else {
283                                tree.get(id)
284                                    .map(|a| {
285                                        if let Archive::Local(a) = a {
286                                            (
287                                                if allowed {
288                                                    Some(a.state_summary())
289                                                } else {
290                                                    None
291                                                },
292                                                a.is_managed().map(ToString::to_string),
293                                            )
294                                        } else {
295                                            (None, None)
296                                        }
297                                    })
298                                    .unwrap_or_default()
299                            };
300                            archives.push(ArchiveData {
301                                id: id.clone(),
302                                summary,
303                                git,
304                            });
305                        }
306                        AoG::Group(g) => {
307                            let summary = if allowed {
308                                Some(g.state.summarize())
309                            } else {
310                                None
311                            };
312                            groups.push(ArchiveGroupData {
313                                id: g.id.clone(),
314                                summary,
315                            });
316                        }
317                    }
318                }
319                Ok((groups, archives))
320            })
321        })
322        .await
323    }
324    pub async fn archive_entries(
325        archive: ArchiveId,
326        path: Option<String>,
327    ) -> Result<(Vec<DirectoryData>, Vec<FileData>), ServerFnError<String>> {
328        use either::Either;
329        use flams_system::backend::{Backend, archives::source_files::SourceEntry};
330        let login = LoginState::get_server();
331
332        blocking_server_fn(move || {
333            let allowed = matches!(
334                login,
335                LoginState::Admin
336                    | LoginState::NoAccounts
337                    | LoginState::User { is_admin: true, .. }
338            );
339            flams_system::backend::GlobalBackend::get().with_local_archive(&archive, |a| {
340                let Some(a) = a else {
341                    return Err(format!("Archive {archive} not found").into());
342                };
343                a.with_sources(|d| {
344                    let d = match path {
345                        None => d,
346                        Some(p) => match d.find(&p) {
347                            Some(Either::Left(d)) => d,
348                            _ => {
349                                return Err(format!(
350                                    "Directory {p} not found in archive {archive}"
351                                )
352                                .into());
353                            }
354                        },
355                    };
356                    let mut ds = Vec::new();
357                    let mut fs = Vec::new();
358                    for d in &d.children {
359                        match d {
360                            SourceEntry::Dir(d) => ds.push(DirectoryData {
361                                rel_path: d.relative_path.to_string(),
362                                summary: if allowed {
363                                    Some(d.state.summarize())
364                                } else {
365                                    None
366                                },
367                            }),
368                            SourceEntry::File(f) => fs.push(FileData {
369                                rel_path: f.relative_path.to_string(),
370                                format: f.format.to_string(),
371                            }),
372                        }
373                    }
374                    Ok((ds, fs))
375                })
376            })
377        })
378        .await
379    }
380
381    pub async fn archive_dependencies(
382        archives: Vec<ArchiveId>,
383    ) -> Result<Vec<ArchiveId>, ServerFnError<String>> {
384        use flams_system::backend::archives::ArchiveOrGroup;
385        let mut archives: VecSet<_> = archives.into_iter().collect();
386        blocking_server_fn(move || {
387            let mut ret = VecSet::new();
388            let mut dones = VecSet::new();
389            let backend = flams_system::backend::GlobalBackend::get();
390            while let Some(archive) = archives.0.pop() {
391                if dones.0.contains(&archive) {
392                    continue;
393                }
394                dones.insert(archive.clone());
395                let Some(iri) = backend.with_archive_tree(|tree| {
396                    let mut steps = archive.steps();
397                    if let Some(mut n) = steps.next() {
398                        let mut curr = tree.groups.as_slice();
399                        while let Some(g) = curr.iter().find_map(|a| match a {
400                            ArchiveOrGroup::Group(g) if g.id.last_name() == n => Some(g),
401                            _ => None,
402                        }) {
403                            curr = g.children.as_slice();
404                            if let Some(a) = curr.iter().find_map(|a| match a {
405                                ArchiveOrGroup::Archive(a) if a.is_meta() => Some(a),
406                                _ => None,
407                            }) {
408                                if !ret.0.contains(a) {
409                                    ret.insert(a.clone());
410                                    archives.insert(a.clone());
411                                }
412                            }
413                            if let Some(m) = steps.next() {
414                                n = m;
415                            } else {
416                                break;
417                            }
418                        }
419                    }
420                    tree.get(&archive).map(|a| a.uri().to_iri())
421                }) else {
422                    return Err(format!("Archive {archive} not found"));
423                };
424                let res = flams_system::backend::GlobalBackend::get()
425                    .triple_store()
426                    .query_str(format!(
427                        "SELECT DISTINCT ?a WHERE {{
428                            <{}> ulo:contains ?d.
429                            ?d rdf:type ulo:document .
430                            ?d ulo:contains* ?x.
431                            ?x (dc:requires|ulo:imports|dc:hasPart) ?m.
432                            ?e ulo:contains? ?m.
433                            ?e rdf:type ulo:document.
434                            ?a ulo:contains ?e.
435                        }}",
436                        iri.as_str()
437                    ))
438                    .map_err(|e| e.to_string())?;
439                for i in res.into_uris::<ArchiveURI>() {
440                    let id = i.archive_id();
441                    if !ret.0.contains(&id) {
442                        archives.insert(id.clone());
443                        ret.insert(id.clone());
444                    }
445                }
446            }
447            Ok(ret.0)
448        })
449        .await
450    }
451    pub async fn build_status(
452        archive: Option<ArchiveId>,
453        path: Option<String>,
454    ) -> Result<FileStates, ServerFnError<String>> {
455        use either::Either;
456        use flams_system::backend::Backend;
457        use flams_system::backend::archives::{Archive, ArchiveOrGroup as AoG};
458        let login = LoginState::get_server();
459
460        blocking_server_fn(move || {
461            let allowed = matches!(
462                login,
463                LoginState::Admin
464                    | LoginState::NoAccounts
465                    | LoginState::User { is_admin: true, .. }
466            );
467            if !allowed {
468                return Err("Not logged in".to_string().into());
469            }
470            path.map_or_else(
471                || {
472                    if let Some(archive) = archive.as_ref() {
473                        GlobalBackend::get().with_archive_tree(|tree| match tree.find(archive) {
474                            None => Err(format!("Archive {archive} not found").into()),
475                            Some(AoG::Archive(id)) => {
476                                let Some(Archive::Local(archive)) = tree.get(id) else {
477                                    return Err(format!("Archive {archive} not found").into());
478                                };
479                                Ok(archive.file_state().into())
480                            }
481                            Some(AoG::Group(g)) => Ok(g.state.clone().into()),
482                        })
483                    } else {
484                        Ok(GlobalBackend::get()
485                            .with_archive_tree(|tree| tree.state())
486                            .into())
487                    }
488                },
489                |path| {
490                    let Some(archive) = archive.as_ref() else {
491                        return Err("path without archive".to_string().into());
492                    };
493                    GlobalBackend::get().with_local_archive(&archive, |a| {
494                        let Some(a) = a else {
495                            return Err(format!("Archive {archive} not found").into());
496                        };
497                        a.with_sources(|d| match d.find(&path) {
498                            Some(Either::Left(d)) => Ok(d.state.clone().into()),
499                            Some(Either::Right(f)) => Ok((&f.target_state).into()),
500                            None => {
501                                Err(format!("Directory {path} not found in archive {archive}")
502                                    .into())
503                            }
504                        })
505                    })
506                },
507            )
508        })
509        .await
510    }
511    pub async fn archive_stream(
512        id: ArchiveId,
513    ) -> Result<leptos::server_fn::codec::ByteStream<ServerFnError<String>>, ServerFnError<String>>
514    {
515        use futures::TryStreamExt;
516        let stream = GlobalBackend::get()
517            .with_local_archive(&id, |a| a.map(|a| a.zip()))
518            .ok_or_else(|| format!("No archive with id {id} found!"))?;
519        Ok(leptos::server_fn::codec::ByteStream::new(
520            stream.map_err(|e| e.to_string().into()), //.map_err(|e| ServerFnError::new(e.to_string())),
521        ))
522    }
523}