annis_web/
lib.rs

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        // Fallback to a a store based on a cache
71        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;