Add vault listing endpoint

This commit is contained in:
Andras Schmelczer 2026-03-28 18:15:43 +00:00
parent 9ae1a5e09e
commit 44947dc3a5
6 changed files with 314 additions and 5 deletions

View file

@ -6,7 +6,7 @@ use log::info;
use models::{
DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId, VaultUpdateId,
};
use sqlx::{ConnectOptions, sqlite::SqliteConnectOptions, types::chrono::Utc};
use sqlx::{ConnectOptions, Connection, sqlite::SqliteConnectOptions, types::chrono::Utc};
pub mod models;
@ -171,6 +171,45 @@ fn rollback_before_acquire(
}
impl Database {
/// Lists all vault IDs that exist on disk (have a `.sqlite` file).
pub async fn list_vaults(&self) -> Result<Vec<VaultId>> {
let mut vaults = Vec::new();
let mut entries = tokio::fs::read_dir(&self.config.databases_directory_path)
.await
.context("Failed to read databases directory")?;
while let Some(entry) = entries.next_entry().await? {
let name = entry.file_name().to_string_lossy().to_string();
if let Some(vault) = name.strip_suffix(".sqlite") {
vaults.push(vault.to_owned());
}
}
vaults.sort();
Ok(vaults)
}
pub async fn get_vault_stats(
&self,
vault: &VaultId,
) -> Result<models::VaultStats> {
let pool = self.get_connection_pool(vault).await?;
let row = sqlx::query!(
r#"
SELECT
(SELECT MIN(updated_date) FROM documents)
AS "created_at: chrono::DateTime<Utc>",
(SELECT COUNT(DISTINCT document_id) FROM latest_document_versions
WHERE is_deleted = false)
AS "document_count!: u32"
"#,
)
.fetch_one(&pool)
.await?;
Ok(models::VaultStats {
created_at: row.created_at,
document_count: row.document_count,
})
}
pub async fn try_new(
config: &DatabaseConfig,
broadcasts: &Broadcasts,
@ -683,6 +722,140 @@ impl Database {
Ok(())
}
/// Return all versions (without content) of a specific document, ordered by `vault_update_id`
pub async fn get_document_versions(
&self,
vault: &VaultId,
document_id: &DocumentId,
connection: Option<&mut SqliteConnection>,
) -> Result<Vec<DocumentVersionWithoutContent>> {
let document_id = document_id.as_hyphenated();
let query = sqlx::query!(
r#"
select
vault_update_id,
document_id as "document_id: Hyphenated",
relative_path,
updated_date as "updated_date: chrono::DateTime<Utc>",
is_deleted,
user_id,
device_id,
length(content) as "content_size: u64"
from documents
where document_id = ?
order by vault_update_id
"#,
document_id,
);
if let Some(conn) = connection {
query.fetch_all(&mut *conn).await
} else {
query
.fetch_all(&self.get_connection_pool(vault).await?)
.await
}
.with_context(|| format!("Cannot fetch document versions for document `{document_id}`"))
.map(|rows| {
rows.into_iter()
.map(|row| DocumentVersionWithoutContent {
vault_update_id: row.vault_update_id,
document_id: row.document_id.into(),
relative_path: row.relative_path,
updated_date: row.updated_date,
is_deleted: row.is_deleted,
user_id: row.user_id,
device_id: row.device_id,
content_size: row.content_size.unwrap_or(0),
})
.collect()
})
}
/// Return all versions across all documents, paginated, ordered by `vault_update_id` DESC
pub async fn get_vault_history(
&self,
vault: &VaultId,
limit: i64,
before_update_id: Option<VaultUpdateId>,
connection: Option<&mut SqliteConnection>,
) -> Result<Vec<DocumentVersionWithoutContent>> {
let map_row = |row: models::VaultHistoryRow| DocumentVersionWithoutContent {
vault_update_id: row.vault_update_id,
document_id: row.document_id,
relative_path: row.relative_path,
updated_date: row.updated_date,
is_deleted: row.is_deleted,
user_id: row.user_id,
device_id: row.device_id,
content_size: row.content_size.unwrap_or(0),
};
if let Some(before) = before_update_id {
let query = sqlx::query_as!(
models::VaultHistoryRow,
r#"
select
vault_update_id,
document_id as "document_id: Hyphenated",
relative_path,
updated_date as "updated_date: chrono::DateTime<Utc>",
is_deleted,
user_id,
device_id,
length(content) as "content_size: u64"
from documents
where vault_update_id < ?
order by vault_update_id desc
limit ?
"#,
before,
limit,
);
let rows = if let Some(conn) = connection {
query.fetch_all(&mut *conn).await
} else {
query
.fetch_all(&self.get_connection_pool(vault).await?)
.await
}
.context("Cannot fetch vault history")?;
Ok(rows.into_iter().map(map_row).collect())
} else {
let query = sqlx::query_as!(
models::VaultHistoryRow,
r#"
select
vault_update_id,
document_id as "document_id: Hyphenated",
relative_path,
updated_date as "updated_date: chrono::DateTime<Utc>",
is_deleted,
user_id,
device_id,
length(content) as "content_size: u64"
from documents
order by vault_update_id desc
limit ?
"#,
limit,
);
let rows = if let Some(conn) = connection {
query.fetch_all(&mut *conn).await
} else {
query
.fetch_all(&self.get_connection_pool(vault).await?)
.await
}
.context("Cannot fetch vault history")?;
Ok(rows.into_iter().map(map_row).collect())
}
}
/// Cleanup idle connection pools that haven't been accessed in more than 5 minutes
async fn cleanup_idle_pools(&self) {
// Collect idle vaults and remove them from the map while holding

View file

@ -77,6 +77,24 @@ pub struct DocumentVersion {
pub device_id: DeviceId,
}
/// Row struct for vault history queries (used by `sqlx::query_as!`)
#[derive(Debug)]
pub struct VaultHistoryRow {
pub vault_update_id: VaultUpdateId,
pub document_id: DocumentId,
pub relative_path: String,
pub updated_date: DateTime<Utc>,
pub is_deleted: bool,
pub user_id: String,
pub device_id: String,
pub content_size: Option<u64>,
}
pub struct VaultStats {
pub created_at: Option<DateTime<Utc>>,
pub document_count: u32,
}
impl From<StoredDocumentVersion> for DocumentVersion {
fn from(value: StoredDocumentVersion) -> Self {
Self {