Skip to main content

flams/server/
files.rs

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