flams/server/
files.rs

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    //request: http::Request<axum::body::Body>,
67) -> 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    //tracing::info!("HERE: {spec:?}");
98    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}