1use std::str::FromStr;
2
3use async_trait::async_trait;
4use atuin_common::record::{EncryptedData, HostId, Record, RecordIdx, RecordStatus};
5use atuin_server_database::{
6 Database, DbError, DbResult, DbSettings,
7 models::{NewSession, NewUser, Session, User},
8};
9use sqlx::{
10 sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions},
11 types::Uuid,
12};
13use tracing::instrument;
14use wrappers::{DbRecord, DbSession, DbUser};
15
16mod wrappers;
17
18#[derive(Clone)]
19pub struct Sqlite {
20 pool: sqlx::Pool<sqlx::sqlite::Sqlite>,
21}
22
23#[async_trait]
24impl Database for Sqlite {
25 async fn new(settings: &DbSettings) -> DbResult<Self> {
26 let opts = SqliteConnectOptions::from_str(&settings.db_uri)?
27 .journal_mode(SqliteJournalMode::Wal)
28 .create_if_missing(true);
29
30 let pool = SqlitePoolOptions::new().connect_with(opts).await?;
31
32 sqlx::migrate!("./migrations")
33 .run(&pool)
34 .await
35 .map_err(|error| DbError::Other(error.into()))?;
36
37 Ok(Self { pool })
38 }
39
40 #[instrument(skip_all)]
41 async fn get_session(&self, token: &str) -> DbResult<Session> {
42 sqlx::query_as("select id, user_id, token from sessions where token = $1")
43 .bind(token)
44 .fetch_one(&self.pool)
45 .await
46 .map_err(Into::into)
47 .map(|DbSession(session)| session)
48 }
49
50 #[instrument(skip_all)]
51 async fn get_session_user(&self, token: &str) -> DbResult<User> {
52 sqlx::query_as(
53 "select users.id, users.username, users.email, users.password from users
54 inner join sessions
55 on users.id = sessions.user_id
56 and sessions.token = $1",
57 )
58 .bind(token)
59 .fetch_one(&self.pool)
60 .await
61 .map_err(Into::into)
62 .map(|DbUser(user)| user)
63 }
64
65 #[instrument(skip_all)]
66 async fn add_session(&self, session: &NewSession) -> DbResult<()> {
67 let token: &str = &session.token;
68
69 sqlx::query(
70 "insert into sessions
71 (user_id, token)
72 values($1, $2)",
73 )
74 .bind(session.user_id)
75 .bind(token)
76 .execute(&self.pool)
77 .await?;
78
79 Ok(())
80 }
81
82 #[instrument(skip_all)]
83 async fn get_user(&self, username: &str) -> DbResult<User> {
84 sqlx::query_as("select id, username, email, password from users where username = $1")
85 .bind(username)
86 .fetch_one(&self.pool)
87 .await
88 .map_err(Into::into)
89 .map(|DbUser(user)| user)
90 }
91
92 #[instrument(skip_all)]
93 async fn get_user_session(&self, u: &User) -> DbResult<Session> {
94 sqlx::query_as("select id, user_id, token from sessions where user_id = $1")
95 .bind(u.id)
96 .fetch_one(&self.pool)
97 .await
98 .map_err(Into::into)
99 .map(|DbSession(session)| session)
100 }
101
102 #[instrument(skip_all)]
103 async fn add_user(&self, user: &NewUser) -> DbResult<i64> {
104 let email: &str = &user.email;
105 let username: &str = &user.username;
106 let password: &str = &user.password;
107
108 let res: (i64,) = sqlx::query_as(
109 "insert into users
110 (username, email, password)
111 values($1, $2, $3)
112 returning id",
113 )
114 .bind(username)
115 .bind(email)
116 .bind(password)
117 .fetch_one(&self.pool)
118 .await?;
119
120 Ok(res.0)
121 }
122
123 #[instrument(skip_all)]
124 async fn update_user_password(&self, user: &User) -> DbResult<()> {
125 sqlx::query(
126 "update users
127 set password = $1
128 where id = $2",
129 )
130 .bind(&user.password)
131 .bind(user.id)
132 .execute(&self.pool)
133 .await?;
134
135 Ok(())
136 }
137
138 #[instrument(skip_all)]
139 async fn delete_user(&self, u: &User) -> DbResult<()> {
140 sqlx::query("delete from sessions where user_id = $1")
141 .bind(u.id)
142 .execute(&self.pool)
143 .await?;
144
145 sqlx::query("delete from users where id = $1")
146 .bind(u.id)
147 .execute(&self.pool)
148 .await?;
149
150 sqlx::query("delete from history where user_id = $1")
151 .bind(u.id)
152 .execute(&self.pool)
153 .await?;
154
155 Ok(())
156 }
157
158 async fn delete_store(&self, user: &User) -> DbResult<()> {
159 sqlx::query(
160 "delete from store
161 where user_id = $1",
162 )
163 .bind(user.id)
164 .execute(&self.pool)
165 .await?;
166
167 Ok(())
168 }
169
170 #[instrument(skip_all)]
171 async fn add_records(&self, user: &User, records: &[Record<EncryptedData>]) -> DbResult<()> {
172 let mut tx = self.pool.begin().await?;
173
174 for i in records {
175 let id = atuin_common::utils::uuid_v7();
176
177 sqlx::query(
178 "insert into store
179 (id, client_id, host, idx, timestamp, version, tag, data, cek, user_id)
180 values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
181 on conflict do nothing
182 ",
183 )
184 .bind(id)
185 .bind(i.id)
186 .bind(i.host.id)
187 .bind(i.idx as i64)
188 .bind(i.timestamp as i64) .bind(&i.version)
190 .bind(&i.tag)
191 .bind(&i.data.data)
192 .bind(&i.data.content_encryption_key)
193 .bind(user.id)
194 .execute(&mut *tx)
195 .await?;
196 }
197
198 tx.commit().await?;
199
200 Ok(())
201 }
202
203 #[instrument(skip_all)]
204 async fn next_records(
205 &self,
206 user: &User,
207 host: HostId,
208 tag: String,
209 start: Option<RecordIdx>,
210 count: u64,
211 ) -> DbResult<Vec<Record<EncryptedData>>> {
212 tracing::debug!("{:?} - {:?} - {:?}", host, tag, start);
213 let start = start.unwrap_or(0);
214
215 let records: Result<Vec<DbRecord>, DbError> = sqlx::query_as(
216 "select client_id, host, idx, timestamp, version, tag, data, cek from store
217 where user_id = $1
218 and tag = $2
219 and host = $3
220 and idx >= $4
221 order by idx asc
222 limit $5",
223 )
224 .bind(user.id)
225 .bind(tag.clone())
226 .bind(host)
227 .bind(start as i64)
228 .bind(count as i64)
229 .fetch_all(&self.pool)
230 .await
231 .map_err(Into::into);
232
233 let ret = match records {
234 Ok(records) => {
235 let records: Vec<Record<EncryptedData>> = records
236 .into_iter()
237 .map(|f| {
238 let record: Record<EncryptedData> = f.into();
239 record
240 })
241 .collect();
242
243 records
244 }
245 Err(DbError::NotFound) => {
246 tracing::debug!("no records found in store: {:?}/{}", host, tag);
247 return Ok(vec![]);
248 }
249 Err(e) => return Err(e),
250 };
251
252 Ok(ret)
253 }
254
255 async fn status(&self, user: &User) -> DbResult<RecordStatus> {
256 const STATUS_SQL: &str =
257 "select host, tag, max(idx) from store where user_id = $1 group by host, tag";
258
259 let res: Vec<(Uuid, String, i64)> = sqlx::query_as(STATUS_SQL)
260 .bind(user.id)
261 .fetch_all(&self.pool)
262 .await?;
263
264 let mut status = RecordStatus::new();
265
266 for i in res {
267 status.set_raw(HostId(i.0), i.1, i.2 as u64);
268 }
269
270 Ok(status)
271 }
272}