diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-07-09 22:00:59 +0200 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-07-09 22:00:59 +0200 |
| commit | 620cf8fa33835c1ce1e56b1028ff6e2a6fbe0f39 (patch) | |
| tree | e97e54ff112c8e3fa9f88335bfd1275420a4d69f /crates/server/src | |
| parent | chore: Separate daemon, client, server, and lib into crates (diff) | |
| download | atuin-620cf8fa33835c1ce1e56b1028ff6e2a6fbe0f39.zip | |
chore: Add more things
Diffstat (limited to 'crates/server/src')
| -rw-r--r-- | crates/server/src/database/db/mod.rs | 18 | ||||
| -rw-r--r-- | crates/server/src/database/db/wrappers.rs | 4 | ||||
| -rw-r--r-- | crates/server/src/database/mod.rs | 18 | ||||
| -rw-r--r-- | crates/server/src/database/models.rs | 4 | ||||
| -rw-r--r-- | crates/server/src/handlers/mod.rs | 14 | ||||
| -rw-r--r-- | crates/server/src/handlers/v0/mod.rs | 2 | ||||
| -rw-r--r-- | crates/server/src/handlers/v0/record.rs | 10 | ||||
| -rw-r--r-- | crates/server/src/lib.rs | 12 | ||||
| -rw-r--r-- | crates/server/src/main.rs | 61 | ||||
| -rw-r--r-- | crates/server/src/metrics.rs | 4 | ||||
| -rw-r--r-- | crates/server/src/router.rs | 17 | ||||
| -rw-r--r-- | crates/server/src/settings.rs | 30 |
12 files changed, 126 insertions, 68 deletions
diff --git a/crates/server/src/database/db/mod.rs b/crates/server/src/database/db/mod.rs index 77bd0c61..c95a2ed4 100644 --- a/crates/server/src/database/db/mod.rs +++ b/crates/server/src/database/db/mod.rs @@ -2,11 +2,9 @@ use std::collections::HashMap; use rand::Rng; -use crate::{ - atuin_common::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus}, - atuin_server::database::{DbError, DbResult, DbSettings, models::User}, -}; +use crate::database::{DbError, DbResult, DbSettings, models::User}; use sqlx::postgres::PgPoolOptions; +use turtle_common::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus}; use tracing::instrument; use uuid::Uuid; @@ -17,7 +15,7 @@ mod wrappers; const MIN_PG_VERSION: u32 = 14; #[derive(Clone)] -pub(crate) struct ServerPostgres { +pub struct ServerPostgres { pool: sqlx::Pool<sqlx::postgres::Postgres>, /// Optional read replica pool for read-only queries read_pool: Option<sqlx::Pool<sqlx::postgres::Postgres>>, @@ -32,7 +30,7 @@ impl ServerPostgres { } impl ServerPostgres { - pub(crate) async fn new(settings: &DbSettings) -> DbResult<Self> { + pub async fn new(settings: &DbSettings) -> DbResult<Self> { let pool = PgPoolOptions::new() .max_connections(100) .connect(settings.db_uri.as_str()) @@ -93,7 +91,7 @@ impl ServerPostgres { } #[instrument(skip_all)] - pub(crate) async fn add_records( + pub async fn add_records( &self, user: &User, records: &[Record<EncryptedData>], @@ -110,7 +108,7 @@ impl ServerPostgres { let mut heads = HashMap::<(HostId, &str), u64>::new(); for i in records { - let id = crate::atuin_common::utils::uuid_v7(); + let id = turtle_common::utils::uuid_v7(); let result = sqlx::query( " @@ -169,7 +167,7 @@ impl ServerPostgres { } #[instrument(skip_all)] - pub(crate) async fn next_records( + pub async fn next_records( &self, user: &User, host: HostId, @@ -222,7 +220,7 @@ impl ServerPostgres { Ok(ret) } - pub(crate) async fn status(&self, user: &User) -> DbResult<RecordStatus> { + pub async fn status(&self, user: &User) -> DbResult<RecordStatus> { // If IDX_CACHE_ROLLOUT is set, then we // 1. Read the value of the var, use it as a % chance of using the cache // 2. If we use the cache, just read from the cache table diff --git a/crates/server/src/database/db/wrappers.rs b/crates/server/src/database/db/wrappers.rs index 0315e331..8054289a 100644 --- a/crates/server/src/database/db/wrappers.rs +++ b/crates/server/src/database/db/wrappers.rs @@ -1,7 +1,7 @@ -use crate::atuin_common::record::{EncryptedData, Host, Record}; +use turtle_common::record::{EncryptedData, Host, Record}; use sqlx::{Row, postgres::PgRow}; -pub(crate) struct DbRecord(pub Record<EncryptedData>); +pub struct DbRecord(pub Record<EncryptedData>); impl<'a> ::sqlx::FromRow<'a, PgRow> for DbRecord { fn from_row(row: &'a PgRow) -> ::sqlx::Result<Self> { diff --git a/crates/server/src/database/mod.rs b/crates/server/src/database/mod.rs index 43fe5c3b..c05fa783 100644 --- a/crates/server/src/database/mod.rs +++ b/crates/server/src/database/mod.rs @@ -1,12 +1,12 @@ -pub(crate) mod db; -pub(crate) mod models; +pub mod db; +pub mod models; use std::fmt::{Debug, Display}; use serde::{Deserialize, Serialize}; #[derive(Debug)] -pub(crate) enum DbError { +pub enum DbError { NotFound, Other(eyre::Report), } @@ -43,24 +43,24 @@ impl From<sqlx::Error> for DbError { impl std::error::Error for DbError {} -pub(crate) type DbResult<T> = Result<T, DbError>; +pub type DbResult<T> = Result<T, DbError>; #[derive(Debug, PartialEq)] -pub(crate) enum DbType { +pub enum DbType { Postgres, Unknown, } #[derive(Clone, Deserialize, Serialize)] -pub(crate) struct DbSettings { - pub(crate) db_uri: String, +pub struct DbSettings { + pub db_uri: String, /// Optional URI for read replicas. If set, read-only queries will use this connection. - pub(crate) read_db_uri: Option<String>, + pub read_db_uri: Option<String>, } impl DbSettings { - pub(crate) fn db_type(&self) -> DbType { + pub fn db_type(&self) -> DbType { if self.db_uri.starts_with("postgres://") || self.db_uri.starts_with("postgresql://") { DbType::Postgres } else { diff --git a/crates/server/src/database/models.rs b/crates/server/src/database/models.rs index 3fa6f471..9f6241ae 100644 --- a/crates/server/src/database/models.rs +++ b/crates/server/src/database/models.rs @@ -1,5 +1,5 @@ use uuid::Uuid; -pub(crate) struct User { - pub(crate) id: Uuid, +pub struct User { + pub id: Uuid, } diff --git a/crates/server/src/handlers/mod.rs b/crates/server/src/handlers/mod.rs index c4332f80..e24bad14 100644 --- a/crates/server/src/handlers/mod.rs +++ b/crates/server/src/handlers/mod.rs @@ -1,13 +1,13 @@ -use crate::atuin_common::api::{ErrorResponse, IndexResponse}; +use turtle_common::api::{ErrorResponse, IndexResponse}; use axum::{Json, extract::State, http, response::IntoResponse}; use crate::router::AppState; -pub(crate) mod v0; +pub mod v0; const VERSION: &str = env!("CARGO_PKG_VERSION"); -pub(crate) async fn index(state: State<AppState>) -> Json<IndexResponse> { +pub async fn index(state: State<AppState>) -> Json<IndexResponse> { let homage = r#""Through the fathomless deeps of space swims the star turtle Great A'Tuin, bearing on its back the four giant elephants who carry on their shoulders the mass of the Discworld." -- Sir Terry Pratchett"#; let version = state @@ -28,12 +28,12 @@ impl IntoResponse for ErrorResponseStatus<'_> { } } -pub(crate) struct ErrorResponseStatus<'a> { - pub(crate) error: ErrorResponse<'a>, - pub(crate) status: http::StatusCode, +pub struct ErrorResponseStatus<'a> { + pub error: ErrorResponse<'a>, + pub status: http::StatusCode, } -pub(crate) trait RespExt<'a> { +pub trait RespExt<'a> { fn with_status(self, status: http::StatusCode) -> ErrorResponseStatus<'a>; fn reply(reason: &'a str) -> Self; } diff --git a/crates/server/src/handlers/v0/mod.rs b/crates/server/src/handlers/v0/mod.rs index 78fb47b8..2066636c 100644 --- a/crates/server/src/handlers/v0/mod.rs +++ b/crates/server/src/handlers/v0/mod.rs @@ -1 +1 @@ -pub(crate) mod record; +pub mod record; diff --git a/crates/server/src/handlers/v0/record.rs b/crates/server/src/handlers/v0/record.rs index 0381ded8..f7758c1a 100644 --- a/crates/server/src/handlers/v0/record.rs +++ b/crates/server/src/handlers/v0/record.rs @@ -8,10 +8,10 @@ use crate::{ router::{AppState, UserAuth}, }; -use crate::atuin_common::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus}; +use turtle_common::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus}; #[instrument(skip_all, fields(user.id = user.id.to_string()))] -pub(crate) async fn post( +pub async fn post( UserAuth(user): UserAuth, state: State<AppState>, Json(records): Json<Vec<Record<EncryptedData>>>, @@ -50,7 +50,7 @@ pub(crate) async fn post( } #[instrument(skip_all, fields(user.id = user.id.to_string()))] -pub(crate) async fn index( +pub async fn index( UserAuth(user): UserAuth, state: State<AppState>, ) -> Result<Json<RecordStatus>, ErrorResponseStatus<'static>> { @@ -75,7 +75,7 @@ pub(crate) async fn index( } #[derive(Deserialize)] -pub(crate) struct NextParams { +pub struct NextParams { host: HostId, tag: String, start: Option<RecordIdx>, @@ -83,7 +83,7 @@ pub(crate) struct NextParams { } #[instrument(skip_all, fields(user.id = user.id.to_string()))] -pub(crate) async fn next( +pub async fn next( params: Query<NextParams>, UserAuth(user): UserAuth, state: State<AppState>, diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index a4b10acf..726afeea 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -5,14 +5,14 @@ use axum::{Router, serve}; use database::db::ServerPostgres; use eyre::{Context, Result}; -pub(crate) mod database; +pub mod database; mod handlers; mod metrics; mod router; -pub(crate) use settings::Settings; +pub use settings::Settings; -pub(crate) mod settings; +pub mod settings; use tokio::net::TcpListener; use tokio::signal; @@ -31,7 +31,7 @@ async fn shutdown_signal() { eprintln!("Shutting down gracefully..."); } -pub(crate) async fn launch(settings: Settings, addr: SocketAddr) -> Result<()> { +pub async fn launch(settings: Settings, addr: SocketAddr) -> Result<()> { launch_with_tcp_listener( settings, TcpListener::bind(addr) @@ -42,7 +42,7 @@ pub(crate) async fn launch(settings: Settings, addr: SocketAddr) -> Result<()> { .await } -pub(crate) async fn launch_with_tcp_listener( +pub async fn launch_with_tcp_listener( settings: Settings, listener: TcpListener, shutdown: impl Future<Output = ()> + Send + 'static, @@ -58,7 +58,7 @@ pub(crate) async fn launch_with_tcp_listener( // The separate listener means it's much easier to ensure metrics are not accidentally exposed to // the public. -pub(crate) async fn launch_metrics_server(host: String, port: u16) -> Result<()> { +pub async fn launch_metrics_server(host: String, port: u16) -> Result<()> { let listener = TcpListener::bind((host, port)) .await .context("failed to bind metrics tcp")?; diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs new file mode 100644 index 00000000..94d6d143 --- /dev/null +++ b/crates/server/src/main.rs @@ -0,0 +1,61 @@ +use std::net::SocketAddr; + +use turtle_server::{Settings, database::DbType, launch, launch_metrics_server}; + +use clap::Parser; +use eyre::{Context, Result, eyre}; + +#[derive(Parser, Clone, Debug)] +#[command(infer_subcommands = true)] +pub enum Cmd { + /// Start the server + Start { + /// The host address to bind + #[clap(long)] + host: Option<String>, + + /// The port to bind + #[clap(long, short)] + port: Option<u16>, + }, + + /// Print server example configuration + DefaultConfig, +} + +impl Cmd { + pub async fn run(self) -> Result<()> { + match self { + Self::Start { host, port } => { + let settings = Settings::new().wrap_err("could not load server settings")?; + let host = host.as_ref().unwrap_or(&settings.host).clone(); + let port = port.unwrap_or(settings.port); + let addr = SocketAddr::new(host.parse()?, port); + + if settings.metrics.enable { + tokio::spawn(launch_metrics_server( + settings.metrics.host.clone(), + settings.metrics.port, + )); + } + + match settings.db_settings.db_type() { + DbType::Postgres => launch(settings, addr).await, + DbType::Unknown => { + Err(eyre!("db_uri must start with postgres:// or sqlite://")) + } + } + } + Self::DefaultConfig => { + // TODO(@bpeetz): Add this back <2026-06-11> + println!("TODO"); + Ok(()) + } + } + } +} + +#[tokio::main] +async fn main() -> Result<()> { + Cmd::parse().run().await +} diff --git a/crates/server/src/metrics.rs b/crates/server/src/metrics.rs index 6380bef1..987af807 100644 --- a/crates/server/src/metrics.rs +++ b/crates/server/src/metrics.rs @@ -7,7 +7,7 @@ use axum::{ }; use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle}; -pub(crate) fn setup_metrics_recorder() -> PrometheusHandle { +pub fn setup_metrics_recorder() -> PrometheusHandle { const EXPONENTIAL_SECONDS: &[f64] = &[ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, ]; @@ -25,7 +25,7 @@ pub(crate) fn setup_metrics_recorder() -> PrometheusHandle { /// Middleware to record some common HTTP metrics /// Generic over B to allow for arbitrary body types (eg Vec<u8>, Streams, a deserialized thing, etc) /// Someday tower-http might provide a metrics middleware: <https://github.com/tower-rs/tower-http/issues/57> -pub(crate) async fn track_metrics(req: Request, next: Next) -> impl IntoResponse { +pub async fn track_metrics(req: Request, next: Next) -> impl IntoResponse { let start = Instant::now(); let path = req.extensions().get::<MatchedPath>().map_or_else( diff --git a/crates/server/src/router.rs b/crates/server/src/router.rs index 2a5c5f15..b0b98ecc 100644 --- a/crates/server/src/router.rs +++ b/crates/server/src/router.rs @@ -1,7 +1,6 @@ -use crate::{ - atuin_common::api::{ATUIN_CARGO_VERSION, ATUIN_HEADER_VERSION, ErrorResponse}, - atuin_server::database::{db::ServerPostgres, models::User}, -}; +use crate::database::{db::ServerPostgres, models::User}; +use turtle_common::api::{ATUIN_CARGO_VERSION, ATUIN_HEADER_VERSION, ErrorResponse}; + use axum::{ Router, extract::{FromRequestParts, Path, Request}, @@ -22,7 +21,7 @@ use crate::{ settings::Settings, }; -pub(crate) struct UserAuth(pub(crate) User); +pub struct UserAuth(pub User); impl FromRequestParts<AppState> for UserAuth { type Rejection = ErrorResponseStatus<'static>; @@ -66,12 +65,12 @@ async fn semver(request: Request, next: Next) -> Response { } #[derive(Clone)] -pub(crate) struct AppState { - pub(crate) database: ServerPostgres, - pub(crate) settings: Settings, +pub struct AppState { + pub database: ServerPostgres, + pub settings: Settings, } -pub(crate) fn router(database: ServerPostgres, settings: Settings) -> Router { +pub fn router(database: ServerPostgres, settings: Settings) -> Router { let routes = Router::new() .route("/", get(handlers::index)) .route("/api/v0/{user_id}/record", post(handlers::v0::record::post)) diff --git a/crates/server/src/settings.rs b/crates/server/src/settings.rs index 6a32fb9b..e6626b15 100644 --- a/crates/server/src/settings.rs +++ b/crates/server/src/settings.rs @@ -9,11 +9,11 @@ use tracing::info; use crate::database::DbSettings; #[derive(Clone, Debug, Deserialize, Serialize)] -pub(crate) struct Metrics { +pub struct Metrics { #[serde(alias = "enabled")] - pub(crate) enable: bool, - pub(crate) host: String, - pub(crate) port: u16, + pub enable: bool, + pub host: String, + pub port: u16, } impl Default for Metrics { @@ -27,29 +27,29 @@ impl Default for Metrics { } #[derive(Clone, Debug, Deserialize, Serialize)] -pub(crate) struct Settings { - pub(crate) host: String, - pub(crate) port: u16, - pub(crate) path: String, - pub(crate) max_history_length: usize, - pub(crate) max_record_size: usize, - pub(crate) page_size: i64, - pub(crate) metrics: Metrics, +pub struct Settings { + pub host: String, + pub port: u16, + pub path: String, + pub max_history_length: usize, + pub max_record_size: usize, + pub page_size: i64, + pub metrics: Metrics, /// Advertise a version that is not what we are _actually_ running /// Many clients compare their version with api.atuin.sh, and if they differ, notify the user /// that an update is available. /// Now that we take beta releases, we should be able to advertise a different version to avoid /// notifying users when the server runs something that is not a stable release. - pub(crate) fake_version: Option<String>, + pub fake_version: Option<String>, #[serde(flatten)] #[expect(clippy::struct_field_names)] - pub(crate) db_settings: DbSettings, + pub db_settings: DbSettings, } impl Settings { - pub(crate) fn new() -> Result<Self> { + pub fn new() -> Result<Self> { // create the config file if it does not exist let mut config_builder = Config::builder() .set_default("host", "127.0.0.1")? |
