Skip to main content

atuin_server_sqlite/
lib.rs

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) // throwing away some data, but i64 is still big in terms of time
189            .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}