#![deny(clippy::pedantic)]
extern crate core;
mod file_cache;
mod filesystem;
mod render;
mod templates;
mod utils;
mod webserver;
use crate::filesystem::FileSystem;
use crate::webserver::database::{FileCache, ParsedSqlFile};
use crate::webserver::Database;
use anyhow::Context;
use std::env;
use std::net::{SocketAddr, ToSocketAddrs};
use std::path::PathBuf;
use templates::AllTemplates;
const TEMPLATES_DIR: &str = "sqlpage/templates";
const MIGRATIONS_DIR: &str = "sqlpage/migrations";
const DEFAULT_DATABASE_FILE: &str = "sqlpage.db";
pub struct AppState {
db: Database,
all_templates: AllTemplates,
sql_file_cache: FileCache<ParsedSqlFile>,
file_system: FileSystem,
}
impl AppState {
async fn init() -> anyhow::Result<Self> {
let database_url = get_database_url();
let db = Database::init(&database_url).await?;
log::info!("Connecting to database: {database_url}");
let all_templates = AllTemplates::init()?;
let web_root = get_web_root();
let sql_file_cache = FileCache::new();
let file_system = FileSystem::init(&web_root, &db).await;
Ok(AppState {
db,
all_templates,
sql_file_cache,
file_system,
})
}
}
fn get_web_root() -> PathBuf {
env::var("WEB_ROOT").map_or_else(
|_| PathBuf::from(&std::path::Component::CurDir),
PathBuf::from,
)
}
pub struct Config {
listen_on: SocketAddr,
}
#[actix_web::main]
async fn main() {
init_logging();
if let Err(e) = start().await {
log::error!("{:?}", e);
std::process::exit(1);
}
}
async fn start() -> anyhow::Result<()> {
let state = AppState::init().await?;
webserver::apply_migrations(&state.db).await?;
let listen_on = get_listen_on()?;
log::info!("Starting server on {}", listen_on);
let config = Config { listen_on };
webserver::http::run_server(config, state).await?;
Ok(())
}
fn get_listen_on() -> anyhow::Result<SocketAddr> {
let host_str = env::var("LISTEN_ON").unwrap_or_else(|_| "0.0.0.0:8080".to_string());
let mut host_addr = host_str
.to_socket_addrs()?
.next()
.with_context(|| format!("host '{host_str}' does not resolve to an IP"))?;
if let Ok(port) = env::var("PORT") {
host_addr.set_port(port.parse().with_context(|| "Invalid PORT")?);
}
Ok(host_addr)
}
fn init_logging() {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
}
fn get_database_url() -> String {
env::var("DATABASE_URL").unwrap_or_else(|_| default_database_url())
}
fn default_database_url() -> String {
let prefix = "sqlite://".to_owned();
if cfg!(test) {
return prefix + ":memory:";
}
#[cfg(not(feature = "lambda-web"))]
if std::path::Path::new(DEFAULT_DATABASE_FILE).exists() {
log::info!(
"No DATABASE_URL, using the default sqlite database './{DEFAULT_DATABASE_FILE}'"
);
return prefix + DEFAULT_DATABASE_FILE;
} else if let Ok(tmp_file) = std::fs::File::create(DEFAULT_DATABASE_FILE) {
log::info!("No DATABASE_URL provided, the current directory is writeable, creating {DEFAULT_DATABASE_FILE}");
drop(tmp_file);
std::fs::remove_file(DEFAULT_DATABASE_FILE).expect("removing temp file");
return prefix + DEFAULT_DATABASE_FILE + "?mode=rwc";
}
log::warn!("No DATABASE_URL provided, and the current directory is not writeable. Using a temporary in-memory SQLite database. All the data created will be lost when this server shuts down.");
prefix + ":memory:"
}