refactor: update AppState references to use state module
- Change all instances of AppState to reference the new state module across multiple files for consistency. - Clean up imports in auth, books, index_jobs, libraries, pages, search, settings, thumbnails, and tokens modules. - Simplify main.rs by removing unused code and organizing middleware and route handlers under the new handlers module.
This commit is contained in:
42
apps/api/src/api_middleware.rs
Normal file
42
apps/api/src/api_middleware.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use std::time::Duration;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
pub async fn request_counter(
|
||||
State(state): State<AppState>,
|
||||
req: axum::extract::Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
state.metrics.requests_total.fetch_add(1, Ordering::Relaxed);
|
||||
next.run(req).await
|
||||
}
|
||||
|
||||
pub async fn read_rate_limit(
|
||||
State(state): State<AppState>,
|
||||
req: axum::extract::Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let mut limiter = state.read_rate_limit.lock().await;
|
||||
if limiter.window_started_at.elapsed() >= Duration::from_secs(1) {
|
||||
limiter.window_started_at = std::time::Instant::now();
|
||||
limiter.requests_in_window = 0;
|
||||
}
|
||||
|
||||
if limiter.requests_in_window >= 120 {
|
||||
return (
|
||||
axum::http::StatusCode::TOO_MANY_REQUESTS,
|
||||
"rate limit exceeded",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
limiter.requests_in_window += 1;
|
||||
drop(limiter);
|
||||
next.run(req).await
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use axum::{
|
||||
use chrono::Utc;
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::{error::ApiError, AppState};
|
||||
use crate::{error::ApiError, state::AppState};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Scope {
|
||||
|
||||
@@ -5,7 +5,7 @@ use sqlx::Row;
|
||||
use uuid::Uuid;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{error::ApiError, AppState};
|
||||
use crate::{error::ApiError, state::AppState};
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ListBooksQuery {
|
||||
|
||||
26
apps/api/src/handlers.rs
Normal file
26
apps/api/src/handlers.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use axum::{extract::State, Json};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::{error::ApiError, state::AppState};
|
||||
|
||||
pub async fn health() -> &'static str {
|
||||
"ok"
|
||||
}
|
||||
|
||||
pub async fn docs_redirect() -> impl axum::response::IntoResponse {
|
||||
axum::response::Redirect::to("/swagger-ui/")
|
||||
}
|
||||
|
||||
pub async fn ready(State(state): State<AppState>) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
sqlx::query("SELECT 1").execute(&state.pool).await?;
|
||||
Ok(Json(serde_json::json!({"status": "ready"})))
|
||||
}
|
||||
|
||||
pub async fn metrics(State(state): State<AppState>) -> String {
|
||||
format!(
|
||||
"requests_total {}\npage_cache_hits {}\npage_cache_misses {}\n",
|
||||
state.metrics.requests_total.load(Ordering::Relaxed),
|
||||
state.metrics.page_cache_hits.load(Ordering::Relaxed),
|
||||
state.metrics.page_cache_misses.load(Ordering::Relaxed),
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use tokio_stream::Stream;
|
||||
use uuid::Uuid;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{error::ApiError, AppState};
|
||||
use crate::{error::ApiError, state::AppState};
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct RebuildRequest {
|
||||
|
||||
@@ -6,7 +6,7 @@ use sqlx::Row;
|
||||
use uuid::Uuid;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{error::ApiError, AppState};
|
||||
use crate::{error::ApiError, state::AppState};
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct LibraryResponse {
|
||||
|
||||
@@ -1,91 +1,36 @@
|
||||
mod auth;
|
||||
mod books;
|
||||
mod error;
|
||||
mod handlers;
|
||||
mod index_jobs;
|
||||
mod libraries;
|
||||
mod api_middleware;
|
||||
mod openapi;
|
||||
mod pages;
|
||||
mod search;
|
||||
mod settings;
|
||||
mod state;
|
||||
mod thumbnails;
|
||||
mod tokens;
|
||||
|
||||
use std::{
|
||||
num::NonZeroUsize,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use axum::{
|
||||
middleware,
|
||||
response::IntoResponse,
|
||||
routing::{delete, get},
|
||||
Json, Router,
|
||||
Router,
|
||||
};
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
use lru::LruCache;
|
||||
use std::num::NonZeroUsize;
|
||||
use stripstream_core::config::ApiConfig;
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use tokio::sync::{Mutex, Semaphore};
|
||||
use tracing::info;
|
||||
use sqlx::{Pool, Postgres, Row};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
pool: sqlx::PgPool,
|
||||
bootstrap_token: Arc<str>,
|
||||
meili_url: Arc<str>,
|
||||
meili_master_key: Arc<str>,
|
||||
page_cache: Arc<Mutex<LruCache<String, Arc<Vec<u8>>>>>,
|
||||
page_render_limit: Arc<Semaphore>,
|
||||
metrics: Arc<Metrics>,
|
||||
read_rate_limit: Arc<Mutex<ReadRateLimit>>,
|
||||
}
|
||||
|
||||
struct Metrics {
|
||||
requests_total: AtomicU64,
|
||||
page_cache_hits: AtomicU64,
|
||||
page_cache_misses: AtomicU64,
|
||||
}
|
||||
|
||||
struct ReadRateLimit {
|
||||
window_started_at: Instant,
|
||||
requests_in_window: u32,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
requests_total: AtomicU64::new(0),
|
||||
page_cache_hits: AtomicU64::new(0),
|
||||
page_cache_misses: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_concurrent_renders(pool: &Pool<Postgres>) -> usize {
|
||||
let default_concurrency = 8;
|
||||
let row = sqlx::query(r#"SELECT value FROM app_settings WHERE key = 'limits'"#)
|
||||
.fetch_optional(pool)
|
||||
.await;
|
||||
|
||||
match row {
|
||||
Ok(Some(row)) => {
|
||||
let value: Value = row.get("value");
|
||||
value
|
||||
.get("concurrent_renders")
|
||||
.and_then(|v: &Value| v.as_u64())
|
||||
.map(|v| v as usize)
|
||||
.unwrap_or(default_concurrency)
|
||||
}
|
||||
_ => default_concurrency,
|
||||
}
|
||||
}
|
||||
use crate::state::{load_concurrent_renders, AppState, Metrics, ReadRateLimit};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
@@ -150,21 +95,21 @@ async fn main() -> anyhow::Result<()> {
|
||||
.route("/books/:id/pages/:n", get(pages::get_page))
|
||||
.route("/libraries/:library_id/series", get(books::list_series))
|
||||
.route("/search", get(search::search_books))
|
||||
.route_layer(middleware::from_fn_with_state(state.clone(), read_rate_limit))
|
||||
.route_layer(middleware::from_fn_with_state(state.clone(), api_middleware::read_rate_limit))
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
auth::require_read,
|
||||
));
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/ready", get(ready))
|
||||
.route("/metrics", get(metrics))
|
||||
.route("/docs", get(docs_redirect))
|
||||
.route("/health", get(handlers::health))
|
||||
.route("/ready", get(handlers::ready))
|
||||
.route("/metrics", get(handlers::metrics))
|
||||
.route("/docs", get(handlers::docs_redirect))
|
||||
.merge(SwaggerUi::new("/swagger-ui").url("/openapi.json", openapi::ApiDoc::openapi()))
|
||||
.merge(admin_routes)
|
||||
.merge(read_routes)
|
||||
.layer(middleware::from_fn_with_state(state.clone(), request_counter))
|
||||
.layer(middleware::from_fn_with_state(state.clone(), api_middleware::request_counter))
|
||||
.with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&config.listen_addr).await?;
|
||||
@@ -173,57 +118,3 @@ async fn main() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn health() -> &'static str {
|
||||
"ok"
|
||||
}
|
||||
|
||||
async fn docs_redirect() -> impl axum::response::IntoResponse {
|
||||
axum::response::Redirect::to("/swagger-ui/")
|
||||
}
|
||||
|
||||
async fn ready(axum::extract::State(state): axum::extract::State<AppState>) -> Result<Json<serde_json::Value>, error::ApiError> {
|
||||
sqlx::query("SELECT 1").execute(&state.pool).await?;
|
||||
Ok(Json(serde_json::json!({"status": "ready"})))
|
||||
}
|
||||
|
||||
async fn metrics(axum::extract::State(state): axum::extract::State<AppState>) -> String {
|
||||
format!(
|
||||
"requests_total {}\npage_cache_hits {}\npage_cache_misses {}\n",
|
||||
state.metrics.requests_total.load(Ordering::Relaxed),
|
||||
state.metrics.page_cache_hits.load(Ordering::Relaxed),
|
||||
state.metrics.page_cache_misses.load(Ordering::Relaxed),
|
||||
)
|
||||
}
|
||||
|
||||
async fn request_counter(
|
||||
axum::extract::State(state): axum::extract::State<AppState>,
|
||||
req: axum::extract::Request,
|
||||
next: axum::middleware::Next,
|
||||
) -> axum::response::Response {
|
||||
state.metrics.requests_total.fetch_add(1, Ordering::Relaxed);
|
||||
next.run(req).await
|
||||
}
|
||||
|
||||
async fn read_rate_limit(
|
||||
axum::extract::State(state): axum::extract::State<AppState>,
|
||||
req: axum::extract::Request,
|
||||
next: axum::middleware::Next,
|
||||
) -> axum::response::Response {
|
||||
let mut limiter = state.read_rate_limit.lock().await;
|
||||
if limiter.window_started_at.elapsed() >= Duration::from_secs(1) {
|
||||
limiter.window_started_at = Instant::now();
|
||||
limiter.requests_in_window = 0;
|
||||
}
|
||||
|
||||
if limiter.requests_in_window >= 120 {
|
||||
return (
|
||||
axum::http::StatusCode::TOO_MANY_REQUESTS,
|
||||
"rate limit exceeded",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
limiter.requests_in_window += 1;
|
||||
drop(limiter);
|
||||
next.run(req).await
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ use tracing::{debug, error, info, instrument, warn};
|
||||
use uuid::Uuid;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::{error::ApiError, AppState};
|
||||
use crate::{error::ApiError, state::AppState};
|
||||
|
||||
fn remap_libraries_path(path: &str) -> String {
|
||||
if let Ok(root) = std::env::var("LIBRARIES_ROOT_PATH") {
|
||||
|
||||
@@ -2,7 +2,7 @@ use axum::{extract::{Query, State}, Json};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{error::ApiError, AppState};
|
||||
use crate::{error::ApiError, state::AppState};
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct SearchQuery {
|
||||
|
||||
@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::{error::ApiError, AppState};
|
||||
use crate::{error::ApiError, state::AppState};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateSettingRequest {
|
||||
|
||||
61
apps/api/src/state.rs
Normal file
61
apps/api/src/state.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use std::sync::{
|
||||
atomic::AtomicU64,
|
||||
Arc,
|
||||
};
|
||||
use std::time::Instant;
|
||||
|
||||
use lru::LruCache;
|
||||
use sqlx::{Pool, Postgres, Row};
|
||||
use tokio::sync::{Mutex, Semaphore};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub pool: sqlx::PgPool,
|
||||
pub bootstrap_token: Arc<str>,
|
||||
pub meili_url: Arc<str>,
|
||||
pub meili_master_key: Arc<str>,
|
||||
pub page_cache: Arc<Mutex<LruCache<String, Arc<Vec<u8>>>>>,
|
||||
pub page_render_limit: Arc<Semaphore>,
|
||||
pub metrics: Arc<Metrics>,
|
||||
pub read_rate_limit: Arc<Mutex<ReadRateLimit>>,
|
||||
}
|
||||
|
||||
pub struct Metrics {
|
||||
pub requests_total: AtomicU64,
|
||||
pub page_cache_hits: AtomicU64,
|
||||
pub page_cache_misses: AtomicU64,
|
||||
}
|
||||
|
||||
pub struct ReadRateLimit {
|
||||
pub window_started_at: Instant,
|
||||
pub requests_in_window: u32,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
requests_total: AtomicU64::new(0),
|
||||
page_cache_hits: AtomicU64::new(0),
|
||||
page_cache_misses: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_concurrent_renders(pool: &Pool<Postgres>) -> usize {
|
||||
let default_concurrency = 8;
|
||||
let row = sqlx::query(r#"SELECT value FROM app_settings WHERE key = 'limits'"#)
|
||||
.fetch_optional(pool)
|
||||
.await;
|
||||
|
||||
match row {
|
||||
Ok(Some(row)) => {
|
||||
let value: serde_json::Value = row.get("value");
|
||||
value
|
||||
.get("concurrent_renders")
|
||||
.and_then(|v: &serde_json::Value| v.as_u64())
|
||||
.map(|v| v as usize)
|
||||
.unwrap_or(default_concurrency)
|
||||
}
|
||||
_ => default_concurrency,
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ use tracing::{info, warn};
|
||||
use uuid::Uuid;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{error::ApiError, index_jobs, pages, AppState};
|
||||
use crate::{error::ApiError, index_jobs, pages, state::AppState};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ThumbnailConfig {
|
||||
|
||||
@@ -8,7 +8,7 @@ use sqlx::Row;
|
||||
use uuid::Uuid;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{error::ApiError, AppState};
|
||||
use crate::{error::ApiError, state::AppState};
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct CreateTokenRequest {
|
||||
|
||||
Reference in New Issue
Block a user