1mod auth;
2pub mod client;
3pub mod config;
4pub mod converter;
5pub(crate) mod errors;
6pub mod state;
7mod views;
8
9use axum::{
10 body::{self, Empty, Full},
11 error_handling::HandleErrorLayer,
12 extract::Path,
13 http::{header, HeaderValue, Response, StatusCode},
14 response::{IntoResponse, Redirect},
15 routing::get,
16 BoxError, Router,
17};
18use chrono::Duration;
19use config::CliConfig;
20use include_dir::{include_dir, Dir};
21use state::GlobalAppState;
22use std::sync::Arc;
23use tower::ServiceBuilder;
24use tower_sessions::{
25 cookie::SameSite, sqlx::SqlitePool, MokaStore, SessionManagerLayer, SessionStore, SqliteStore,
26};
27
28static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static");
29static TEMPLATES_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates");
30
31pub type Result<T> = std::result::Result<T, errors::AppError>;
32
33async fn static_file(Path(path): Path<String>) -> Result<impl IntoResponse> {
34 let path = path.trim_start_matches('/');
35 let mime_type = mime_guess::from_path(path).first_or_text_plain();
36
37 let response = match STATIC_DIR.get_file(path) {
38 None => Response::builder()
39 .status(StatusCode::NOT_FOUND)
40 .body(body::boxed(Empty::new()))?,
41 Some(file) => Response::builder()
42 .status(StatusCode::OK)
43 .header(
44 header::CONTENT_TYPE,
45 HeaderValue::from_str(mime_type.as_ref()).unwrap(),
46 )
47 .body(body::boxed(Full::from(file.contents())))?,
48 };
49 Ok(response)
50}
51
52pub async fn app(config: &CliConfig, cleanup_interval: Duration) -> Result<Router> {
53 let global_state = GlobalAppState::new(config)?;
54 let global_state = Arc::new(global_state);
55
56 if let Some(session_file) = &config.session_file {
57 let db_uri = format!("sqlite://{}?mode=rwc", session_file.to_string_lossy());
58 let db_pool = SqlitePool::connect(&db_uri).await?;
59 let store = SqliteStore::new(db_pool);
60 store.migrate().await?;
61
62 tokio::task::spawn(
63 store
64 .clone()
65 .continuously_delete_expired(cleanup_interval.to_std()?),
66 );
67
68 app_with_state(global_state, store, cleanup_interval).await
69 } else {
70 let store = MokaStore::new(Some(1_000));
72 app_with_state(global_state, store, cleanup_interval).await
73 }
74}
75
76async fn app_with_state<S: SessionStore>(
77 global_state: Arc<GlobalAppState>,
78 session_store: S,
79 cleanup_interval: Duration,
80) -> Result<Router> {
81 let routes = Router::new()
82 .route("/", get(|| async { Redirect::temporary("corpora") }))
83 .route("/static/*path", get(static_file))
84 .nest("/corpora", views::corpora::create_routes()?)
85 .nest("/export", views::export::create_routes()?)
86 .nest("/about", views::about::create_routes()?)
87 .nest("/oauth", views::oauth::create_routes()?)
88 .with_state(global_state.clone());
89
90 let session_service = ServiceBuilder::new()
91 .layer(HandleErrorLayer::new(|_: BoxError| async {
92 StatusCode::BAD_REQUEST
93 }))
94 .layer(SessionManagerLayer::new(session_store).with_same_site(SameSite::Lax));
95 let cleanup_interval = cleanup_interval.to_std()?;
96
97 tokio::task::spawn(async move {
98 loop {
99 tokio::time::sleep(cleanup_interval).await;
100 global_state.cleanup().await;
101 }
102 });
103
104 Ok(routes.layer(session_service))
105}
106
107#[cfg(test)]
108pub mod tests;