repos
/ finance-rust master

finance-rust

mirror archived upstream

Single-binary self-hosted market watcher for stocks, ETFs, indexes, and futures: live charts, key stats, fundamentals, SEC filings, and SSE streaming.

axumdockerfinancerustself-hostedsqlitestocksvite

4.3 KB · 125 lines · Rust Raw History
  1use axum::{http::HeaderValue, middleware as axum_middleware, Router};
  2use minijinja::Environment;
  3use sqlx::SqlitePool;
  4use std::path::PathBuf;
  5use std::sync::Arc;
  6use tower_http::compression::CompressionLayer;
  7use tower_http::services::ServeDir;
  8use tower_http::set_header::SetResponseHeaderLayer;
  9
 10use crate::routes;
 11use crate::{db, middleware, templates};
 12
 13/// A current desktop Chrome string. Outbound data requests carry this so the
 14/// public upstreams (Yahoo, SEC) see an ordinary-looking browser.
 15/// Override with FINANCE_USER_AGENT.
 16const DEFAULT_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
 17    AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36";
 18
 19#[derive(Clone)]
 20pub struct AppState {
 21    pub env: Arc<Environment<'static>>,
 22    pub pool: SqlitePool,
 23    pub config: Arc<Config>,
 24    /// Live-data pub/sub hub: shared by the `/stream` route and the scheduler.
 25    pub hub: Arc<crate::stream::Hub>,
 26}
 27
 28#[derive(Debug, Clone)]
 29pub struct Config {
 30    /// Project root: where `templates/` and `dist/` live.
 31    pub root: PathBuf,
 32    /// Where `db.sqlite3` lives.
 33    pub data_dir: PathBuf,
 34    /// Absolute origin for sitemap / og tags. No trailing slash. May be empty.
 35    pub base_url: String,
 36    pub site_title: String,
 37    /// User-Agent sent on every outbound data request.
 38    pub user_agent: String,
 39    /// Appended to the User-Agent on sec.gov requests so SEC can identify us.
 40    pub sec_contact_email: String,
 41    /// Which `QuoteProvider` impl to use for live data.
 42    pub quote_provider: String,
 43}
 44
 45impl AppState {
 46    pub async fn from_env() -> anyhow::Result<Self> {
 47        let root: PathBuf = std::env::var("FINANCE_ROOT")
 48            .map(PathBuf::from)
 49            .unwrap_or_else(|_| PathBuf::from("."));
 50        let data_dir = std::env::var("FINANCE_DATA_DIR")
 51            .map(PathBuf::from)
 52            .unwrap_or_else(|_| root.join("data"));
 53        std::fs::create_dir_all(&data_dir)?;
 54
 55        let base_url = std::env::var("BASE_URL").unwrap_or_default();
 56        let site_title =
 57            std::env::var("FINANCE_TITLE").unwrap_or_else(|_| "Finance".to_string());
 58        let user_agent = std::env::var("FINANCE_USER_AGENT")
 59            .ok()
 60            .filter(|s| !s.is_empty())
 61            .unwrap_or_else(|| DEFAULT_USER_AGENT.to_string());
 62        let sec_contact_email = std::env::var("SEC_CONTACT_EMAIL").unwrap_or_default();
 63        let quote_provider = std::env::var("FINANCE_QUOTE_PROVIDER")
 64            .ok()
 65            .filter(|s| !s.is_empty())
 66            .unwrap_or_else(|| "yahoo".to_string());
 67
 68        let pool = db::init(&data_dir).await?;
 69
 70        let templates_dir = root.join("templates");
 71        let manifest_path = root.join("dist/.vite/manifest.json");
 72        let env = Arc::new(templates::build_env(&templates_dir, &manifest_path));
 73
 74        let config = Arc::new(Config {
 75            root,
 76            data_dir,
 77            base_url,
 78            site_title,
 79            user_agent,
 80            sec_contact_email,
 81            quote_provider,
 82        });
 83
 84        let hub = Arc::new(crate::stream::Hub::new());
 85
 86        Ok(Self {
 87            env,
 88            pool,
 89            config,
 90            hub,
 91        })
 92    }
 93}
 94
 95pub fn router(state: AppState) -> Router {
 96    let dist_dir = state.config.root.join("dist");
 97
 98    let static_cache = SetResponseHeaderLayer::if_not_present(
 99        axum::http::header::CACHE_CONTROL,
100        HeaderValue::from_static("public, max-age=31536000"),
101    );
102
103    Router::new()
104        .merge(routes::home::router())
105        .merge(routes::symbols::router())
106        .merge(routes::watchlist::router())
107        .merge(routes::search::router())
108        .merge(routes::stream::router())
109        .merge(routes::health::router())
110        .merge(routes::seo::router())
111        .nest_service(
112            "/static",
113            tower::ServiceBuilder::new()
114                .layer(static_cache)
115                .service(ServeDir::new(&dist_dir)),
116        )
117        .fallback(middleware::not_found)
118        .layer(axum_middleware::from_fn(middleware::log_requests))
119        // Compress HTML / JSON / static text on the fly (brotli or gzip per
120        // Accept-Encoding). tower-http's default predicate already skips
121        // `text/event-stream`, so `/stream` SSE frames still flush unbuffered.
122        .layer(CompressionLayer::new())
123        .with_state(state)
124}