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