From 1eba969197825efdba1d900727832c619a9500d0 Mon Sep 17 00:00:00 2001 From: StNicolay Date: Thu, 1 Aug 2024 20:30:10 +0300 Subject: [PATCH] get_structure endpoint --- Cargo.lock | 7 ++- Cargo.toml | 3 +- sql/get_folders.sql | 11 +++++ src/auth.rs | 9 +--- src/db/file.rs | 12 ++--- src/db/folder.rs | 40 ++++++++++++---- src/endpoints/folder/get_structure.rs | 69 +++++++++++++++++++++++++++ src/endpoints/folder/list.rs | 6 +-- src/endpoints/folder/mod.rs | 1 + src/main.rs | 9 +--- 10 files changed, 131 insertions(+), 36 deletions(-) create mode 100644 sql/get_folders.sql create mode 100644 src/endpoints/folder/get_structure.rs diff --git a/Cargo.lock b/Cargo.lock index 6200663..d2b8f96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1042,9 +1042,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown", @@ -1142,6 +1142,7 @@ checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", + "serde", ] [[package]] @@ -1534,6 +1535,7 @@ dependencies = [ "futures", "jsonwebtoken", "oauth2", + "parking_lot", "reqwest 0.12.5", "serde", "sha2", @@ -2478,6 +2480,7 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "socket2", "tokio-macros", diff --git a/Cargo.toml b/Cargo.toml index 160a9a3..0cd24ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ dotenvy = "0.15" futures = "0.3" jsonwebtoken = "9" oauth2 = "4" +parking_lot = { version = "0.12.3", features = ["serde"] } reqwest = { version = "0.12", features = [ "http2", "rustls-tls", @@ -41,7 +42,7 @@ sqlx = { version = "0.8", features = [ "chrono", "uuid", ] } -tokio = { version = "1", features = ["rt-multi-thread"] } +tokio = { version = "1", features = ["parking_lot", "rt-multi-thread"] } tokio-util = { version = "0.7" } tower = { version = "0.4" } tower-http = { version = "0.5", features = [ diff --git a/sql/get_folders.sql b/sql/get_folders.sql new file mode 100644 index 0000000..084f0c5 --- /dev/null +++ b/sql/get_folders.sql @@ -0,0 +1,11 @@ +SELECT + f.folder_id, + owner_id, + folder_name, + created_at +FROM + folders f + JOIN permissions p ON f.folder_id = p.folder_id +WHERE + parent_folder_id = $1 + AND p.user_id = $2 \ No newline at end of file diff --git a/src/auth.rs b/src/auth.rs index 062798b..2a972e0 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -5,21 +5,16 @@ use axum::{ }; use serde::Deserialize; -use crate::AppState; - #[derive(Deserialize, Debug)] pub struct Claims { pub user_id: i32, } #[axum::async_trait] -impl FromRequestParts for Claims { +impl FromRequestParts for Claims { type Rejection = StatusCode; - async fn from_request_parts( - parts: &mut Parts, - _state: &AppState, - ) -> Result { + async fn from_request_parts(parts: &mut Parts, _state: &T) -> Result { match parts.extract().await { Ok(Query(claims)) => Ok(claims), Err(err) => { diff --git a/src/db/file.rs b/src/db/file.rs index 7b7217c..0641ffa 100644 --- a/src/db/file.rs +++ b/src/db/file.rs @@ -33,12 +33,12 @@ pub async fn update(file_id: Uuid, size: i64, hash: Vec, pool: &Pool) -> sql #[derive(Debug, serde::Serialize)] #[allow(clippy::struct_field_names, clippy::module_name_repetitions)] pub struct FileWithoutParentId { - file_id: Uuid, - file_name: String, - file_size: i64, - sha512: String, - created_at: chrono::NaiveDateTime, - updated_at: chrono::NaiveDateTime, + pub file_id: Uuid, + pub file_name: String, + pub file_size: i64, + pub sha512: String, + pub created_at: chrono::NaiveDateTime, + pub updated_at: chrono::NaiveDateTime, } pub async fn get_files(folder_id: Uuid, pool: &Pool) -> sqlx::Result> { diff --git a/src/db/folder.rs b/src/db/folder.rs index d7f9f3b..f85dafb 100644 --- a/src/db/folder.rs +++ b/src/db/folder.rs @@ -33,7 +33,7 @@ pub async fn get_root(user_id: i32, pool: &Pool) -> sqlx::Result { .map(|row| row.folder_id) } -pub async fn get_by_id(id: Option, user_id: i32, pool: &Pool) -> sqlx::Result> { +pub async fn process_id(id: Option, user_id: i32, pool: &Pool) -> sqlx::Result> { match id { Some(id) => get_permissions(id, user_id, pool) .await @@ -45,21 +45,41 @@ pub async fn get_by_id(id: Option, user_id: i32, pool: &Pool) -> sqlx::Res #[derive(Debug, serde::Serialize)] #[allow(clippy::struct_field_names, clippy::module_name_repetitions)] pub struct FolderWithoutParentId { - folder_id: Uuid, - owner_id: i32, - folder_name: String, - created_at: chrono::NaiveDateTime, + pub folder_id: Uuid, + pub owner_id: i32, + pub folder_name: String, + pub created_at: chrono::NaiveDateTime, } +pub async fn get_by_id( + folder_id: Uuid, + pool: &Pool, +) -> sqlx::Result> { + sqlx::query_as!( + FolderWithoutParentId, + "SELECT folder_id, owner_id, folder_name, created_at FROM folders WHERE folder_id = $1", + folder_id + ) + .fetch_optional(pool) + .await +} + +/// Get folders that user can read +/// +/// # Warning +/// +/// This function doesn't check that the user can read the parent folder itself pub async fn get_folders( parent_folder_id: Uuid, + user_id: i32, pool: &Pool, ) -> sqlx::Result> { - sqlx::query_as!( - FolderWithoutParentId, - "SELECT folder_id, owner_id, folder_name, created_at FROM folders WHERE parent_folder_id = $1", - parent_folder_id, -) + sqlx::query_file_as!( + FolderWithoutParentId, + "sql/get_folders.sql", + parent_folder_id, + user_id + ) .fetch_all(pool) .await } diff --git a/src/endpoints/folder/get_structure.rs b/src/endpoints/folder/get_structure.rs new file mode 100644 index 0000000..eaa0a16 --- /dev/null +++ b/src/endpoints/folder/get_structure.rs @@ -0,0 +1,69 @@ +use std::sync::Arc; + +use parking_lot::Mutex; +use tokio::try_join; + +use super::list::Params; +use crate::prelude::*; + +#[derive(Serialize, Debug)] +pub struct FolderStructure { + #[serde(flatten)] + folder_base: db::folder::FolderWithoutParentId, + folders: Vec, + files: Vec, +} + +type WrappedStructure = Arc>; + +impl From for WrappedStructure { + fn from(value: db::folder::FolderWithoutParentId) -> Self { + let fs = FolderStructure { + folder_base: value, + folders: Vec::new(), + files: Vec::new(), + }; + Arc::new(Mutex::new(fs)) + } +} + +#[derive(Debug, Serialize)] +pub struct Response { + folder_id: Uuid, + structure: WrappedStructure, +} + +pub async fn structure( + Query(params): Query, + State(pool): State, + claims: Claims, +) -> Result, StatusCode> { + let folder_id = db::folder::process_id(params.folder_id, claims.user_id, &pool) + .await + .handle_internal()? + .ok_or(StatusCode::NOT_FOUND)?; + let folder = db::folder::get_by_id(folder_id, &pool) + .await + .handle_internal()? + .ok_or(StatusCode::NOT_FOUND)?; + let response = Response { + folder_id, + structure: folder.into(), + }; + // TODO: Spawn tasks instead of a single loop + let mut stack: Vec = vec![Arc::clone(&response.structure)]; + while let Some(folder) = stack.pop() { + let folder_id = folder.lock().folder_base.folder_id; + let (files, folders) = try_join!( + db::file::get_files(folder_id, &pool), + db::folder::get_folders(folder_id, claims.user_id, &pool) + ) + .handle_internal()?; + let folders: Vec<_> = folders.into_iter().map(Into::into).collect(); + stack.extend(folders.iter().cloned()); + let mut lock = folder.lock(); + lock.folders = folders; + lock.files = files; + } + Ok(Json(response)) +} diff --git a/src/endpoints/folder/list.rs b/src/endpoints/folder/list.rs index 2f092d9..dad806e 100644 --- a/src/endpoints/folder/list.rs +++ b/src/endpoints/folder/list.rs @@ -4,7 +4,7 @@ use crate::prelude::*; #[derive(Debug, Deserialize)] pub struct Params { - folder_id: Option, + pub(super) folder_id: Option, } #[derive(Debug, Serialize)] @@ -19,14 +19,14 @@ pub async fn list( State(pool): State, claims: Claims, ) -> Result, StatusCode> { - let folder_id = db::folder::get_by_id(params.folder_id, claims.user_id, &pool) + let folder_id = db::folder::process_id(params.folder_id, claims.user_id, &pool) .await .handle_internal()? .ok_or(StatusCode::NOT_FOUND)?; let (files, folders) = try_join!( db::file::get_files(folder_id, &pool), - db::folder::get_folders(folder_id, &pool) + db::folder::get_folders(folder_id, claims.user_id, &pool) ) .handle_internal()?; diff --git a/src/endpoints/folder/mod.rs b/src/endpoints/folder/mod.rs index 5b29de1..cbe4a3a 100644 --- a/src/endpoints/folder/mod.rs +++ b/src/endpoints/folder/mod.rs @@ -1,3 +1,4 @@ pub mod create; pub mod delete; +pub mod get_structure; pub mod list; diff --git a/src/main.rs b/src/main.rs index 5234a7e..d6a3d52 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,18 +13,12 @@ use tokio::net::TcpListener; type Pool = sqlx::postgres::PgPool; -#[derive(Clone)] +#[derive(Clone, FromRef)] struct AppState { pool: Pool, storage: FileStorage, } -impl FromRef for Pool { - fn from_ref(input: &AppState) -> Self { - input.pool.clone() - } -} - async fn create_test_users(pool: &Pool) -> anyhow::Result<()> { let count = sqlx::query!("SELECT count(user_id) FROM users") .fetch_one(pool) @@ -104,6 +98,7 @@ fn app(state: AppState) -> Router { .post(folder::create::create) .delete(folder::delete::delete), ) + .route("/folders/structure", get(folder::get_structure::structure)) .route( "/permissions", get(permissions::get::get)