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 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 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 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 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()), ))
522 }
523}