migrate to sqlite

NOTE: extension loading crashes docker (for some reason)
This commit is contained in:
2025-09-07 15:09:14 +02:00
parent 5c3057e998
commit a1c9832515
32 changed files with 536 additions and 256 deletions

1
backend/Cargo.lock generated
View File

@@ -1235,6 +1235,7 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]

View File

@@ -13,7 +13,7 @@ path = "src/bin/backend/main.rs"
[dependencies]
anyhow = "1.0.98"
argon2 = "0.5.3"
argon2 = { version = "0.5.3", features = ["std"] }
axum = { version = "0.8.4", features = ["multipart", "query"] }
axum-extra = { version = "0.10.1", features = ["cookie", "multipart"] }
base64 = "0.22.1"
@@ -29,13 +29,7 @@ regex = "1.11.1"
rustix = { version = "1.0.8", features = ["fs"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
sqlx = { version = "0.8.6", features = [
"chrono",
"postgres",
"runtime-tokio",
"time",
"uuid",
] }
sqlx = { version = "0.8.6", features = ["chrono", "runtime-tokio", "sqlite", "time", "uuid"] }
thiserror = "2.0.12"
tokio = { version = "1.46.1", features = ["full"] }
tokio-stream = "0.1.17"

View File

@@ -1,7 +0,0 @@
CREATE TABLE warrens (
id UUID PRIMARY KEY DEFAULT GEN_RANDOM_UUID(),
path VARCHAR NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_warrens_path ON warrens(path);

View File

@@ -1 +0,0 @@
ALTER TABLE warrens ADD COLUMN name VARCHAR NOT NULL;

View File

@@ -1,9 +0,0 @@
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT GEN_RANDOM_UUID(),
name VARCHAR NOT NULL,
email VARCHAR NOT NULL,
hash VARCHAR NOT NULL,
admin BOOLEAN NOT NULL DEFAULT FALSE,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View File

@@ -1 +0,0 @@
ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email);

View File

@@ -1,6 +0,0 @@
CREATE TABLE auth_sessions (
session_id VARCHAR NOT NULL PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View File

@@ -1,11 +0,0 @@
CREATE TABLE user_warrens (
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
warren_id UUID NOT NULL REFERENCES warrens(id) ON DELETE CASCADE,
can_create_children BOOLEAN NOT NULL,
can_list_files BOOLEAN NOT NULL,
can_read_files BOOLEAN NOT NULL,
can_modify_files BOOLEAN NOT NULL,
can_delete_files BOOLEAN NOT NULL,
can_delete_warren BOOLEAN NOT NULL,
PRIMARY KEY(user_id, warren_id)
);

View File

@@ -1 +0,0 @@
ALTER TABLE user_warrens DROP COLUMN can_create_children, DROP COLUMN can_delete_warren;

View File

@@ -1,2 +0,0 @@
ALTER TABLE users ALTER COLUMN hash DROP NOT NULL;
ALTER TABLE users ADD COLUMN oidc_sub VARCHAR UNIQUE;

View File

@@ -1,9 +0,0 @@
CREATE TABLE shares (
id UUID PRIMARY KEY DEFAULT GEN_RANDOM_UUID(),
creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
warren_id UUID NOT NULL REFERENCES warrens(id) ON DELETE CASCADE,
path VARCHAR NOT NULL,
password_hash VARCHAR NOT NULL,
expires_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View File

@@ -1,21 +0,0 @@
ALTER TABLE
user_warrens
ADD COLUMN
can_list_shares BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN
can_create_shares BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN
can_modify_shares BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN
can_delete_shares BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE
user_warrens
ALTER COLUMN
can_list_shares DROP DEFAULT,
ALTER COLUMN
can_create_shares DROP DEFAULT,
ALTER COLUMN
can_modify_shares DROP DEFAULT,
ALTER COLUMN
can_delete_shares DROP DEFAULT;

View File

@@ -1 +0,0 @@
ALTER TABLE shares ALTER COLUMN password_hash DROP NOT NULL;

View File

@@ -1 +0,0 @@
CREATE INDEX idx_shares_path ON shares(path);

View File

@@ -0,0 +1,50 @@
CREATE TABLE users (
id BLOB NOT NULL PRIMARY KEY DEFAULT (uuid_blob(uuid())),
oidc_sub TEXT UNIQUE,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
admin BOOLEAN NOT NULL DEFAULT FALSE,
hash TEXT,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE warrens (
id BLOB NOT NULL PRIMARY KEY DEFAULT (uuid_blob(uuid())),
name TEXT NOT NULL,
path TEXT NOT NULL UNIQUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE user_warrens (
user_id BLOB NOT NULL REFERENCES users(id) ON DELETE CASCADE,
warren_id BLOB NOT NULL REFERENCES warrens(id) ON DELETE CASCADE,
can_list_files BOOLEAN NOT NULL,
can_read_files BOOLEAN NOT NULL,
can_modify_files BOOLEAN NOT NULL,
can_delete_files BOOLEAN NOT NULL,
can_list_shares BOOLEAN NOT NULL,
can_create_shares BOOLEAN NOT NULL,
can_modify_shares BOOLEAN NOT NULL,
can_delete_shares BOOLEAN NOT NULL,
PRIMARY KEY(user_id, warren_id)
);
CREATE TABLE shares (
id BLOB NOT NULL PRIMARY KEY DEFAULT (uuid_blob(uuid())),
creator_id BLOB NOT NULL REFERENCES users(id) ON DELETE CASCADE,
warren_id BLOB NOT NULL REFERENCES warrens(id) ON DELETE CASCADE,
path TEXT NOT NULL,
password_hash TEXT,
expires_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_shares_path ON shares(path);
CREATE TABLE auth_sessions (
session_id TEXT NOT NULL PRIMARY KEY,
user_id BLOB NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

BIN
backend/sqlite_extensions/uuid Executable file

Binary file not shown.

View File

@@ -0,0 +1,231 @@
/*
** 2019-10-23
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
**
** May you do good and not evil.
** May you find forgiveness for yourself and forgive others.
** May you share freely, never taking more than you give.
**
******************************************************************************
**
** This SQLite extension implements functions that handling RFC-4122 UUIDs
** Three SQL functions are implemented:
**
** uuid() - generate a version 4 UUID as a string
** uuid_str(X) - convert a UUID X into a well-formed UUID string
** uuid_blob(X) - convert a UUID X into a 16-byte blob
**
** The output from uuid() and uuid_str(X) are always well-formed RFC-4122
** UUID strings in this format:
**
** xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
**
** All of the 'x', 'M', and 'N' values are lower-case hexadecimal digits.
** The M digit indicates the "version". For uuid()-generated UUIDs, the
** version is always "4" (a random UUID). The upper three bits of N digit
** are the "variant". This library only supports variant 1 (indicated
** by values of N between '8' and 'b') as those are overwhelming the most
** common. Other variants are for legacy compatibility only.
**
** The output of uuid_blob(X) is always a 16-byte blob. The UUID input
** string is converted in network byte order (big-endian) in accordance
** with RFC-4122 specifications for variant-1 UUIDs. Note that network
** byte order is *always* used, even if the input self-identifies as a
** variant-2 UUID.
**
** The input X to the uuid_str() and uuid_blob() functions can be either
** a string or a BLOB. If it is a BLOB it must be exactly 16 bytes in
** length or else a NULL is returned. If the input is a string it must
** consist of 32 hexadecimal digits, upper or lower case, optionally
** surrounded by {...} and with optional "-" characters interposed in the
** middle. The flexibility of input is inspired by the PostgreSQL
** implementation of UUID functions that accept in all of the following
** formats:
**
** A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11
** {a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}
** a0eebc999c0b4ef8bb6d6bb9bd380a11
** a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11
** {a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}
**
** If any of the above inputs are passed into uuid_str(), the output will
** always be in the canonical RFC-4122 format:
**
** a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11
**
** If the X input string has too few or too many digits or contains
** stray characters other than {, }, or -, then NULL is returned.
*/
#include "sqlite3ext.h"
SQLITE_EXTENSION_INIT1
#include <assert.h>
#include <ctype.h>
#include <string.h>
#if !defined(SQLITE_ASCII) && !defined(SQLITE_EBCDIC)
#define SQLITE_ASCII 1
#endif
/*
** Translate a single byte of Hex into an integer.
** This routine only works if h really is a valid hexadecimal
** character: 0..9a..fA..F
*/
static unsigned char sqlite3UuidHexToInt(int h) {
assert((h >= '0' && h <= '9') || (h >= 'a' && h <= 'f') ||
(h >= 'A' && h <= 'F'));
#ifdef SQLITE_ASCII
h += 9 * (1 & (h >> 6));
#endif
#ifdef SQLITE_EBCDIC
h += 9 * (1 & ~(h >> 4));
#endif
return (unsigned char)(h & 0xf);
}
/*
** Convert a 16-byte BLOB into a well-formed RFC-4122 UUID. The output
** buffer zStr should be at least 37 bytes in length. The output will
** be zero-terminated.
*/
static void sqlite3UuidBlobToStr(const unsigned char *aBlob, /* Input blob */
unsigned char *zStr /* Write the answer here */
) {
static const char zDigits[] = "0123456789abcdef";
int i, k;
unsigned char x;
k = 0;
for (i = 0, k = 0x550; i < 16; i++, k = k >> 1) {
if (k & 1) {
zStr[0] = '-';
zStr++;
}
x = aBlob[i];
zStr[0] = zDigits[x >> 4];
zStr[1] = zDigits[x & 0xf];
zStr += 2;
}
*zStr = 0;
}
/*
** Attempt to parse a zero-terminated input string zStr into a binary
** UUID. Return 0 on success, or non-zero if the input string is not
** parsable.
*/
static int sqlite3UuidStrToBlob(const unsigned char *zStr, /* Input string */
unsigned char *aBlob /* Write results here */
) {
int i;
if (zStr[0] == '{')
zStr++;
for (i = 0; i < 16; i++) {
if (zStr[0] == '-')
zStr++;
if (isxdigit(zStr[0]) && isxdigit(zStr[1])) {
aBlob[i] = (sqlite3UuidHexToInt(zStr[0]) << 4) +
sqlite3UuidHexToInt(zStr[1]);
zStr += 2;
} else {
return 1;
}
}
if (zStr[0] == '}')
zStr++;
return zStr[0] != 0;
}
/*
** Render sqlite3_value pIn as a 16-byte UUID blob. Return a pointer
** to the blob, or NULL if the input is not well-formed.
*/
static const unsigned char *
sqlite3UuidInputToBlob(sqlite3_value *pIn, /* Input text */
unsigned char *pBuf /* output buffer */
) {
switch (sqlite3_value_type(pIn)) {
case SQLITE_TEXT: {
const unsigned char *z = sqlite3_value_text(pIn);
if (sqlite3UuidStrToBlob(z, pBuf))
return 0;
return pBuf;
}
case SQLITE_BLOB: {
int n = sqlite3_value_bytes(pIn);
return n == 16 ? sqlite3_value_blob(pIn) : 0;
}
default: {
return 0;
}
}
}
/* Implementation of uuid() */
static void sqlite3UuidFunc(sqlite3_context *context, int argc,
sqlite3_value **argv) {
unsigned char aBlob[16];
unsigned char zStr[37];
(void)argc;
(void)argv;
sqlite3_randomness(16, aBlob);
aBlob[6] = (aBlob[6] & 0x0f) + 0x40;
aBlob[8] = (aBlob[8] & 0x3f) + 0x80;
sqlite3UuidBlobToStr(aBlob, zStr);
sqlite3_result_text(context, (char *)zStr, 36, SQLITE_TRANSIENT);
}
/* Implementation of uuid_str() */
static void sqlite3UuidStrFunc(sqlite3_context *context, int argc,
sqlite3_value **argv) {
unsigned char aBlob[16];
unsigned char zStr[37];
const unsigned char *pBlob;
(void)argc;
pBlob = sqlite3UuidInputToBlob(argv[0], aBlob);
if (pBlob == 0)
return;
sqlite3UuidBlobToStr(pBlob, zStr);
sqlite3_result_text(context, (char *)zStr, 36, SQLITE_TRANSIENT);
}
/* Implementation of uuid_blob() */
static void sqlite3UuidBlobFunc(sqlite3_context *context, int argc,
sqlite3_value **argv) {
unsigned char aBlob[16];
const unsigned char *pBlob;
(void)argc;
pBlob = sqlite3UuidInputToBlob(argv[0], aBlob);
if (pBlob == 0)
return;
sqlite3_result_blob(context, pBlob, 16, SQLITE_TRANSIENT);
}
#ifdef _WIN32
__declspec(dllexport)
#endif
int sqlite3_uuid_init(
sqlite3 *db,
char **pzErrMsg,
const sqlite3_api_routines *pApi
){
int rc = SQLITE_OK;
SQLITE_EXTENSION_INIT2(pApi);
(void)pzErrMsg; /* Unused parameter */
rc = sqlite3_create_function(db, "uuid", 0, SQLITE_UTF8 | SQLITE_INNOCUOUS,
0, sqlite3UuidFunc, 0, 0);
if (rc == SQLITE_OK) {
rc = sqlite3_create_function(db, "uuid_str", 1,
SQLITE_UTF8 | SQLITE_INNOCUOUS |
SQLITE_DETERMINISTIC,
0, sqlite3UuidStrFunc, 0, 0);
}
if (rc == SQLITE_OK) {
rc = sqlite3_create_function(db, "uuid_blob", 1,
SQLITE_UTF8 | SQLITE_INNOCUOUS |
SQLITE_DETERMINISTIC,
0, sqlite3UuidBlobFunc, 0, 0);
}
return rc;
}

View File

@@ -7,7 +7,7 @@ use warren::{
metrics_debug_logger::MetricsDebugLogger,
notifier_debug_logger::NotifierDebugLogger,
oidc::{Oidc, OidcConfig},
postgres::{Postgres, PostgresConfig},
sqlite::{Sqlite, SqliteConfig},
},
};
@@ -25,16 +25,15 @@ async fn main() -> anyhow::Result<()> {
let metrics = MetricsDebugLogger::new();
let notifier = NotifierDebugLogger::new();
let postgres_config =
PostgresConfig::new(config.database_url.clone(), config.database_name.clone());
let postgres = Postgres::new(postgres_config).await?;
let sqlite_config = SqliteConfig::new(config.database_url.clone());
let sqlite = Sqlite::new(sqlite_config).await?;
let fs_config = FileSystemConfig::from_env(config.serve_dir.clone())?;
let fs = FileSystem::new(fs_config)?;
let fs_service = domain::warren::service::file_system::Service::new(fs, metrics, notifier);
let warren_service = domain::warren::service::warren::Service::new(
postgres.clone(),
sqlite.clone(),
metrics,
notifier,
fs_service.clone(),
@@ -48,7 +47,7 @@ async fn main() -> anyhow::Result<()> {
};
let auth_service = domain::warren::service::auth::Service::new(
postgres,
sqlite,
metrics,
notifier,
config.auth,

View File

@@ -6,7 +6,6 @@ use tracing::level_filters::LevelFilter;
use crate::domain::warren::service::auth::AuthConfig;
const DATABASE_URL_KEY: &str = "DATABASE_URL";
const DATABASE_NAME_KEY: &str = "DATABASE_NAME";
const SERVER_ADDRESS_KEY: &str = "SERVER_ADDRESS";
const SERVER_PORT_KEY: &str = "SERVER_PORT";
@@ -28,7 +27,6 @@ pub struct Config {
pub static_frontend_dir: Option<String>,
pub database_url: String,
pub database_name: String,
pub log_level: LevelFilter,
@@ -45,7 +43,6 @@ impl Config {
let static_frontend_dir = Self::load_env(STATIC_FRONTEND_DIRECTORY).ok();
let database_url = Self::load_env(DATABASE_URL_KEY)?;
let database_name = Self::load_env(DATABASE_NAME_KEY)?;
let log_level =
LevelFilter::from_str(&Self::load_env(LOG_LEVEL_KEY).unwrap_or("INFO".to_string()))
@@ -62,7 +59,6 @@ impl Config {
static_frontend_dir,
database_url,
database_name,
log_level,

View File

@@ -2,4 +2,4 @@ pub mod file_system;
pub mod metrics_debug_logger;
pub mod notifier_debug_logger;
pub mod oidc;
pub mod postgres;
pub mod sqlite;

View File

@@ -1,95 +0,0 @@
use std::{str::FromStr as _, time::Duration};
use anyhow::Context as _;
use sqlx::{
ConnectOptions as _, Connection as _, PgConnection, PgPool,
postgres::{PgConnectOptions, PgPoolOptions},
};
use tokio::task::JoinHandle;
pub mod auth;
pub mod share;
pub mod warrens;
#[derive(Debug, Clone)]
pub struct PostgresConfig {
database_url: String,
database_name: String,
}
impl PostgresConfig {
pub fn new(database_url: String, database_name: String) -> Self {
Self {
database_url,
database_name,
}
}
}
#[derive(Debug, Clone)]
pub struct Postgres {
pool: PgPool,
}
impl Postgres {
pub async fn new(config: PostgresConfig) -> anyhow::Result<Self> {
let opts = PgConnectOptions::from_str(&config.database_url)?.disable_statement_logging();
let mut connection = PgConnection::connect_with(&opts)
.await
.context("Failed to connect to the PostgreSQL database")?;
match sqlx::query("SELECT datname FROM pg_database WHERE datname = $1")
.bind(&config.database_name)
.fetch_one(&mut connection)
.await
{
Ok(_) => (),
Err(sqlx::Error::RowNotFound) => {
sqlx::query(&format!("CREATE DATABASE {}", config.database_name))
.execute(&mut connection)
.await?;
}
Err(e) => return Err(e.into()),
};
connection.close().await?;
let pool = PgPoolOptions::new()
.connect_with(opts.database(&config.database_name))
.await?;
sqlx::migrate!("./migrations").run(&pool).await?;
// 3600 seconds = 1 hour
Self::start_cleanup_tasks(pool.clone(), Duration::from_secs(3600));
Ok(Self { pool })
}
pub(super) fn start_cleanup_tasks(pool: PgPool, interval: Duration) -> JoinHandle<()> {
tokio::spawn(async move {
loop {
{
let Ok(mut connection) = pool.acquire().await else {
break;
};
if let Ok(count) = Self::delete_expired_auth_sessions(&mut connection).await {
tracing::debug!("Removed {count} expired auth session(s)");
}
if let Ok(count) = Self::delete_expired_shares(&mut connection).await {
tracing::debug!("Deleted {count} expired share(s)");
}
}
tokio::time::sleep(interval).await;
}
tracing::debug!("Session cleanup task stopped");
})
}
}
pub(super) fn is_not_found_error(err: &sqlx::Error) -> bool {
matches!(err, sqlx::Error::RowNotFound)
}

View File

@@ -7,7 +7,7 @@ use argon2::{
},
};
use chrono::Utc;
use sqlx::{Acquire as _, PgConnection};
use sqlx::{Acquire as _, SqliteConnection};
use uuid::Uuid;
use crate::domain::warren::{
@@ -40,9 +40,9 @@ use crate::domain::warren::{
ports::{AuthRepository, WarrenService},
};
use super::{Postgres, is_not_found_error};
use super::{Sqlite, is_not_found_error};
impl AuthRepository for Postgres {
impl AuthRepository for Sqlite {
async fn create_user(&self, request: CreateUserRequest) -> Result<User, CreateUserError> {
let mut connection = self
.pool
@@ -368,9 +368,9 @@ impl AuthRepository for Postgres {
}
}
impl Postgres {
impl Sqlite {
pub(super) async fn delete_expired_auth_sessions(
connection: &mut PgConnection,
connection: &mut SqliteConnection,
) -> Result<u64, sqlx::Error> {
let delete_count = sqlx::query(
"
@@ -389,7 +389,7 @@ impl Postgres {
async fn create_user(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
name: &UserName,
email: &UserEmail,
password: &UserPassword,
@@ -431,7 +431,7 @@ impl Postgres {
async fn create_or_update_user(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
sub: &String,
name: &UserName,
email: &UserEmail,
@@ -546,7 +546,7 @@ impl Postgres {
async fn edit_user(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
id: &Uuid,
name: &UserName,
email: &UserEmail,
@@ -592,7 +592,7 @@ impl Postgres {
async fn delete_user_sessions(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
user_id: &Uuid,
) -> Result<u64, sqlx::Error> {
let rows_affected = sqlx::query(
@@ -613,7 +613,7 @@ impl Postgres {
async fn delete_user_from_database(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
user_id: &Uuid,
) -> Result<User, sqlx::Error> {
let user: User = sqlx::query_as(
@@ -635,7 +635,7 @@ impl Postgres {
async fn get_user_from_id(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
id: &Uuid,
) -> Result<User, sqlx::Error> {
let user: User = sqlx::query_as(
@@ -657,7 +657,7 @@ impl Postgres {
async fn get_user_from_email(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
email: &UserEmail,
) -> Result<User, sqlx::Error> {
let user: User = sqlx::query_as(
@@ -698,7 +698,7 @@ impl Postgres {
async fn create_session(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
user: &User,
expiration: &SessionExpirationTime,
) -> anyhow::Result<AuthSession> {
@@ -721,7 +721,7 @@ impl Postgres {
) VALUES (
$1,
$2,
TO_TIMESTAMP($3::double precision / 1000)
datetime($3, 'unixepoch')
)
RETURNING
*
@@ -729,7 +729,7 @@ impl Postgres {
)
.bind(session_id)
.bind(user.id())
.bind(expiration_time)
.bind(expiration_time / 1000)
.fetch_one(&mut *tx)
.await?;
@@ -740,7 +740,7 @@ impl Postgres {
async fn get_auth_session(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
session_id: &AuthSessionId,
) -> Result<AuthSession, sqlx::Error> {
let session: AuthSession = sqlx::query_as(
@@ -762,7 +762,7 @@ impl Postgres {
async fn get_user_warrens(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
user_id: &Uuid,
) -> Result<Vec<UserWarren>, sqlx::Error> {
let user_warrens: Vec<UserWarren> = sqlx::query_as(
@@ -784,7 +784,7 @@ impl Postgres {
async fn get_all_user_warrens(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
) -> Result<Vec<UserWarren>, sqlx::Error> {
let user_warrens: Vec<UserWarren> = sqlx::query_as(
"
@@ -802,7 +802,7 @@ impl Postgres {
async fn get_user_warren(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
user_id: &Uuid,
warren_id: &Uuid,
) -> Result<UserWarren, sqlx::Error> {
@@ -825,7 +825,10 @@ impl Postgres {
Ok(ids)
}
async fn fetch_users(&self, connection: &mut PgConnection) -> Result<Vec<User>, sqlx::Error> {
async fn fetch_users(
&self,
connection: &mut SqliteConnection,
) -> Result<Vec<User>, sqlx::Error> {
let users: Vec<User> = sqlx::query_as(
"
SELECT
@@ -844,7 +847,7 @@ impl Postgres {
async fn add_user_to_warren(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
user_warren: &UserWarren,
) -> Result<UserWarren, sqlx::Error> {
let user_warren: UserWarren = sqlx::query_as(
@@ -855,14 +858,22 @@ impl Postgres {
can_list_files,
can_read_files,
can_modify_files,
can_delete_files
can_delete_files,
can_list_shares,
can_create_shares,
can_modify_shares,
can_delete_shares
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6
$6,
$7,
$8,
$9,
$10
)
RETURNING
*
@@ -874,6 +885,10 @@ impl Postgres {
.bind(user_warren.can_read_files())
.bind(user_warren.can_modify_files())
.bind(user_warren.can_delete_files())
.bind(user_warren.can_list_shares())
.bind(user_warren.can_create_shares())
.bind(user_warren.can_modify_shares())
.bind(user_warren.can_delete_shares())
.fetch_one(connection)
.await?;
@@ -882,7 +897,7 @@ impl Postgres {
async fn update_user_warren(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
user_warren: &UserWarren,
) -> Result<UserWarren, sqlx::Error> {
let user_warren: UserWarren = sqlx::query_as(
@@ -923,7 +938,7 @@ impl Postgres {
async fn remove_user_from_warren(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
user_id: &Uuid,
warren_id: &Uuid,
) -> Result<UserWarren, sqlx::Error> {

View File

@@ -0,0 +1,74 @@
use std::{str::FromStr as _, time::Duration};
use sqlx::{
ConnectOptions as _, SqlitePool,
sqlite::{SqliteConnectOptions, SqlitePoolOptions},
};
use tokio::task::JoinHandle;
pub mod auth;
pub mod share;
pub mod warrens;
#[derive(Debug, Clone)]
pub struct SqliteConfig {
database_url: String,
}
impl SqliteConfig {
pub fn new(database_url: String) -> Self {
Self { database_url }
}
}
#[derive(Debug, Clone)]
pub struct Sqlite {
pool: SqlitePool,
}
impl Sqlite {
pub async fn new(config: SqliteConfig) -> anyhow::Result<Self> {
let opts = SqliteConnectOptions::from_str(&config.database_url)?
.create_if_missing(true)
.extension_with_entrypoint(
"/var/lib/warren/sqlite_extensions/uuid",
"sqlite3_uuid_init",
)
.disable_statement_logging();
let pool = SqlitePoolOptions::new().connect_with(opts).await?;
sqlx::migrate!("./migrations").run(&pool).await?;
// 3600 seconds = 1 hour
Self::start_cleanup_tasks(pool.clone(), Duration::from_secs(3600));
Ok(Self { pool })
}
pub(super) fn start_cleanup_tasks(pool: SqlitePool, interval: Duration) -> JoinHandle<()> {
tokio::spawn(async move {
loop {
{
let Ok(mut connection) = pool.acquire().await else {
break;
};
if let Ok(count) = Self::delete_expired_auth_sessions(&mut connection).await {
tracing::debug!("Removed {count} expired auth session(s)");
}
if let Ok(count) = Self::delete_expired_shares(&mut connection).await {
tracing::debug!("Deleted {count} expired share(s)");
}
}
tokio::time::sleep(interval).await;
}
tracing::debug!("Session cleanup task stopped");
})
}
}
pub(super) fn is_not_found_error(err: &sqlx::Error) -> bool {
matches!(err, sqlx::Error::RowNotFound)
}

View File

@@ -1,10 +1,10 @@
use anyhow::anyhow;
use argon2::{
Argon2, PasswordHash, PasswordVerifier as _,
password_hash::{PasswordHasher as _, SaltString, rand_core::OsRng},
password_hash::{PasswordHasher as _, SaltString},
};
use chrono::{NaiveDateTime, Utc};
use sqlx::{Acquire as _, PgConnection};
use sqlx::{Acquire as _, SqliteConnection};
use thiserror::Error;
use uuid::Uuid;
@@ -17,7 +17,7 @@ use crate::domain::warren::models::{
warren::HasWarrenId as _,
};
use super::{Postgres, is_not_found_error};
use super::{Sqlite, is_not_found_error};
#[derive(sqlx::FromRow)]
struct ShareRow {
@@ -62,7 +62,7 @@ impl TryFrom<ShareRow> for Share {
}
pub(super) async fn get_share(
connection: &mut PgConnection,
connection: &mut SqliteConnection,
request: GetShareRequest,
) -> anyhow::Result<Share> {
let share_row: ShareRow = sqlx::query_as(
@@ -90,7 +90,7 @@ pub(super) async fn get_share(
}
pub(super) async fn list_shares(
connection: &mut PgConnection,
connection: &mut SqliteConnection,
request: ListSharesRequest,
) -> anyhow::Result<Vec<Share>> {
let share_rows: Vec<ShareRow> = sqlx::query_as(
@@ -126,13 +126,13 @@ pub(super) async fn list_shares(
}
pub(super) async fn create_share(
connection: &mut PgConnection,
connection: &mut SqliteConnection,
request: CreateShareRequest,
) -> anyhow::Result<Share> {
let mut tx = connection.begin().await?;
let password_hash = if let Some(password) = request.base().password() {
let salt = SaltString::generate(&mut OsRng);
let salt = SaltString::generate(&mut argon2::password_hash::rand_core::OsRng);
let argon2 = Argon2::default();
Some(
@@ -164,7 +164,7 @@ pub(super) async fn create_share(
$2,
$3,
$4,
TO_TIMESTAMP($5::double precision / 1000)
datetime($5, 'unixepoch')
)
RETURNING
*
@@ -174,7 +174,7 @@ pub(super) async fn create_share(
.bind(request.warren_id())
.bind(request.base().path())
.bind(password_hash)
.bind(expires_at)
.bind(expires_at.map(|v| v / 1000))
.fetch_one(&mut *tx)
.await?;
@@ -184,7 +184,7 @@ pub(super) async fn create_share(
}
pub(super) async fn delete_share(
connection: &mut PgConnection,
connection: &mut SqliteConnection,
request: DeleteShareRequest,
) -> anyhow::Result<Share> {
let mut tx = connection.begin().await?;
@@ -209,7 +209,7 @@ pub(super) async fn delete_share(
}
pub(super) async fn verify_password(
connection: &mut PgConnection,
connection: &mut SqliteConnection,
request: VerifySharePasswordRequest,
) -> Result<Share, VerifySharePasswordError> {
let share_row: ShareRow = sqlx::query_as(
@@ -264,9 +264,9 @@ pub(super) async fn verify_password(
}
}
impl Postgres {
impl Sqlite {
pub(super) async fn delete_expired_shares(
connection: &mut PgConnection,
connection: &mut SqliteConnection,
) -> Result<u64, sqlx::Error> {
let delete_count = sqlx::query(
"

View File

@@ -1,5 +1,5 @@
use anyhow::{Context as _, anyhow};
use sqlx::{Acquire as _, PgConnection};
use sqlx::{Acquire as _, SqliteConnection};
use uuid::Uuid;
use crate::domain::warren::{
@@ -21,9 +21,9 @@ use crate::domain::warren::{
ports::WarrenRepository,
};
use super::{Postgres, is_not_found_error};
use super::{Sqlite, is_not_found_error};
impl WarrenRepository for Postgres {
impl WarrenRepository for Sqlite {
async fn create_warren(
&self,
request: CreateWarrenRequest,
@@ -220,10 +220,10 @@ impl WarrenRepository for Postgres {
}
}
impl Postgres {
impl Sqlite {
async fn create_warren(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
name: &WarrenName,
path: &AbsoluteFilePath,
) -> Result<Warren, sqlx::Error> {
@@ -254,7 +254,7 @@ impl Postgres {
async fn edit_warren(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
id: &Uuid,
name: &WarrenName,
path: &AbsoluteFilePath,
@@ -287,7 +287,7 @@ impl Postgres {
async fn delete_warren(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
id: &Uuid,
) -> Result<Warren, sqlx::Error> {
let mut tx = connection.begin().await?;
@@ -313,7 +313,7 @@ impl Postgres {
async fn get_warren(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
id: &Uuid,
) -> Result<Warren, sqlx::Error> {
let warren: Warren = sqlx::query_as(
@@ -335,20 +335,28 @@ impl Postgres {
async fn fetch_warrens(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
ids: &[Uuid],
) -> Result<Vec<Warren>, sqlx::Error> {
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Postgres, Warren>(
let mut ids_as_string = ids.into_iter().fold(String::new(), |mut acc, id| {
let encoded = hex::encode(id.as_bytes());
acc.push_str("x'");
acc.push_str(encoded.as_str());
acc.push_str("',");
acc
});
ids_as_string.pop();
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Sqlite, Warren>(&format!(
"
SELECT
*
FROM
warrens
WHERE
id = ANY($1)
id IN ({ids_as_string})
",
)
.bind(ids)
))
.fetch_all(&mut *connection)
.await?;
@@ -357,9 +365,9 @@ impl Postgres {
async fn fetch_all_warrens(
&self,
connection: &mut PgConnection,
connection: &mut SqliteConnection,
) -> Result<Vec<Warren>, sqlx::Error> {
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Postgres, Warren>(
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Sqlite, Warren>(
"
SELECT
*

BIN
backend/warren.db Normal file

Binary file not shown.