use std::path::Path;
use std::sync::Arc;
use rusqlite::{Connection, OptionalExtension, params};
use crate::db::{self, Db};
use crate::error::AppError;
use crate::storage::FileScope;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LibraryRef {
User(u64),
Group(u64),
}
impl LibraryRef {
pub fn parse(name: &str) -> Option<Self> {
let (kind, id) = name.split_at_checked(1)?;
let id = id.parse().ok().filter(|_| !id.starts_with('+'))?;
match kind {
"u" => Some(LibraryRef::User(id)),
"g" => Some(LibraryRef::Group(id)),
_ => None,
}
}
}
impl std::fmt::Display for LibraryRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LibraryRef::User(id) => write!(f, "u{id}"),
LibraryRef::Group(id) => write!(f, "g{id}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct User {
pub id: u64,
pub orcid: Option<String>,
pub username: String,
pub display_name: Option<String>,
pub profile_hidden: bool,
pub disabled: bool,
pub legacy_files: bool,
}
impl User {
pub fn files(&self) -> FileScope {
if self.legacy_files {
FileScope::Legacy
} else {
FileScope::User(self.id)
}
}
}
pub fn now() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs() as i64)
}
pub fn open_connection(path: &Path) -> rusqlite::Result<Connection> {
let mut conn = Connection::open(path)?;
conn.busy_timeout(std::time::Duration::from_secs(30))?;
conn.execute_batch("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;")?;
db::migrate(&mut conn, db::CONTROL)?;
Ok(conn)
}
pub fn user(conn: &Connection, id: u64) -> rusqlite::Result<Option<User>> {
conn.query_row(
"SELECT id, orcid, username, display_name, profile_hidden, disabled, files_layout
FROM user WHERE id = ?1",
[id as i64],
|r| {
Ok(User {
id: r.get::<_, i64>(0)? as u64,
orcid: r.get(1)?,
username: r.get(2)?,
display_name: r.get(3)?,
profile_hidden: r.get(4)?,
disabled: r.get(5)?,
legacy_files: r.get::<_, String>(6)? == "legacy",
})
},
)
.optional()
}
pub fn insert_user(conn: &Connection, user: &User) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO user (id, orcid, username, display_name, profile_hidden, disabled,
files_layout, created_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![
user.id as i64,
user.orcid,
user.username,
user.display_name,
user.profile_hidden,
user.disabled,
if user.legacy_files { "legacy" } else { "user" },
now(),
],
)?;
Ok(())
}
pub fn user_count(conn: &Connection) -> rusqlite::Result<i64> {
conn.query_row("SELECT count(*) FROM user", [], |r| r.get(0))
}
pub fn libraries(conn: &Connection) -> rusqlite::Result<Vec<(LibraryRef, FileScope)>> {
let mut stmt = conn.prepare("SELECT id, files_layout FROM user ORDER BY id")?;
let rows = stmt.query_map([], |r| {
let id = r.get::<_, i64>(0)? as u64;
let legacy = r.get::<_, String>(1)? == "legacy";
Ok((
LibraryRef::User(id),
if legacy {
FileScope::Legacy
} else {
FileScope::User(id)
},
))
})?;
rows.collect()
}
pub fn version_high(conn: &Connection, library: LibraryRef) -> rusqlite::Result<i64> {
Ok(conn
.query_row(
"SELECT version_high FROM library_state WHERE library = ?1",
[library.to_string()],
|r| r.get(0),
)
.optional()?
.unwrap_or(0))
}
pub fn note_version(conn: &Connection, library: LibraryRef, version: i64) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO library_state (library, version_high) VALUES (?1, ?2)
ON CONFLICT (library) DO UPDATE SET version_high = excluded.version_high
WHERE excluded.version_high > library_state.version_high",
params![library.to_string(), version],
)?;
Ok(())
}
pub struct Control {
pub db: Arc<Db>,
}
impl Control {
pub fn open(path: &Path) -> Result<Self, AppError> {
Ok(Control {
db: Db::open_with(path, db::CONTROL, 4)?,
})
}
pub async fn server_id(&self) -> Result<String, AppError> {
self.db.server_id().await
}
pub async fn user(&self, id: u64) -> Result<Option<User>, AppError> {
self.db.read(move |c| user(c, id)).await
}
pub async fn ensure_owner(&self, owner: User) -> Result<(), AppError> {
let id = owner.id;
let outcome = self
.db
.write(move |c| {
let tx = c.transaction()?;
if user(&tx, id)?.is_some() {
return Ok(None);
}
if user_count(&tx)? > 0 {
let ids: Vec<i64> = tx
.prepare("SELECT id FROM user ORDER BY id")?
.query_map([], |r| r.get(0))?
.collect::<rusqlite::Result<_>>()?;
return Ok(Some(ids));
}
insert_user(&tx, &owner)?;
tx.commit()?;
Ok(None)
})
.await?;
match outcome {
None => Ok(()),
Some(ids) => Err(AppError::Internal(format!(
"user {id} (DEFTER_USER_ID) is not among this server's users ({ids:?})"
))),
}
}
pub async fn set_profile_hidden(&self, id: u64, hidden: bool) -> Result<(), AppError> {
self.db
.write(move |c| {
c.execute(
"UPDATE user SET profile_hidden = ?2 WHERE id = ?1",
params![id as i64, hidden],
)?;
Ok(())
})
.await
}
pub async fn set_orcid(&self, id: u64, orcid: String) -> Result<(), AppError> {
self.db
.write(move |c| {
c.execute(
"UPDATE user SET orcid = ?2 WHERE id = ?1",
params![id as i64, orcid],
)?;
Ok(())
})
.await
}
pub async fn meta(&self, key: &'static str) -> Result<Option<String>, AppError> {
self.db.read(move |c| db::get_meta(c, key)).await
}
pub async fn set_meta(&self, values: Vec<(&'static str, String)>) -> Result<(), AppError> {
self.db
.write(move |c| {
let tx = c.transaction()?;
for (key, value) in &values {
db::set_meta(&tx, key, value)?;
}
tx.commit()
})
.await
}
pub async fn version_high(&self, library: LibraryRef) -> Result<i64, AppError> {
self.db.read(move |c| version_high(c, library)).await
}
pub async fn note_version(&self, library: LibraryRef, version: i64) -> Result<(), AppError> {
self.db
.write(move |c| note_version(c, library, version))
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
fn owner(id: u64) -> User {
User {
id,
orcid: None,
username: "owner".into(),
display_name: None,
profile_hidden: false,
disabled: false,
legacy_files: false,
}
}
#[test]
fn names_libraries() {
assert_eq!(LibraryRef::User(12).to_string(), "u12");
assert_eq!(LibraryRef::parse("g3"), Some(LibraryRef::Group(3)));
assert_eq!(LibraryRef::parse("u12"), Some(LibraryRef::User(12)));
for bad in ["", "u", "x1", "u-1", "u+1", "u1.sqlite3", "control"] {
assert_eq!(LibraryRef::parse(bad), None, "{bad}");
}
}
#[tokio::test]
async fn keeps_users_and_what_libraries_served() {
let dir = tempfile::tempdir().unwrap();
let control = Control::open(&dir.path().join("control.sqlite3")).unwrap();
control.ensure_owner(owner(7)).await.unwrap();
control.ensure_owner(owner(7)).await.unwrap();
let refused = control.ensure_owner(owner(8)).await.unwrap_err();
assert!(refused.to_string().contains("[7]"), "{refused}");
let user = control.user(7).await.unwrap().unwrap();
assert_eq!(user.files(), FileScope::User(7));
control.set_profile_hidden(7, true).await.unwrap();
control
.set_orcid(7, "0000-0003-1419-2405".into())
.await
.unwrap();
let user = control.user(7).await.unwrap().unwrap();
assert!(user.profile_hidden);
assert_eq!(user.orcid.as_deref(), Some("0000-0003-1419-2405"));
let library = LibraryRef::User(7);
assert_eq!(control.version_high(library).await.unwrap(), 0);
control.note_version(library, 5).await.unwrap();
control.note_version(library, 3).await.unwrap();
assert_eq!(control.version_high(library).await.unwrap(), 5);
}
#[tokio::test]
async fn counts_every_change() {
let dir = tempfile::tempdir().unwrap();
let control = Control::open(&dir.path().join("control.sqlite3")).unwrap();
let revision = || async {
control
.db
.read(|c| c.query_row("SELECT n FROM revision", [], |r| r.get::<_, i64>(0)))
.await
.unwrap()
};
let before = revision().await;
control.ensure_owner(owner(1)).await.unwrap();
control.note_version(LibraryRef::User(1), 2).await.unwrap();
control
.set_meta(vec![("schema", "{}".into())])
.await
.unwrap();
assert_eq!(revision().await, before + 3);
control.user(1).await.unwrap();
assert_eq!(revision().await, before + 3);
}
}