ctoolbox/io/webui/controllers/
base.rs1use std::borrow::Cow;
4
5use anyhow::anyhow;
6use async_trait::async_trait;
7use axum::{
8 body::Body,
9 extract::State,
10 http::{HeaderValue, Uri, header},
11 response::{IntoResponse, Redirect, Response},
12};
13use axum_extra::response::file_stream::FileStream;
14use mime_guess;
15use tokio::fs::File;
16use tokio_util::io::ReaderStream;
17
18use crate::storage::get_asset;
19use crate::storage::pc_settings::DEFAULT_SERVER_URL;
20use crate::utilities::build_info;
21use crate::{
22 io::webui::{AppState, RequestState, error_400, error_404, render_view},
23 json_value,
24};
25
26fn json_redirect_response(location: &str) -> Response {
28 let mut resp = Response::new(Body::from(
29 serde_json::json!({ "url": location }).to_string(),
30 ));
31 resp.headers_mut().insert(
32 "X-CollectiveToolbox-IsJsRedirect",
33 HeaderValue::from_static("true"),
34 );
35 resp.headers_mut().insert(
36 header::CONTENT_TYPE,
37 HeaderValue::from_static("application/json"),
38 );
39 *resp.status_mut() = axum::http::StatusCode::OK;
40 resp
41}
42
43pub fn redirect_temporary_preserve_method(
47 is_js_req: bool,
48 location: &str,
49) -> Response {
50 if is_js_req {
51 json_redirect_response(location)
52 } else {
53 axum::response::Redirect::temporary(location).into_response()
54 }
55}
56
57pub fn redirect_temporary(
61 is_js_req: bool,
62 location: &str,
63) -> Response {
64 if is_js_req {
65 json_redirect_response(location)
66 } else {
67 axum::response::Redirect::to(location).into_response()
68 }
69}
70
71pub fn redirect_permanent(
75 is_js_req: bool,
76 location: &str,
77) -> Response {
78 if is_js_req {
79 json_redirect_response(location)
80 } else {
81 axum::response::Redirect::permanent(location).into_response()
82 }
83}
84
85pub async fn get_app_css(
86 State(state): State<AppState>,
87 req: RequestState,
88) -> Response {
89 match render_view(
90 &state.hbs,
91 "encre_css".to_string(),
92 &req,
93 &json_value!({}),
94 ) {
95 Ok(css) => {
96 let mut resp = Response::new(Body::from(css));
97 resp.headers_mut().insert(
98 header::CONTENT_TYPE,
99 HeaderValue::from_static("text/css"),
100 );
101 resp
102 }
103 Err(e) => error_400(&state, &req, e),
104 }
105}
106
107pub async fn get_installer_linux_x64(
108 State(state): State<AppState>,
109 req: RequestState,
110) -> Response {
111 let path = match std::env::current_exe() {
113 Ok(p) => p,
114 Err(e) => return error_400(&state, &req, anyhow!(e)),
115 };
116
117 let file = match File::open(&path).await {
119 Ok(f) => f,
120 Err(e) => return error_400(&state, &req, anyhow!(e)),
121 };
122
123 let stream = ReaderStream::new(file);
124
125 let build_info = build_info();
126 let version = build_info.version;
127 let commit = build_info.commit;
128
129 let file_name = format!("ctoolbox-installer-linux-x64-{version}-{commit}");
130
131 let file_stream_resp = FileStream::new(stream).file_name(file_name);
132 let mut resp = file_stream_resp.into_response();
133 resp.headers_mut().insert(
134 header::CONTENT_TYPE,
135 HeaderValue::from_static("application/x-executable"),
136 );
137 resp
138}
139
140fn get_asset_zip(state: &AppState, req: RequestState, path: &str) -> Response {
141 let path = format!("{path}.zip");
142 let asset = get_asset(&path);
143
144 if let Some(bytes) = asset {
145 let build_info = build_info();
146 let version = build_info.version;
147 let commit = build_info.commit;
148
149 let file_name = format!("ctoolbox-{path}-{version}-{commit}.zip");
150
151 let mut resp = Response::new(Body::from(bytes));
152 resp.headers_mut().insert(
153 header::CONTENT_TYPE,
154 HeaderValue::from_static("application/zip"),
155 );
156
157 if let Ok(disposition) = HeaderValue::from_str(&format!(
159 "attachment; filename=\"{file_name}\""
160 )) {
161 resp.headers_mut()
162 .insert(header::CONTENT_DISPOSITION, disposition);
163 }
164
165 return resp;
166 }
167
168 error_404(
170 state,
171 &req,
172 format!("The requested URL '{path}' was not found."),
173 )
174}
175
176pub async fn get_src_zip(
177 State(state): State<AppState>,
178 req: RequestState,
179) -> Response {
180 return get_asset_zip(&state, req, "src");
181}
182
183pub async fn get_dependencies_zip(
192 State(state): State<AppState>,
193 req: RequestState,
194) -> Response {
195 let exe_path = match std::env::current_exe() {
197 Ok(p) => p,
198 Err(e) => return error_400(&state, &req, anyhow!(e)),
199 };
200 let build_info = build_info();
201 let version = build_info.version;
202 let commit = build_info.commit;
203 let deps_original_filename = format!("ctoolbox-dependencies-{commit}.zip");
204 let deps_filename = format!("ctoolbox-dependencies-{version}-{commit}.zip");
205 let deps_primary_url = format!("{DEFAULT_SERVER_URL}/dependencies.zip");
206
207 let deps_path = match exe_path.parent() {
208 Some(parent) => parent.join(&deps_original_filename),
209 None => {
210 return redirect_temporary(req.is_js_request, &deps_primary_url);
211 }
212 };
213
214 match File::open(&deps_path).await {
216 Ok(file) => {
217 let stream = ReaderStream::new(file);
218 let file_stream_resp =
219 FileStream::new(stream).file_name(deps_filename);
220 let mut resp = file_stream_resp.into_response();
221 resp.headers_mut().insert(
222 header::CONTENT_TYPE,
223 HeaderValue::from_static("application/zip"),
224 );
225 resp
226 }
227 Err(e) => {
228 if e.kind() == std::io::ErrorKind::NotFound {
229 redirect_temporary(req.is_js_request, &deps_primary_url)
230 } else {
231 error_400(&state, &req, anyhow!(e))
232 }
233 }
234 }
235}
236
237pub async fn get_doc_index(
238 State(state): State<AppState>,
239 req: RequestState,
240) -> Response {
241 redirect_temporary(req.is_js_request, "/docs/rust/ctoolbox/index.html")
242}
243
244pub async fn get_doc_page(
245 State(state): State<AppState>,
246 req: RequestState,
247 path: axum::extract::Path<String>,
248) -> Response {
249 let path = path.as_str();
250
251 asset_or_404(&state, req, format!("/docs/{path}").as_str())
252}
253
254pub async fn static_or_404(
255 State(state): State<AppState>,
256 req: RequestState,
257 uri: Uri,
258) -> Response {
259 asset_or_404(&state, req, uri.path())
260}
261
262fn asset_or_404(state: &AppState, req: RequestState, path: &str) -> Response {
263 let path = path.trim_start_matches('/');
264
265 let mut asset = get_asset(path);
267 if asset.is_none() {
268 asset = get_asset(format!("web/{path}").as_str());
269 }
270
271 if let Some(bytes) = asset {
272 let mime_guess = mime_guess::from_path(path).first();
273 let mime_guess_str: Cow<'static, str> = match mime_guess {
274 Some(mime) => Cow::Owned(mime.essence_str().to_string()),
275 None => Cow::Borrowed("application/octet-stream"),
276 };
277
278 let mut resp = Response::new(Body::from(bytes));
279 resp.headers_mut().insert(
280 header::CONTENT_TYPE,
281 HeaderValue::from_str(mime_guess_str.as_ref()).unwrap_or_else(
282 |_| HeaderValue::from_static("application/octet-stream"),
283 ),
284 );
285 return resp;
286 }
287
288 error_404(
290 state,
291 &req,
292 format!("The requested URL '{path}' was not found."),
293 )
294}
295
296#[cfg(test)]
297mod tests {
298 use crate::io::webui::test_helpers::{
299 test_get_no_login, test_get_no_login_json, test_get_redirect_no_login,
300 };
301 use crate::utilities::{
302 assert_string_contains, assert_string_not_contains,
303 };
304
305 #[crate::ctb_test(tokio::test)]
306 async fn can_get_doc_index() {
307 let (status, location) = test_get_redirect_no_login("/docs").await;
308 assert_eq!(status, 303);
309 assert_eq!(location, "/docs/rust/ctoolbox/index.html");
310 }
311
312 #[crate::ctb_test(tokio::test)]
313 async fn can_get_rust_index() {
314 let (status, body) =
315 test_get_no_login("/docs/rust/ctoolbox/index.html").await;
316 assert_eq!(status, 200);
317 assert!(body.contains("<title>ctoolbox - Rust</title>"));
318 }
319
320 #[crate::ctb_test(tokio::test)]
321 async fn can_get_css() {
322 let (status, body) = test_get_no_login("/app.css").await;
323 assert_eq!(status, 200);
324 assert!(body.contains("Abstract-Polygon-Background")); assert!(body.contains("SFMono-Regular")); }
327
328 #[crate::ctb_test(tokio::test)]
329 async fn can_download_source() {
330 let (status, body) = test_get_no_login("/src.zip").await;
331 assert_eq!(status, 200);
332 assert!(body.starts_with("PK"));
333 }
334
335 #[crate::ctb_test(tokio::test)]
336 async fn can_get_404() {
337 let (status, body) = test_get_no_login("/nonexistent").await;
338 assert_eq!(status, 404);
339 assert_string_contains("was not found.</h1>", &body);
340 }
341
342 #[crate::ctb_test(tokio::test)]
343 async fn can_get_404_json() {
344 let (status, body) = test_get_no_login_json("/nonexistent").await;
345 assert_eq!(status, 404);
346 assert_string_contains("was not found.", &body);
347 assert_string_not_contains("was not found.</h1>", &body);
348 }
349}