mirror of
https://github.com/trailbaseio/trailbase.git
synced 2025-12-20 08:59:44 -06:00
There's a couple of reasons: * As for the rust bindings: they're sub-par to rusqlite, though rusqlite is amazing. Even libsql-server uses rusqlite over their own bindings. The bindings are missing features such as update hooks and the hard-coded execution model suffers from lock congestion. * We've fixed bugs (e.g. trivial null ptr accesses went unnoticed), raised issues, and tried to add missing functionality such as update hooks. It's unclear if the rust-bindings are a priority or covered by the principles laid out in the libsql manifesto. From the outside it looks like focus has shifted to https://github.com/penberg/limbo. * As for the C-libsql fork for SQLite itself, it's getting more and more outdated (2024-01-30 (3.45.1)) and it's unclear when and if the ideas from the manifesto will manifest. Looking forward this opens the door for TrailBase to: * Bundle more recent versions of SQLite * Implement more performant, better scaling execution models. * Implement realtime APIs for subscribing to data changes.
198 lines
5.5 KiB
Rust
198 lines
5.5 KiB
Rust
use log::*;
|
|
use std::path::PathBuf;
|
|
use thiserror::Error;
|
|
|
|
use crate::app_state::{build_objectstore, AppState, AppStateArgs};
|
|
use crate::auth::jwt::{JwtHelper, JwtHelperError};
|
|
use crate::config::load_or_init_config_textproto;
|
|
use crate::constants::USER_TABLE;
|
|
use crate::migrations::{apply_logs_migrations, apply_main_migrations};
|
|
use crate::rand::generate_random_string;
|
|
use crate::server::DataDir;
|
|
use crate::table_metadata::TableMetadataCache;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum InitError {
|
|
#[error("TRusqlite error: {0}")]
|
|
TokioRusqlite(#[from] tokio_rusqlite::Error),
|
|
#[error("Rusqlite error: {0}")]
|
|
Rusqlite(#[from] rusqlite::Error),
|
|
#[error("RusqliteFromSql error: {0}")]
|
|
FromSql(#[from] rusqlite::types::FromSqlError),
|
|
#[error("DB Migration error: {0}")]
|
|
Migration(#[from] refinery::Error),
|
|
#[error("IO error: {0}")]
|
|
IO(#[from] std::io::Error),
|
|
#[error("Config error: {0}")]
|
|
Config(#[from] crate::config::ConfigError),
|
|
#[error("JwtHelper error: {0}")]
|
|
JwtHelper(#[from] JwtHelperError),
|
|
#[error("CreateAdmin error: {0}")]
|
|
CreateAdmin(String),
|
|
#[error("Custom initializer error: {0}")]
|
|
CustomInit(String),
|
|
#[error("Table error: {0}")]
|
|
TableError(#[from] crate::table_metadata::TableLookupError),
|
|
#[error("Schema error: {0}")]
|
|
SchemaError(#[from] trailbase_sqlite::schema::SchemaError),
|
|
#[error("Script error: {0}")]
|
|
ScriptError(String),
|
|
#[error("ObjectStore error: {0}")]
|
|
ObjectStore(#[from] object_store::Error),
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct InitArgs {
|
|
pub dev: bool,
|
|
pub js_runtime_threads: Option<usize>,
|
|
}
|
|
|
|
pub async fn init_app_state(
|
|
data_dir: DataDir,
|
|
public_dir: Option<PathBuf>,
|
|
args: InitArgs,
|
|
) -> Result<(bool, AppState), InitError> {
|
|
// First create directory structure.
|
|
data_dir.ensure_directory_structure().await?;
|
|
|
|
// Then open or init new databases.
|
|
let logs_conn = {
|
|
let mut conn = init_logs_db(&data_dir)?;
|
|
apply_logs_migrations(&mut conn)?;
|
|
tokio_rusqlite::Connection::from_conn(conn).await?
|
|
};
|
|
|
|
// Open or init the main db. Note that we derive whether a new DB was initialized based on
|
|
// whether the V1 migration had to be applied. Should be fairly robust.
|
|
let (conn2, new_db) = {
|
|
let mut conn = trailbase_sqlite::connect_sqlite(Some(data_dir.main_db_path()), None)?;
|
|
let new_db = apply_main_migrations(&mut conn, Some(data_dir.migrations_path()))?;
|
|
|
|
(tokio_rusqlite::Connection::from_conn(conn).await?, new_db)
|
|
};
|
|
|
|
let table_metadata = TableMetadataCache::new(conn2.clone()).await?;
|
|
|
|
// Read config or write default one.
|
|
let config = load_or_init_config_textproto(&data_dir, &table_metadata).await?;
|
|
|
|
debug!("Initializing JSON schemas from config");
|
|
trailbase_sqlite::set_user_schemas(
|
|
config
|
|
.schemas
|
|
.iter()
|
|
.filter_map(|s| {
|
|
let Some(ref name) = s.name else {
|
|
warn!("Schema config entry missing name: {s:?}");
|
|
return None;
|
|
};
|
|
|
|
let Some(ref schema) = s.schema else {
|
|
warn!("Schema config entry missing schema: {s:?}");
|
|
return None;
|
|
};
|
|
|
|
let json = match serde_json::from_str(schema) {
|
|
Ok(json) => json,
|
|
Err(err) => {
|
|
error!("Invalid schema config entry for '{name}': {err}");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
return Some((name.clone(), json));
|
|
})
|
|
.collect(),
|
|
)?;
|
|
|
|
let jwt = JwtHelper::init_from_path(&data_dir).await?;
|
|
|
|
// Init geoip if present.
|
|
let geoip_db_path = data_dir.root().join("GeoLite2-Country.mmdb");
|
|
if let Err(err) = trailbase_sqlite::load_geoip_db(geoip_db_path.clone()) {
|
|
debug!("Failed to load maxmind geoip DB '{geoip_db_path:?}': {err}");
|
|
}
|
|
|
|
let object_store = build_objectstore(&data_dir, config.server.s3_storage_config.as_ref())?;
|
|
|
|
// Write out the latest .js/.d.ts runtime files.
|
|
#[cfg(feature = "v8")]
|
|
crate::js::write_js_runtime_files(&data_dir).await;
|
|
|
|
let app_state = AppState::new(AppStateArgs {
|
|
data_dir: data_dir.clone(),
|
|
public_dir,
|
|
dev: args.dev,
|
|
table_metadata,
|
|
config,
|
|
conn2,
|
|
logs_conn,
|
|
jwt,
|
|
object_store,
|
|
js_runtime_threads: args.js_runtime_threads,
|
|
});
|
|
|
|
if new_db {
|
|
let num_admins: i64 = crate::util::query_one_row(
|
|
app_state.user_conn(),
|
|
&format!("SELECT COUNT(*) FROM {USER_TABLE} WHERE admin = TRUE"),
|
|
(),
|
|
)
|
|
.await?
|
|
.get(0)?;
|
|
|
|
if num_admins == 0 {
|
|
let email = "admin@localhost".to_string();
|
|
let password = generate_random_string(20);
|
|
|
|
app_state
|
|
.user_conn()
|
|
.execute(
|
|
&format!(
|
|
r#"
|
|
INSERT INTO {USER_TABLE}
|
|
(email, password_hash, verified, admin)
|
|
VALUES
|
|
('{email}', (hash_password('{password}')), TRUE, TRUE);
|
|
INSERT INTO
|
|
"#
|
|
),
|
|
(),
|
|
)
|
|
.await?;
|
|
|
|
info!(
|
|
"{}",
|
|
indoc::formatdoc!(
|
|
r#"
|
|
Created new admin user:
|
|
email: '{email}'
|
|
password: '{password}'
|
|
"#
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
if cfg!(debug_assertions) {
|
|
let text_config = app_state.get_config().to_text()?;
|
|
debug!("Config: {text_config}");
|
|
}
|
|
|
|
return Ok((new_db, app_state));
|
|
}
|
|
|
|
fn init_logs_db(data_dir: &DataDir) -> Result<rusqlite::Connection, InitError> {
|
|
let conn = trailbase_sqlite::connect_sqlite(data_dir.logs_db_path().into(), None)?;
|
|
|
|
// Turn off secure_deletions, i.e. don't wipe the memory with zeros.
|
|
conn
|
|
.query_row("PRAGMA secure_delete = FALSE", (), |_row| Ok(()))
|
|
.unwrap();
|
|
|
|
// Sync less often
|
|
conn.execute("PRAGMA synchronous = 1", ()).unwrap();
|
|
|
|
return Ok(conn);
|
|
}
|