From f6ed06de488d3e674367fc21c9a611a8d73f4893 Mon Sep 17 00:00:00 2001 From: StNicolay Date: Thu, 1 Aug 2024 20:30:10 +0300 Subject: [PATCH] get_structure endpoint --- ...b1406552c2b7c69f4f1f02a147df5411e692.json} | 4 +- ...8f35283687a9b13d050ac15f16e2a8cec046f.json | 41 ++++++++++++ Cargo.lock | 13 ++-- Cargo.toml | 2 +- sql/get_folders.sql | 11 ++++ src/auth.rs | 9 +-- src/db/file.rs | 12 ++-- src/db/folder.rs | 45 ++++++++++---- src/endpoints/folder/get_structure.rs | 62 +++++++++++++++++++ src/endpoints/folder/list.rs | 7 ++- src/endpoints/folder/mod.rs | 1 + src/main.rs | 9 +-- 12 files changed, 171 insertions(+), 45 deletions(-) rename .sqlx/{query-9cc887509746b773ebbc8c130331b768f9a1deeab34d56aa3b0a833d718114fe.json => query-3028a7c8ec616933e490ed267967b1406552c2b7c69f4f1f02a147df5411e692.json} (84%) create mode 100644 .sqlx/query-b11a87b3b9f6289e831b1f0cb0e8f35283687a9b13d050ac15f16e2a8cec046f.json create mode 100644 sql/get_folders.sql create mode 100644 src/endpoints/folder/get_structure.rs diff --git a/.sqlx/query-9cc887509746b773ebbc8c130331b768f9a1deeab34d56aa3b0a833d718114fe.json b/.sqlx/query-3028a7c8ec616933e490ed267967b1406552c2b7c69f4f1f02a147df5411e692.json similarity index 84% rename from .sqlx/query-9cc887509746b773ebbc8c130331b768f9a1deeab34d56aa3b0a833d718114fe.json rename to .sqlx/query-3028a7c8ec616933e490ed267967b1406552c2b7c69f4f1f02a147df5411e692.json index 1184e8a..9bea034 100644 --- a/.sqlx/query-9cc887509746b773ebbc8c130331b768f9a1deeab34d56aa3b0a833d718114fe.json +++ b/.sqlx/query-3028a7c8ec616933e490ed267967b1406552c2b7c69f4f1f02a147df5411e692.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT folder_id, owner_id, folder_name, created_at FROM folders WHERE parent_folder_id = $1", + "query": "SELECT folder_id, owner_id, folder_name, created_at FROM folders WHERE folder_id = $1", "describe": { "columns": [ { @@ -36,5 +36,5 @@ false ] }, - "hash": "9cc887509746b773ebbc8c130331b768f9a1deeab34d56aa3b0a833d718114fe" + "hash": "3028a7c8ec616933e490ed267967b1406552c2b7c69f4f1f02a147df5411e692" } diff --git a/.sqlx/query-b11a87b3b9f6289e831b1f0cb0e8f35283687a9b13d050ac15f16e2a8cec046f.json b/.sqlx/query-b11a87b3b9f6289e831b1f0cb0e8f35283687a9b13d050ac15f16e2a8cec046f.json new file mode 100644 index 0000000..9555f51 --- /dev/null +++ b/.sqlx/query-b11a87b3b9f6289e831b1f0cb0e8f35283687a9b13d050ac15f16e2a8cec046f.json @@ -0,0 +1,41 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n f.folder_id,\n owner_id,\n folder_name,\n created_at\nFROM\n folders f\n JOIN permissions p ON f.folder_id = p.folder_id\nWHERE\n parent_folder_id = $1\n AND p.user_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "folder_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "owner_id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "folder_name", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamp" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Int4" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "b11a87b3b9f6289e831b1f0cb0e8f35283687a9b13d050ac15f16e2a8cec046f" +} diff --git a/Cargo.lock b/Cargo.lock index 6200663..909d903 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,9 +321,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca2be1d5c43812bae364ee3f30b3afcb7877cf59f4aeb94c66f313a41d2fac9" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" @@ -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", @@ -1962,9 +1962,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.121" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" dependencies = [ "itoa", "memchr", @@ -2478,6 +2478,7 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "socket2", "tokio-macros", diff --git a/Cargo.toml b/Cargo.toml index 160a9a3..54eda93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,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..9420521 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,25 +45,44 @@ 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_folders( - parent_folder_id: Uuid, +pub async fn get_by_id( + folder_id: Uuid, pool: &Pool, -) -> sqlx::Result> { +) -> 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, -) - .fetch_all(pool) + 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 fn get_folders( + parent_folder_id: Uuid, + user_id: i32, + pool: &Pool, +) -> impl Stream> + '_ { + sqlx::query_file_as!( + FolderWithoutParentId, + "sql/get_folders.sql", + parent_folder_id, + user_id + ) + .fetch(pool) +} + pub async fn name_exists(parent_folder_id: Uuid, name: &str, pool: &Pool) -> sqlx::Result { sqlx::query_file!("sql/name_exists.sql", parent_folder_id, name) .fetch_one(pool) diff --git a/src/endpoints/folder/get_structure.rs b/src/endpoints/folder/get_structure.rs new file mode 100644 index 0000000..7122eed --- /dev/null +++ b/src/endpoints/folder/get_structure.rs @@ -0,0 +1,62 @@ +use futures::TryStreamExt; +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, +} + +impl From for FolderStructure { + fn from(value: db::folder::FolderWithoutParentId) -> Self { + FolderStructure { + folder_base: value, + folders: Vec::new(), + files: Vec::new(), + } + } +} + +#[derive(Debug, Serialize)] +pub struct Response { + folder_id: Uuid, + structure: FolderStructure, +} + +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 mut response = Response { + folder_id, + structure: folder.into(), + }; + let mut stack: Vec<&mut FolderStructure> = vec![&mut response.structure]; + while let Some(folder) = stack.pop() { + let (files, folders) = try_join!( + db::file::get_files(folder_id, &pool), + db::folder::get_folders(folder_id, claims.user_id, &pool) + .map_ok(Into::into) + .try_collect() + ) + .handle_internal()?; + folder.folders = folders; + folder.files = files; + stack.extend(folder.folders.iter_mut()); + } + Ok(Json(response)) +} diff --git a/src/endpoints/folder/list.rs b/src/endpoints/folder/list.rs index 2f092d9..9156a6d 100644 --- a/src/endpoints/folder/list.rs +++ b/src/endpoints/folder/list.rs @@ -1,10 +1,11 @@ +use futures::TryStreamExt; use tokio::try_join; use crate::prelude::*; #[derive(Debug, Deserialize)] pub struct Params { - folder_id: Option, + pub(super) folder_id: Option, } #[derive(Debug, Serialize)] @@ -19,14 +20,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).try_collect() ) .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)