1use super::ServerState;
2use axum::body::Body;
3use flams_math_archives::{
4 backend::{GlobalBackend, LocalBackend},
5 LocallyBuilt, MathArchive,
6};
7use flams_system::settings::Settings;
8use ftml_ontology::utils::time::Timestamp;
9use ftml_uris::{
10 components::{DocumentUriComponentTuple, DocumentUriComponents},
11 ArchiveId, DocumentUri, IsNarrativeUri, Language, UriWithArchive, UriWithPath,
12};
13use http::Request;
14use std::{borrow::Cow, path::PathBuf, sync::atomic::AtomicU64};
15use tower::ServiceExt;
16use tower_http::services::{fs::ServeFileSystemResponseBody, ServeFile};
17
18#[derive(Clone, Default)]
19pub struct ImageStore(flams_utils::triomphe::Arc<ImageStoreI>);
20
21#[derive(Default)]
22struct ImageStoreI {
23 map: dashmap::DashMap<ImageSpec, ImageData>,
24 count: AtomicU64,
25}
26
27#[derive(Clone, Debug, Hash, PartialEq, Eq)]
28pub enum ImageSpec {
29 Kpse(Box<str>),
30 ARp(ArchiveId, Box<str>),
31 File(Box<str>),
32}
33impl ImageSpec {
34 pub fn path(&self) -> Option<PathBuf> {
35 match self {
36 Self::Kpse(p) => tex_engine::engine::filesystem::kpathsea::KPATHSEA.which(p),
37 Self::ARp(a, p) => {
38 GlobalBackend.with_local_archive(a, |a| a.map(|a| a.path().join(&**p)))
39 }
40 Self::File(p) => Some(std::path::PathBuf::from(p.to_string())),
41 }
42 }
43}
44
45pub struct ImageData {
46 img: Box<[u8]>,
47 timestamp: AtomicU64,
48}
49impl ImageData {
50 pub fn update(&self) {
51 let now = Timestamp::now();
52 self.timestamp
53 .store(now.0.get() as _, std::sync::atomic::Ordering::SeqCst);
54 }
55 pub fn new(data: &[u8]) -> Self {
56 Self {
57 img: data.into(),
58 timestamp: AtomicU64::new(Timestamp::now().0.get()),
59 }
60 }
61}
62
63pub(crate) async fn img_handler(
64 uri: http::Uri,
65 axum::extract::State(ServerState { images: _, .. }): axum::extract::State<ServerState>,
66 ) -> axum::response::Response<ServeFileSystemResponseBody> {
68 let default = || {
69 let mut resp = axum::response::Response::new(ServeFileSystemResponseBody::default());
70 *resp.status_mut() = http::StatusCode::NOT_FOUND;
71 resp
72 };
73
74 let Some(s) = uri.query() else {
75 return default();
76 };
77
78 let spec = if let Some(s) = s.strip_prefix("kpse=") {
79 ImageSpec::Kpse(s.into())
80 } else if let Some(f) = s.strip_prefix("file=") {
81 if Settings::get().lsp {
82 ImageSpec::File(f.into())
83 } else {
84 return default();
85 }
86 } else if let Some(s) = s.strip_prefix("a=") {
87 let Some((a, rp)) = s.split_once("&rp=") else {
88 return default();
89 };
90 let a = a.parse().unwrap_or_else(|_| unreachable!());
91 let rp = rp.into();
92 ImageSpec::ARp(a, rp)
93 } else {
94 return default();
95 };
96
97 if let Some(p) = spec.path() {
99 let req = Request::builder()
100 .uri(uri.clone())
101 .body(Body::empty())
102 .unwrap();
103 ServeFile::new(p)
104 .oneshot(req)
105 .await
106 .unwrap_or_else(|_| default())
107 } else {
108 default()
109 }
110}
111
112pub(crate) async fn doc_handler(
113 uri: http::Uri,
114) -> axum::response::Response<ServeFileSystemResponseBody> {
115 let req_uri = uri;
116 let default = || {
117 let mut resp = axum::response::Response::new(ServeFileSystemResponseBody::default());
118 *resp.status_mut() = http::StatusCode::NOT_FOUND;
119 resp
120 };
121 let err = |s: &str| {
122 let mut resp = axum::response::Response::new(ServeFileSystemResponseBody::default());
123 tracing::info!("pdf download error: {s}");
124 *resp.status_mut() = http::StatusCode::BAD_REQUEST;
125 resp
126 };
127
128 let Some(params) = Params::new(&req_uri) else {
129 return err("Invalid URI");
130 };
131
132 macro_rules! parse {
133 ($id:literal) => {
134 if let Some(s) = params.get_str($id) {
135 let Ok(r) = s.parse() else {
136 return err("malformed uri");
137 };
138 Some(r)
139 } else {
140 None
141 }
142 };
143 }
144 let Some(format) = params.get_str("format") else {
145 return err("Missing format");
146 };
147
148 let uri: Option<DocumentUri> = parse!("uri");
149 let rp = params.get("rp");
150 let a: Option<ArchiveId> = parse!("a");
151 let p = params.get("p");
152 let l: Option<Language> = parse!("l");
153 let d = params.get("d");
154
155 let comps = DocumentUriComponentTuple {
156 uri,
157 rp,
158 a,
159 p,
160 d,
161 l,
162 };
163
164 let comps: Result<DocumentUriComponents, _> = comps.try_into();
165 let uri = if let Ok(comps) = comps {
166 let Ok(uri) =
167 comps.parse(|a| GlobalBackend.with_archive(a, |a| a.map(|a| a.uri().clone())))
168 else {
169 return err("Malformed URI components");
170 };
171 uri
172 } else {
173 return err("Malformed URI components");
174 };
175 let uri2 = uri.clone();
176 let formatstr = format.to_string();
177 let Ok(Some(path)) = tokio::task::spawn_blocking(move || {
178 GlobalBackend.with_local_archive(uri.archive_id(), |a| {
179 a.map(|a| {
180 a.out_path_of(uri.path(), uri.document_name(), None, uri.language)
181 .join(&formatstr)
182 })
183 })
184 })
185 .await
186 else {
187 return default();
188 };
189
190 let pandq = format!("/{}.{format}", uri2.document_name());
191 let mime = mime_guess::from_ext(&format).first_or_octet_stream();
192 let req_uri = http::Uri::builder()
193 .path_and_query(pandq)
194 .build()
195 .unwrap_or(req_uri);
196 let req = Request::builder()
197 .uri(req_uri)
198 .body(Body::empty())
199 .expect("this is a bug");
200 ServeFile::new_with_mime(path, &mime)
201 .oneshot(req)
202 .await
203 .unwrap_or_else(|_| default())
204}
205
206struct Params<'a>(&'a str);
207impl<'a> Params<'a> {
208 fn new(uri: &'a http::Uri) -> Option<Self> {
209 uri.query().map(Self)
210 }
211 fn get_str(&self, name: &str) -> Option<Cow<'_, str>> {
212 self.0
213 .split('&')
214 .find(|s| s.starts_with(name) && s.as_bytes().get(name.len()) == Some(&b'='))?
215 .split('=')
216 .nth(1)
217 .and_then(|s| urlencoding::decode(s).ok())
218 }
219 fn get(&self, name: &str) -> Option<String> {
220 self.get_str(name).map(Cow::into_owned)
221 }
222}