use crate::constants::{DEFAULT_LIST_LIMIT, DEFAULT_MAX_LIST_LIMIT};
use crate::persistence::sql::entry::EntryEntity;
use crate::{
persistence::sql::{
entities::user::{UserIden, USER_TABLE},
UnifiedExecutor,
},
shared::webdav::{EntryPath, WebDavPath},
};
use sea_query::{Alias, Expr, Iden, Order, PostgresQueryBuilder, Query, SimpleExpr};
use sea_query_binder::SqlxBinder;
use sqlx::{postgres::PgRow, Row};
pub const ENTRY_TABLE: &str = "entries";
pub struct EntryRepository;
impl EntryRepository {
pub async fn create<'a>(
user_id: i32,
path: &WebDavPath,
content_hash: &pubky_common::crypto::Hash,
content_length: u64,
content_type: &str,
executor: &mut UnifiedExecutor<'a>,
) -> Result<i64, sqlx::Error> {
let statement = Query::insert()
.into_table(ENTRY_TABLE)
.columns([
EntryIden::User,
EntryIden::Path,
EntryIden::ContentHash,
EntryIden::ContentLength,
EntryIden::ContentType,
])
.values(vec![
SimpleExpr::Value(user_id.into()),
SimpleExpr::Value(path.as_str().into()),
SimpleExpr::Value(content_hash.as_bytes().to_vec().into()),
SimpleExpr::Value(content_length.into()),
SimpleExpr::Value(content_type.to_string().into()),
])
.expect("Failed to build insert statement")
.returning_col(EntryIden::Id)
.to_owned();
let (query, values) = statement.build_sqlx(PostgresQueryBuilder);
let con = executor.get_con().await?;
let ret_row: PgRow = sqlx::query_with(&query, values).fetch_one(con).await?;
let entry_id: i64 = ret_row.try_get(EntryIden::Id.to_string().as_str())?;
Ok(entry_id)
}
pub async fn get<'a>(
id: i64,
executor: &mut UnifiedExecutor<'a>,
) -> Result<EntryEntity, sqlx::Error> {
let statement = Query::select()
.from(ENTRY_TABLE)
.columns([
(ENTRY_TABLE, EntryIden::Id),
(ENTRY_TABLE, EntryIden::User),
(ENTRY_TABLE, EntryIden::Path),
(ENTRY_TABLE, EntryIden::ContentHash),
(ENTRY_TABLE, EntryIden::ContentLength),
(ENTRY_TABLE, EntryIden::ContentType),
(ENTRY_TABLE, EntryIden::ModifiedAt),
(ENTRY_TABLE, EntryIden::CreatedAt),
])
.column((USER_TABLE, UserIden::PublicKey))
.left_join(
USER_TABLE,
Expr::col((ENTRY_TABLE, EntryIden::User)).eq(Expr::col((USER_TABLE, UserIden::Id))),
)
.and_where(Expr::col((ENTRY_TABLE, EntryIden::Id)).eq(id))
.to_owned();
let (query, values) = statement.build_sqlx(PostgresQueryBuilder);
let con = executor.get_con().await?;
let entry: EntryEntity = sqlx::query_as_with(&query, values).fetch_one(con).await?;
Ok(entry)
}
pub async fn get_by_path<'a>(
path: &EntryPath,
executor: &mut UnifiedExecutor<'a>,
) -> Result<EntryEntity, sqlx::Error> {
let statement = Query::select()
.from(ENTRY_TABLE)
.columns([
(ENTRY_TABLE, EntryIden::Id),
(ENTRY_TABLE, EntryIden::User),
(ENTRY_TABLE, EntryIden::Path),
(ENTRY_TABLE, EntryIden::ContentHash),
(ENTRY_TABLE, EntryIden::ContentLength),
(ENTRY_TABLE, EntryIden::ContentType),
(ENTRY_TABLE, EntryIden::ModifiedAt),
(ENTRY_TABLE, EntryIden::CreatedAt),
])
.column((USER_TABLE, UserIden::PublicKey))
.left_join(
USER_TABLE,
Expr::col((ENTRY_TABLE, EntryIden::User)).eq(Expr::col((USER_TABLE, UserIden::Id))),
)
.and_where(Expr::col((ENTRY_TABLE, EntryIden::Path)).eq(path.path().as_str()))
.and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().z32()))
.to_owned();
let (query, values) = statement.build_sqlx(PostgresQueryBuilder);
let con = executor.get_con().await?;
let entry: EntryEntity = sqlx::query_as_with(&query, values).fetch_one(con).await?;
Ok(entry)
}
pub async fn update<'a>(
entry: &EntryEntity,
executor: &mut UnifiedExecutor<'a>,
) -> Result<(), sqlx::Error> {
let statement = Query::update()
.table(ENTRY_TABLE)
.values(vec![
(
EntryIden::ContentHash,
SimpleExpr::Value(entry.content_hash.as_bytes().to_vec().into()),
),
(
EntryIden::ContentLength,
SimpleExpr::Value(entry.content_length.into()),
),
(
EntryIden::ContentType,
SimpleExpr::Value(entry.content_type.clone().into()),
),
(EntryIden::ModifiedAt, Expr::current_timestamp().into()),
])
.and_where(Expr::col((ENTRY_TABLE, EntryIden::Id)).eq(entry.id))
.to_owned();
let (query, values) = statement.build_sqlx(PostgresQueryBuilder);
let con = executor.get_con().await?;
sqlx::query_with(&query, values).execute(con).await?;
Ok(())
}
pub async fn delete<'a>(
id: i64,
executor: &mut UnifiedExecutor<'a>,
) -> Result<(), sqlx::Error> {
let statement = Query::delete()
.from_table(ENTRY_TABLE)
.and_where(Expr::col((ENTRY_TABLE, EntryIden::Id)).eq(id))
.to_owned();
let (query, values) = statement.build_sqlx(PostgresQueryBuilder);
let con = executor.get_con().await?;
sqlx::query_with(&query, values).execute(con).await?;
Ok(())
}
#[cfg(test)]
pub async fn delete_by_path<'a>(
path: &EntryPath,
executor: &mut UnifiedExecutor<'a>,
) -> Result<(), sqlx::Error> {
let subquery = Query::select()
.column((ENTRY_TABLE, EntryIden::Id))
.from(ENTRY_TABLE)
.left_join(
USER_TABLE,
Expr::col((ENTRY_TABLE, EntryIden::User)).eq(Expr::col((USER_TABLE, UserIden::Id))),
)
.and_where(Expr::col((ENTRY_TABLE, EntryIden::Path)).eq(path.path().as_str()))
.and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().z32()))
.to_owned();
let statement = Query::delete()
.from_table(ENTRY_TABLE)
.and_where(Expr::col((ENTRY_TABLE, EntryIden::Id)).in_subquery(subquery))
.to_owned();
let (query, values) = statement.build_sqlx(PostgresQueryBuilder);
let con = executor.get_con().await?;
sqlx::query_with(&query, values).execute(con).await?;
Ok(())
}
pub async fn contains_directory<'a>(
path: &EntryPath,
executor: &mut UnifiedExecutor<'a>,
) -> Result<bool, sqlx::Error> {
let mut full_path = path.path().to_string();
if !full_path.ends_with("/") {
full_path.push('/');
}
let statement = Query::select()
.from(ENTRY_TABLE)
.expr(Expr::col((ENTRY_TABLE, EntryIden::Id)).count())
.left_join(
USER_TABLE,
Expr::col((ENTRY_TABLE, EntryIden::User)).eq(Expr::col((USER_TABLE, UserIden::Id))),
)
.and_where(Expr::col((ENTRY_TABLE, EntryIden::Path)).like(format!("{}%", full_path))) .and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().z32()))
.limit(1)
.to_owned();
let (query, values) = statement.build_sqlx(PostgresQueryBuilder);
let con = executor.get_con().await?;
let count: i64 = sqlx::query_scalar_with(&query, values)
.fetch_one(con)
.await?;
Ok(count > 0)
}
pub async fn list_shallow<'a>(
path: &EntryPath,
limit: Option<u16>,
cursor: Option<EntryPath>,
reverse: bool,
executor: &mut UnifiedExecutor<'a>,
) -> Result<Vec<EntryPath>, sqlx::Error> {
let mut dir_path = path.path().to_string();
if !dir_path.ends_with("/") {
dir_path.push('/');
}
let inner_statement = Query::select()
.from(ENTRY_TABLE)
.expr(Expr::cust_with_values(
"DISTINCT ON (regpath) regexp_replace(entries.path, '^'||$1||'([^/]*)(\\/?)(.*)?$', $1||'\\1'||'\\2') as regpath",
vec![sea_query::Value::from(dir_path.clone())],
))
.left_join(
USER_TABLE,
Expr::col((ENTRY_TABLE, EntryIden::User)).eq(Expr::col((USER_TABLE, UserIden::Id))),
)
.and_where(Expr::col((ENTRY_TABLE, EntryIden::Path)).like(format!("{}%", dir_path))) .and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().z32()))
.to_owned();
let mut outer_statement = Query::select()
.expr(Expr::col("regpath"))
.from_subquery(inner_statement, Alias::new("t"))
.to_owned();
if reverse {
outer_statement = outer_statement
.order_by_expr(Expr::cust("regpath COLLATE \"C\""), Order::Desc)
.to_owned();
} else {
outer_statement = outer_statement
.order_by_expr(Expr::cust("regpath COLLATE \"C\""), Order::Asc)
.to_owned();
}
if let Some(cursor_entry_path) = cursor {
if reverse {
outer_statement = outer_statement
.and_where(Expr::cust_with_values(
"regpath COLLATE \"C\" < $1",
vec![sea_query::Value::from(cursor_entry_path.path().as_str())],
))
.to_owned();
} else {
outer_statement = outer_statement
.and_where(Expr::cust_with_values(
"regpath COLLATE \"C\" > $1",
vec![sea_query::Value::from(cursor_entry_path.path().as_str())],
))
.to_owned();
}
}
let limit = limit.unwrap_or(DEFAULT_LIST_LIMIT);
let limit = limit.min(DEFAULT_MAX_LIST_LIMIT);
outer_statement = outer_statement.limit(limit.into()).to_owned();
let (query, values) = outer_statement.build_sqlx(PostgresQueryBuilder);
let con = executor.get_con().await?;
let rows: Vec<PgRow> = sqlx::query_with(&query, values).fetch_all(con).await?;
let entries = rows
.iter()
.map(|row| {
let user_pubkey = path.pubkey().clone();
let regpath: String = row.try_get("regpath")?;
let webdav_path =
WebDavPath::new(®path).map_err(|e| sqlx::Error::Decode(e.into()))?;
let entry_path = EntryPath::new(user_pubkey, webdav_path);
Ok(entry_path)
})
.collect::<Result<Vec<EntryPath>, sqlx::Error>>()?;
Ok(entries)
}
pub async fn list_deep<'a>(
path: &EntryPath,
limit: Option<u16>,
cursor: Option<EntryPath>,
reverse: bool,
executor: &mut UnifiedExecutor<'a>,
) -> Result<Vec<EntryPath>, sqlx::Error> {
let mut full_path = path.path().to_string();
if !full_path.ends_with("/") {
full_path.push('/');
}
let mut statement = Query::select()
.from(ENTRY_TABLE)
.columns([(ENTRY_TABLE, EntryIden::Path)])
.left_join(
USER_TABLE,
Expr::col((ENTRY_TABLE, EntryIden::User)).eq(Expr::col((USER_TABLE, UserIden::Id))),
)
.and_where(Expr::col((ENTRY_TABLE, EntryIden::Path)).like(format!("{}%", full_path))) .and_where(Expr::col((USER_TABLE, UserIden::PublicKey)).eq(path.pubkey().z32()))
.to_owned();
if reverse {
statement = statement
.order_by_expr(Expr::cust("entries.path COLLATE \"C\""), Order::Desc)
.to_owned();
} else {
statement = statement
.order_by_expr(Expr::cust("entries.path COLLATE \"C\""), Order::Asc)
.to_owned();
}
if let Some(cursor) = cursor {
if reverse {
statement = statement
.and_where(Expr::cust_with_values(
"entries.path COLLATE \"C\" < $1",
vec![sea_query::Value::from(cursor.path().as_str())],
))
.to_owned();
} else {
statement = statement
.and_where(Expr::cust_with_values(
"entries.path COLLATE \"C\" > $1",
vec![sea_query::Value::from(cursor.path().as_str())],
))
.to_owned();
}
}
let limit = limit.unwrap_or(DEFAULT_LIST_LIMIT);
let limit = limit.min(DEFAULT_MAX_LIST_LIMIT);
statement = statement.limit(limit.into()).to_owned();
let (query, values) = statement.build_sqlx(PostgresQueryBuilder);
let con = executor.get_con().await?;
let rows: Vec<PgRow> = sqlx::query_with(&query, values).fetch_all(con).await?;
let entries = rows
.iter()
.map(|row| {
let user_pubkey = path.pubkey().clone();
let path: String = row.try_get(EntryIden::Path.to_string().as_str())?;
let webdav_path =
WebDavPath::new(&path).map_err(|e| sqlx::Error::Decode(e.into()))?;
let entry_path = EntryPath::new(user_pubkey, webdav_path);
Ok(entry_path)
})
.collect::<Result<Vec<EntryPath>, sqlx::Error>>()?;
Ok(entries)
}
}
#[derive(Iden)]
pub enum EntryIden {
Id,
Path,
User,
ContentHash,
ContentLength,
ContentType,
ModifiedAt,
CreatedAt,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::persistence::sql::{entities::user::UserRepository, SqlDb};
use pubky_common::crypto::Keypair;
use std::collections::HashSet;
#[tokio::test]
#[pubky_test_utils::test]
async fn test_create_get_entry() {
let db = SqlDb::test().await;
let user_pubkey = Keypair::random().public_key();
let user = UserRepository::create(&user_pubkey, &mut db.pool().into())
.await
.unwrap();
let entry_id = EntryRepository::create(
user.id,
&WebDavPath::new("/test").unwrap(),
&pubky_common::crypto::Hash::from_bytes([0; 32]),
100,
"text/plain",
&mut db.pool().into(),
)
.await
.unwrap();
let entry = EntryRepository::get(entry_id, &mut db.pool().into())
.await
.unwrap();
assert_eq!(entry.id, entry_id);
assert_eq!(entry.user_id, user.id);
assert_eq!(
entry.path,
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test").unwrap())
);
assert_eq!(entry.content_hash, [0; 32]);
assert_eq!(entry.content_length, 100);
assert_eq!(entry.content_type, "text/plain");
let entry_by_path = EntryRepository::get_by_path(&entry.path, &mut db.pool().into())
.await
.unwrap();
assert_eq!(entry_by_path.id, entry_id);
EntryRepository::delete_by_path(&entry.path, &mut db.pool().into())
.await
.unwrap();
EntryRepository::get_by_path(&entry.path, &mut db.pool().into())
.await
.expect_err("Entry should be deleted");
}
#[tokio::test]
#[pubky_test_utils::test]
async fn test_list_shallow() {
let db = SqlDb::test().await;
let user_pubkey = Keypair::random().public_key();
let user = UserRepository::create(&user_pubkey, &mut db.pool().into())
.await
.unwrap();
let paths = vec![
"/test/1.txt",
"/test/2.txt",
"/test/3.txt",
"/test/sub1",
"/test/sub1/1/1.txt",
"/test/sub1/2.txt",
"/test/sub2/1.txt",
"/test/sub2/2.txt",
];
for path in paths {
EntryRepository::create(
user.id,
&WebDavPath::new(path).unwrap(),
&pubky_common::crypto::Hash::from_bytes([0; 32]),
100,
"text/plain",
&mut db.pool().into(),
)
.await
.unwrap();
}
let entry_path = EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/").unwrap());
let entries =
EntryRepository::list_shallow(&entry_path, None, None, false, &mut db.pool().into())
.await
.unwrap();
assert_eq!(entries.len(), 6);
assert_eq!(
entries[0],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/1.txt").unwrap())
);
assert_eq!(
entries[1],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/2.txt").unwrap())
);
assert_eq!(
entries[2],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/3.txt").unwrap())
);
assert_eq!(
entries[3],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/sub1").unwrap())
);
assert_eq!(
entries[4],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/sub1/").unwrap())
);
assert_eq!(
entries[5],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/sub2/").unwrap())
);
let entries =
EntryRepository::list_shallow(&entry_path, Some(2), None, false, &mut db.pool().into())
.await
.unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(
entries[0],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/1.txt").unwrap())
);
assert_eq!(
entries[1],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/2.txt").unwrap())
);
let entries = EntryRepository::list_shallow(
&entry_path,
None,
Some(EntryPath::new(
user_pubkey.clone(),
WebDavPath::new("/test/3.txt").unwrap(),
)),
false,
&mut db.pool().into(),
)
.await
.unwrap();
assert_eq!(entries.len(), 3);
assert_eq!(
entries[0],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/sub1").unwrap())
);
assert_eq!(
entries[1],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/sub1/").unwrap())
);
assert_eq!(
entries[2],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/sub2/").unwrap())
);
let entries = EntryRepository::list_shallow(
&entry_path,
Some(2),
Some(EntryPath::new(
user_pubkey.clone(),
WebDavPath::new("/test/3.txt").unwrap(),
)),
false,
&mut db.pool().into(),
)
.await
.unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(
entries[0],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/sub1").unwrap())
);
assert_eq!(
entries[1],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/sub1/").unwrap())
);
let mut set: HashSet<EntryPath> = HashSet::new();
let mut last_cursor: Option<EntryPath> = None;
let mut count = 0;
loop {
count += 1;
let new_entries = EntryRepository::list_shallow(
&entry_path,
Some(2),
last_cursor,
false,
&mut db.pool().into(),
)
.await
.unwrap();
if let Some(last_entry) = new_entries.last() {
last_cursor = Some(last_entry.clone());
} else {
break;
}
for entry in new_entries {
set.insert(entry);
}
if count > 10 {
panic!("Too many loops to pull all entries");
}
}
assert_eq!(set.len(), 6);
}
#[tokio::test]
#[pubky_test_utils::test]
async fn test_list_shallow_reverse() {
let db = SqlDb::test().await;
let user_pubkey = Keypair::random().public_key();
let user = UserRepository::create(&user_pubkey, &mut db.pool().into())
.await
.unwrap();
let paths = vec![
"/test/1.txt",
"/test/2.txt",
"/test/3.txt",
"/test/sub1",
"/test/sub1/1/1.txt",
"/test/sub1/2.txt",
"/test/sub2/1.txt",
"/test/sub2/2.txt",
];
for path in paths {
EntryRepository::create(
user.id,
&WebDavPath::new(path).unwrap(),
&pubky_common::crypto::Hash::from_bytes([0; 32]),
100,
"text/plain",
&mut db.pool().into(),
)
.await
.unwrap();
}
let entry_path = EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/").unwrap());
let entries =
EntryRepository::list_shallow(&entry_path, None, None, false, &mut db.pool().into())
.await
.unwrap();
assert_eq!(entries.len(), 6);
assert_eq!(
entries[0],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/1.txt").unwrap())
);
assert_eq!(
entries[1],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/2.txt").unwrap())
);
assert_eq!(
entries[2],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/3.txt").unwrap())
);
assert_eq!(
entries[3],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/sub1").unwrap())
);
assert_eq!(
entries[4],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/sub1/").unwrap())
);
assert_eq!(
entries[5],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/sub2/").unwrap())
);
let entry_path = EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/").unwrap());
let entries =
EntryRepository::list_shallow(&entry_path, None, None, true, &mut db.pool().into())
.await
.unwrap();
assert_eq!(entries.len(), 6);
assert_eq!(
entries[5],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/1.txt").unwrap())
);
assert_eq!(
entries[4],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/2.txt").unwrap())
);
assert_eq!(
entries[3],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/3.txt").unwrap())
);
assert_eq!(
entries[2],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/sub1").unwrap())
);
assert_eq!(
entries[1],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/sub1/").unwrap())
);
assert_eq!(
entries[0],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/sub2/").unwrap())
);
let cursor = EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/3.txt").unwrap());
let entry_path = EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/").unwrap());
let entries = EntryRepository::list_shallow(
&entry_path,
None,
Some(cursor),
true,
&mut db.pool().into(),
)
.await
.unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(
entries[1],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/1.txt").unwrap())
);
assert_eq!(
entries[0],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/2.txt").unwrap())
);
}
#[tokio::test]
#[pubky_test_utils::test]
async fn test_list_deep() {
let db = SqlDb::test().await;
let user_pubkey = Keypair::random().public_key();
let user = UserRepository::create(&user_pubkey, &mut db.pool().into())
.await
.unwrap();
let paths = vec![
"/test/1.txt",
"/test/2.txt",
"/test/3.txt",
"/test/sub1/1/1.txt",
"/test/sub1/2.txt",
"/test/sub2/1.txt",
"/test/sub2/2.txt",
];
for path in paths {
EntryRepository::create(
user.id,
&WebDavPath::new(path).unwrap(),
&pubky_common::crypto::Hash::from_bytes([0; 32]),
100,
"text/plain",
&mut db.pool().into(),
)
.await
.unwrap();
}
let entry_path = EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/").unwrap());
let entries =
EntryRepository::list_deep(&entry_path, None, None, false, &mut db.pool().into())
.await
.unwrap();
assert_eq!(entries.len(), 7);
let entries =
EntryRepository::list_shallow(&entry_path, Some(2), None, false, &mut db.pool().into())
.await
.unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(
entries[0],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/1.txt").unwrap())
);
assert_eq!(
entries[1],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/2.txt").unwrap())
);
let entries = EntryRepository::list_deep(
&entry_path,
None,
Some(EntryPath::new(
user_pubkey.clone(),
WebDavPath::new("/test/3.txt").unwrap(),
)),
false,
&mut db.pool().into(),
)
.await
.unwrap();
assert_eq!(entries.len(), 4);
assert_eq!(
entries[0],
EntryPath::new(
user_pubkey.clone(),
WebDavPath::new("/test/sub1/1/1.txt").unwrap()
)
);
assert_eq!(
entries[1],
EntryPath::new(
user_pubkey.clone(),
WebDavPath::new("/test/sub1/2.txt").unwrap()
)
);
assert_eq!(
entries[2],
EntryPath::new(
user_pubkey.clone(),
WebDavPath::new("/test/sub2/1.txt").unwrap()
)
);
assert_eq!(
entries[3],
EntryPath::new(
user_pubkey.clone(),
WebDavPath::new("/test/sub2/2.txt").unwrap()
)
);
let entries = EntryRepository::list_deep(
&entry_path,
Some(2),
Some(EntryPath::new(
user_pubkey.clone(),
WebDavPath::new("/test/3.txt").unwrap(),
)),
false,
&mut db.pool().into(),
)
.await
.unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(
entries[0],
EntryPath::new(
user_pubkey.clone(),
WebDavPath::new("/test/sub1/1/1.txt").unwrap()
)
);
assert_eq!(
entries[1],
EntryPath::new(
user_pubkey.clone(),
WebDavPath::new("/test/sub1/2.txt").unwrap()
)
);
let mut set: HashSet<EntryPath> = HashSet::new();
let mut last_cursor: Option<EntryPath> = None;
loop {
let new_entries = EntryRepository::list_deep(
&entry_path,
Some(2),
last_cursor.clone(),
false,
&mut db.pool().into(),
)
.await
.unwrap();
if let Some(last_entry) = new_entries.last() {
last_cursor = Some(last_entry.clone());
} else {
break;
}
for entry in new_entries {
set.insert(entry);
}
}
assert_eq!(set.len(), 7);
}
#[tokio::test]
#[pubky_test_utils::test]
async fn test_list_deep_reverse() {
let db = SqlDb::test().await;
let user_pubkey = Keypair::random().public_key();
let user = UserRepository::create(&user_pubkey, &mut db.pool().into())
.await
.unwrap();
let paths = vec![
"/test/1.txt",
"/test/2.txt",
"/test/3.txt",
"/test/sub1/1/1.txt",
"/test/sub1/2.txt",
"/test/sub2/1.txt",
"/test/sub2/2.txt",
];
for path in paths {
EntryRepository::create(
user.id,
&WebDavPath::new(path).unwrap(),
&pubky_common::crypto::Hash::from_bytes([0; 32]),
100,
"text/plain",
&mut db.pool().into(),
)
.await
.unwrap();
}
let entry_path = EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/").unwrap());
let entries =
EntryRepository::list_deep(&entry_path, None, None, true, &mut db.pool().into())
.await
.unwrap();
assert_eq!(entries.len(), 7);
assert_eq!(
entries[0],
EntryPath::new(
user_pubkey.clone(),
WebDavPath::new("/test/sub2/2.txt").unwrap()
)
);
assert_eq!(
entries[1],
EntryPath::new(
user_pubkey.clone(),
WebDavPath::new("/test/sub2/1.txt").unwrap()
)
);
assert_eq!(
entries[2],
EntryPath::new(
user_pubkey.clone(),
WebDavPath::new("/test/sub1/2.txt").unwrap()
)
);
assert_eq!(
entries[3],
EntryPath::new(
user_pubkey.clone(),
WebDavPath::new("/test/sub1/1/1.txt").unwrap()
)
);
assert_eq!(
entries[4],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/3.txt").unwrap())
);
assert_eq!(
entries[5],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/2.txt").unwrap())
);
assert_eq!(
entries[6],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/1.txt").unwrap())
);
let entry_path = EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/").unwrap());
let cursor = EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/3.txt").unwrap());
let entries = EntryRepository::list_deep(
&entry_path,
None,
Some(cursor),
true,
&mut db.pool().into(),
)
.await
.unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(
entries[0],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/2.txt").unwrap())
);
assert_eq!(
entries[1],
EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/1.txt").unwrap())
);
}
#[tokio::test]
#[pubky_test_utils::test]
async fn test_contains_directory() {
let db = SqlDb::test().await;
let user_pubkey = Keypair::random().public_key();
let user = UserRepository::create(&user_pubkey, &mut db.pool().into())
.await
.unwrap();
let exists = EntryRepository::contains_directory(
&EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/").unwrap()),
&mut db.pool().into(),
)
.await
.unwrap();
assert!(!exists);
EntryRepository::create(
user.id,
&WebDavPath::new("/test/file.txt").unwrap(),
&pubky_common::crypto::Hash::from_bytes([0; 32]),
100,
"text/plain",
&mut db.pool().into(),
)
.await
.unwrap();
let exists = EntryRepository::contains_directory(
&EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/").unwrap()),
&mut db.pool().into(),
)
.await
.unwrap();
assert!(exists);
EntryRepository::create(
user.id,
&WebDavPath::new("/test/sub1").unwrap(),
&pubky_common::crypto::Hash::from_bytes([0; 32]),
100,
"text/plain",
&mut db.pool().into(),
)
.await
.unwrap();
let exists = EntryRepository::contains_directory(
&EntryPath::new(user_pubkey.clone(), WebDavPath::new("/test/sub1").unwrap()),
&mut db.pool().into(),
)
.await
.unwrap();
assert!(!exists);
}
}