aboutsummaryrefslogtreecommitdiffstats
path: root/crates/turtle/src/atuin_server/handlers
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-11 16:10:29 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-11 16:10:29 +0200
commit97f207b771b94c5285faae4810d6eeda1b78926b (patch)
tree4482544233c30e0e9a62be6afcfe92c8e01b0a50 /crates/turtle/src/atuin_server/handlers
parentchore: Remove all `pub`s (diff)
downloadatuin-97f207b771b94c5285faae4810d6eeda1b78926b.zip
chore(server): Simplify the database support
Diffstat (limited to 'crates/turtle/src/atuin_server/handlers')
-rw-r--r--crates/turtle/src/atuin_server/handlers/history.rs237
-rw-r--r--crates/turtle/src/atuin_server/handlers/mod.rs5
-rw-r--r--crates/turtle/src/atuin_server/handlers/status.rs45
-rw-r--r--crates/turtle/src/atuin_server/handlers/user.rs32
-rw-r--r--crates/turtle/src/atuin_server/handlers/v0/record.rs13
-rw-r--r--crates/turtle/src/atuin_server/handlers/v0/store.rs5
6 files changed, 26 insertions, 311 deletions
diff --git a/crates/turtle/src/atuin_server/handlers/history.rs b/crates/turtle/src/atuin_server/handlers/history.rs
deleted file mode 100644
index e5057bcb..00000000
--- a/crates/turtle/src/atuin_server/handlers/history.rs
+++ /dev/null
@@ -1,237 +0,0 @@
-use std::{collections::HashMap, convert::TryFrom};
-
-use axum::{
- Json,
- extract::{Path, Query, State},
- http::{HeaderMap, StatusCode},
-};
-use metrics::counter;
-use time::{Month, UtcOffset};
-use tracing::{debug, error, instrument};
-
-use super::{ErrorResponse, ErrorResponseStatus, RespExt};
-use crate::atuin_server::{
- router::{AppState, UserAuth},
- utils::client_version_min,
-};
-use crate::atuin_server_database::{
- Database,
- calendar::{TimePeriod, TimePeriodInfo},
- models::NewHistory,
-};
-
-use crate::atuin_common::api::*;
-
-#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn count<DB: Database>(
- UserAuth(user): UserAuth,
- state: State<AppState<DB>>,
-) -> Result<Json<CountResponse>, ErrorResponseStatus<'static>> {
- let db = &state.0.database;
- match db.count_history_cached(&user).await {
- // By default read out the cached value
- Ok(count) => Ok(Json(CountResponse { count })),
-
- // If that fails, fallback on a full COUNT. Cache is built on a POST
- // only
- Err(_) => match db.count_history(&user).await {
- Ok(count) => Ok(Json(CountResponse { count })),
- Err(_) => Err(ErrorResponse::reply("failed to query history count")
- .with_status(StatusCode::INTERNAL_SERVER_ERROR)),
- },
- }
-}
-
-#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn list<DB: Database>(
- req: Query<SyncHistoryRequest>,
- UserAuth(user): UserAuth,
- headers: HeaderMap,
- state: State<AppState<DB>>,
-) -> Result<Json<SyncHistoryResponse>, ErrorResponseStatus<'static>> {
- let db = &state.0.database;
-
- let agent = headers
- .get("user-agent")
- .map_or("", |v| v.to_str().unwrap_or(""));
-
- let variable_page_size = client_version_min(agent, ">=15.0.0").unwrap_or(false);
-
- let page_size = if variable_page_size {
- state.settings.page_size
- } else {
- 100
- };
-
- if req.sync_ts.unix_timestamp_nanos() < 0 || req.history_ts.unix_timestamp_nanos() < 0 {
- error!("client asked for history from < epoch 0");
- counter!("atuin_history_epoch_before_zero").increment(1);
-
- return Err(
- ErrorResponse::reply("asked for history from before epoch 0")
- .with_status(StatusCode::BAD_REQUEST),
- );
- }
-
- let history = db
- .list_history(&user, req.sync_ts, req.history_ts, &req.host, page_size)
- .await;
-
- if let Err(e) = history {
- error!("failed to load history: {}", e);
- return Err(ErrorResponse::reply("failed to load history")
- .with_status(StatusCode::INTERNAL_SERVER_ERROR));
- }
-
- let history: Vec<String> = history
- .unwrap()
- .iter()
- .map(|i| i.data.to_string())
- .collect();
-
- debug!(
- "loaded {} items of history for user {}",
- history.len(),
- user.id
- );
-
- counter!("atuin_history_returned").increment(history.len() as u64);
-
- Ok(Json(SyncHistoryResponse { history }))
-}
-
-#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn delete<DB: Database>(
- UserAuth(user): UserAuth,
- state: State<AppState<DB>>,
- Json(req): Json<DeleteHistoryRequest>,
-) -> Result<Json<MessageResponse>, ErrorResponseStatus<'static>> {
- let db = &state.0.database;
-
- // user_id is the ID of the history, as set by the user (the server has its own ID)
- let deleted = db.delete_history(&user, req.client_id).await;
-
- if let Err(e) = deleted {
- error!("failed to delete history: {}", e);
- return Err(ErrorResponse::reply("failed to delete history")
- .with_status(StatusCode::INTERNAL_SERVER_ERROR));
- }
-
- Ok(Json(MessageResponse {
- message: String::from("deleted OK"),
- }))
-}
-
-#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn add<DB: Database>(
- UserAuth(user): UserAuth,
- state: State<AppState<DB>>,
- Json(req): Json<Vec<AddHistoryRequest>>,
-) -> Result<(), ErrorResponseStatus<'static>> {
- let State(AppState { database, settings }) = state;
-
- debug!("request to add {} history items", req.len());
- counter!("atuin_history_uploaded").increment(req.len() as u64);
-
- let mut history: Vec<NewHistory> = req
- .into_iter()
- .map(|h| NewHistory {
- client_id: h.id,
- user_id: user.id,
- hostname: h.hostname,
- timestamp: h.timestamp,
- data: h.data,
- })
- .collect();
-
- history.retain(|h| {
- // keep if within limit, or limit is 0 (unlimited)
- let keep = h.data.len() <= settings.max_history_length || settings.max_history_length == 0;
-
- // Don't return an error here. We want to insert as much of the
- // history list as we can, so log the error and continue going.
- if !keep {
- counter!("atuin_history_too_long").increment(1);
-
- tracing::warn!(
- "history too long, got length {}, max {}",
- h.data.len(),
- settings.max_history_length
- );
- }
-
- keep
- });
-
- if let Err(e) = database.add_history(&history).await {
- error!("failed to add history: {}", e);
-
- return Err(ErrorResponse::reply("failed to add history")
- .with_status(StatusCode::INTERNAL_SERVER_ERROR));
- };
-
- Ok(())
-}
-
-#[derive(serde::Deserialize, Debug)]
-pub(crate) struct CalendarQuery {
- #[serde(default = "serde_calendar::zero")]
- year: i32,
- #[serde(default = "serde_calendar::one")]
- month: u8,
-
- #[serde(default = "serde_calendar::utc")]
- tz: UtcOffset,
-}
-
-mod serde_calendar {
- use time::UtcOffset;
-
- pub(crate) fn zero() -> i32 {
- 0
- }
-
- pub(crate) fn one() -> u8 {
- 1
- }
-
- pub(crate) fn utc() -> UtcOffset {
- UtcOffset::UTC
- }
-}
-
-#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn calendar<DB: Database>(
- Path(focus): Path<String>,
- Query(params): Query<CalendarQuery>,
- UserAuth(user): UserAuth,
- state: State<AppState<DB>>,
-) -> Result<Json<HashMap<u64, TimePeriodInfo>>, ErrorResponseStatus<'static>> {
- let focus = focus.as_str();
-
- let year = params.year;
- let month = Month::try_from(params.month).map_err(|e| ErrorResponseStatus {
- error: ErrorResponse {
- reason: e.to_string().into(),
- },
- status: StatusCode::BAD_REQUEST,
- })?;
-
- let period = match focus {
- "year" => TimePeriod::Year,
- "month" => TimePeriod::Month { year },
- "day" => TimePeriod::Day { year, month },
- _ => {
- return Err(ErrorResponse::reply("invalid focus: use year/month/day")
- .with_status(StatusCode::BAD_REQUEST));
- }
- };
-
- let db = &state.0.database;
- let focus = db.calendar(&user, period, params.tz).await.map_err(|_| {
- ErrorResponse::reply("failed to query calendar")
- .with_status(StatusCode::INTERNAL_SERVER_ERROR)
- })?;
-
- Ok(Json(focus))
-}
diff --git a/crates/turtle/src/atuin_server/handlers/mod.rs b/crates/turtle/src/atuin_server/handlers/mod.rs
index 322324c4..3b935834 100644
--- a/crates/turtle/src/atuin_server/handlers/mod.rs
+++ b/crates/turtle/src/atuin_server/handlers/mod.rs
@@ -1,19 +1,16 @@
use crate::atuin_common::api::{ErrorResponse, IndexResponse};
-use crate::atuin_server_database::Database;
use axum::{Json, extract::State, http, response::IntoResponse};
use crate::atuin_server::router::AppState;
pub(crate) mod health;
-pub(crate) mod history;
pub(crate) mod record;
-pub(crate) mod status;
pub(crate) mod user;
pub(crate) mod v0;
const VERSION: &str = env!("CARGO_PKG_VERSION");
-pub(crate) async fn index<DB: Database>(state: State<AppState<DB>>) -> Json<IndexResponse> {
+pub(crate) 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
diff --git a/crates/turtle/src/atuin_server/handlers/status.rs b/crates/turtle/src/atuin_server/handlers/status.rs
deleted file mode 100644
index 59be1e5c..00000000
--- a/crates/turtle/src/atuin_server/handlers/status.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-use axum::{Json, extract::State, http::StatusCode};
-use tracing::instrument;
-
-use super::{ErrorResponse, ErrorResponseStatus, RespExt};
-use crate::atuin_server::router::{AppState, UserAuth};
-use crate::atuin_server_database::Database;
-
-use crate::atuin_common::api::*;
-
-const VERSION: &str = env!("CARGO_PKG_VERSION");
-
-#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn status<DB: Database>(
- UserAuth(user): UserAuth,
- state: State<AppState<DB>>,
-) -> Result<Json<StatusResponse>, ErrorResponseStatus<'static>> {
- let db = &state.0.database;
-
- let deleted = db.deleted_history(&user).await.unwrap_or(vec![]);
-
- let count = match db.count_history_cached(&user).await {
- // By default read out the cached value
- Ok(count) => count,
-
- // If that fails, fallback on a full COUNT. Cache is built on a POST
- // only
- Err(_) => match db.count_history(&user).await {
- Ok(count) => count,
- Err(_) => {
- return Err(ErrorResponse::reply("failed to query history count")
- .with_status(StatusCode::INTERNAL_SERVER_ERROR));
- }
- },
- };
-
- tracing::debug!(user = user.username, "requested sync status");
-
- Ok(Json(StatusResponse {
- count,
- deleted,
- username: user.username,
- version: VERSION.to_string(),
- page_size: state.settings.page_size,
- }))
-}
diff --git a/crates/turtle/src/atuin_server/handlers/user.rs b/crates/turtle/src/atuin_server/handlers/user.rs
index 7708d43e..28cebfab 100644
--- a/crates/turtle/src/atuin_server/handlers/user.rs
+++ b/crates/turtle/src/atuin_server/handlers/user.rs
@@ -16,14 +16,16 @@ use metrics::counter;
use rand::rngs::OsRng;
use tracing::{debug, error, info, instrument};
-use crate::atuin_common::tls::ensure_crypto_provider;
+use crate::{
+ atuin_common::tls::ensure_crypto_provider,
+ atuin_server::database::{
+ DbError,
+ models::{NewSession, NewUser},
+ },
+};
use super::{ErrorResponse, ErrorResponseStatus, RespExt};
use crate::atuin_server::router::{AppState, UserAuth};
-use crate::atuin_server_database::{
- Database, DbError,
- models::{NewSession, NewUser},
-};
use reqwest::header::CONTENT_TYPE;
@@ -63,9 +65,9 @@ async fn send_register_hook(url: &str, username: String, registered: String) {
}
#[instrument(skip_all, fields(user.username = username.as_str()))]
-pub(crate) async fn get<DB: Database>(
+pub(crate) async fn get(
Path(username): Path<String>,
- state: State<AppState<DB>>,
+ state: State<AppState>,
) -> Result<Json<UserResponse>, ErrorResponseStatus<'static>> {
let db = &state.0.database;
let user = match db.get_user(username.as_ref()).await {
@@ -87,8 +89,8 @@ pub(crate) async fn get<DB: Database>(
}
#[instrument(skip_all)]
-pub(crate) async fn register<DB: Database>(
- state: State<AppState<DB>>,
+pub(crate) async fn register(
+ state: State<AppState>,
Json(register): Json<RegisterRequest>,
) -> Result<Json<RegisterResponse>, ErrorResponseStatus<'static>> {
if !state.settings.open_registration {
@@ -163,9 +165,9 @@ pub(crate) async fn register<DB: Database>(
}
#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn delete<DB: Database>(
+pub(crate) async fn delete(
UserAuth(user): UserAuth,
- state: State<AppState<DB>>,
+ state: State<AppState>,
) -> Result<Json<DeleteUserResponse>, ErrorResponseStatus<'static>> {
debug!("request to delete user {}", user.id);
@@ -183,9 +185,9 @@ pub(crate) async fn delete<DB: Database>(
}
#[instrument(skip_all, fields(user.id = user.id, change_password))]
-pub(crate) async fn change_password<DB: Database>(
+pub(crate) async fn change_password(
UserAuth(mut user): UserAuth,
- state: State<AppState<DB>>,
+ state: State<AppState>,
Json(change_password): Json<ChangePasswordRequest>,
) -> Result<Json<ChangePasswordResponse>, ErrorResponseStatus<'static>> {
let db = &state.0.database;
@@ -213,8 +215,8 @@ pub(crate) async fn change_password<DB: Database>(
}
#[instrument(skip_all, fields(user.username = login.username.as_str()))]
-pub(crate) async fn login<DB: Database>(
- state: State<AppState<DB>>,
+pub(crate) async fn login(
+ state: State<AppState>,
login: Json<LoginRequest>,
) -> Result<Json<LoginResponse>, ErrorResponseStatus<'static>> {
let db = &state.0.database;
diff --git a/crates/turtle/src/atuin_server/handlers/v0/record.rs b/crates/turtle/src/atuin_server/handlers/v0/record.rs
index 2cc09118..88027547 100644
--- a/crates/turtle/src/atuin_server/handlers/v0/record.rs
+++ b/crates/turtle/src/atuin_server/handlers/v0/record.rs
@@ -7,14 +7,13 @@ use crate::atuin_server::{
handlers::{ErrorResponse, ErrorResponseStatus, RespExt},
router::{AppState, UserAuth},
};
-use crate::atuin_server_database::Database;
use crate::atuin_common::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus};
#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn post<DB: Database>(
+pub(crate) async fn post(
UserAuth(user): UserAuth,
- state: State<AppState<DB>>,
+ state: State<AppState>,
Json(records): Json<Vec<Record<EncryptedData>>>,
) -> Result<(), ErrorResponseStatus<'static>> {
let State(AppState { database, settings }) = state;
@@ -51,9 +50,9 @@ pub(crate) async fn post<DB: Database>(
}
#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn index<DB: Database>(
+pub(crate) async fn index(
UserAuth(user): UserAuth,
- state: State<AppState<DB>>,
+ state: State<AppState>,
) -> Result<Json<RecordStatus>, ErrorResponseStatus<'static>> {
let State(AppState {
database,
@@ -84,10 +83,10 @@ pub(crate) struct NextParams {
}
#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn next<DB: Database>(
+pub(crate) async fn next(
params: Query<NextParams>,
UserAuth(user): UserAuth,
- state: State<AppState<DB>>,
+ state: State<AppState>,
) -> Result<Json<Vec<Record<EncryptedData>>>, ErrorResponseStatus<'static>> {
let State(AppState {
database,
diff --git a/crates/turtle/src/atuin_server/handlers/v0/store.rs b/crates/turtle/src/atuin_server/handlers/v0/store.rs
index 8269d6b3..f0aa1b36 100644
--- a/crates/turtle/src/atuin_server/handlers/v0/store.rs
+++ b/crates/turtle/src/atuin_server/handlers/v0/store.rs
@@ -7,16 +7,15 @@ use crate::atuin_server::{
handlers::{ErrorResponse, ErrorResponseStatus, RespExt},
router::{AppState, UserAuth},
};
-use crate::atuin_server_database::Database;
#[derive(Deserialize)]
pub(crate) struct DeleteParams {}
#[instrument(skip_all, fields(user.id = user.id))]
-pub(crate) async fn delete<DB: Database>(
+pub(crate) async fn delete(
_params: Query<DeleteParams>,
UserAuth(user): UserAuth,
- state: State<AppState<DB>>,
+ state: State<AppState>,
) -> Result<(), ErrorResponseStatus<'static>> {
let State(AppState {
database,