From e53b8f9ba2d24ce16746e9acb3c0814d785dfef9 Mon Sep 17 00:00:00 2001 From: Dominux Date: Fri, 3 Jan 2025 15:03:49 +0300 Subject: [PATCH] #39 Added search functionallity to backend, testing and debug are needed --- README.md | 6 +++- pentaract/src/repositories/files.rs | 46 +++++++++++++++++++---------- pentaract/src/routers/files.rs | 34 +++++++++++++++++++-- pentaract/src/schemas/files.rs | 26 ++++++++++++++-- pentaract/src/services/files.rs | 12 ++++++++ 5 files changed, 104 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 9189bef..ffa1153 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,13 @@ _Cloud storage system based on using Telegram as a storage so it doesn't use your server filesystem or any other paid cloud storage system underneath the hood._ +**BTC**: `18mquj59AcB4y4VBevdn5HekG5y7gvPYGk` -https://github.com/Dominux/Pentaract/assets/55978340/b62305a7-cae3-4e1c-a509-38e415392dcf +**TON**: `UQDoGRgUIEDA30cko8k-icnI8S5i8QIq2jFvqswNvVUc9F2U` +**USDT TON**: `UQDoGRgUIEDA30cko8k-icnI8S5i8QIq2jFvqswNvVUc9F2U` + +https://github.com/Dominux/Pentaract/assets/55978340/b62305a7-cae3-4e1c-a509-38e415392dcf Pentaract is aimed to take as small disk space as possible. So it does not need any code interpreter/platform to run. The whole app is just several megabytes in size. It also uses Postgres as a database and we try our best to economy space by not creating unneeded fields and tables and to wisely pick proper datatypes. diff --git a/pentaract/src/repositories/files.rs b/pentaract/src/repositories/files.rs index fa9e8aa..2a2d0f4 100644 --- a/pentaract/src/repositories/files.rs +++ b/pentaract/src/repositories/files.rs @@ -97,11 +97,11 @@ impl<'d> FilesRepository<'d> { WHERE storage_id = $3 AND path ~ ('^(' || regexp_quote($1) || regexp_quote($2) || '|' || regexp_quote($1) || ' \(\d+\)' || regexp_quote($2) || ')$') ORDER BY path DESC ) - SELECT + SELECT CASE WHEN NOT EXISTS( - SELECT path - FROM f + SELECT path + FROM f WHERE path = $1 || $2 ) THEN $1 || $2 ELSE @@ -118,9 +118,9 @@ impl<'d> FilesRepository<'d> { ORDER BY i ) SELECT $1 || ' (' || COALESCE(t.next_i, ( - SELECT cte.i + 1 - FROM cte - ORDER BY cte.i DESC + SELECT cte.i + 1 + FROM cte + ORDER BY cte.i DESC LIMIT 1 )) || ')' || $2 FROM cte @@ -206,14 +206,14 @@ impl<'d> FilesRepository<'d> { format!( " - SELECT - DISTINCT {split_part} AS name, + SELECT + DISTINCT {split_part} AS name, $1 || {split_part} = path AS is_file, CASE WHEN $1 || {split_part} = path THEN size ELSE (SELECT SUM(size) FROM {FILES_TABLE} WHERE path LIKE $1 || {split_part} || '/' || '%')::BigInt END AS size - FROM {FILES_TABLE} + FROM {FILES_TABLE} WHERE storage_id = $2 {path_filter} AND is_uploaded AND {split_part} <> ''; " ) @@ -250,6 +250,22 @@ impl<'d> FilesRepository<'d> { Ok(fs_layer) } + pub async fn search( + &self, + search_path: &str, + path: &str, + storage_id: Uuid, + ) -> PentaractResult> { + sqlx::query_as( + format!("SELECT * FROM {FILES_TABLE} WHERE storage_id = $1 AND path ILIKE $2 || '%' || $3 || '%'").as_str(), + ) + .bind(storage_id) + .bind(path) + .bind(search_path) + .fetch_all(self.db) + .await + } + pub async fn get_file_by_path(&self, path: &str, storage_id: Uuid) -> PentaractResult { sqlx::query_as( format!("SELECT * FROM {FILES_TABLE} WHERE storage_id = $1 AND path = $2").as_str(), @@ -289,8 +305,8 @@ impl<'d> FilesRepository<'d> { sqlx::query( format!( " - UPDATE {FILES_TABLE} - SET path = $1 || SUBSTRING(path, {chars_skip}) + UPDATE {FILES_TABLE} + SET path = $1 || SUBSTRING(path, {chars_skip}) WHERE storage_id = $2 AND path LIKE $3 || '%' " ) @@ -328,7 +344,7 @@ impl<'d> FilesRepository<'d> { // deleting file sqlx::query(&format!( " - DELETE FROM {FILES_TABLE} + DELETE FROM {FILES_TABLE} WHERE storage_id = $1 AND path {where_path}; " )) @@ -346,10 +362,10 @@ impl<'d> FilesRepository<'d> { sqlx::query(&format!( " INSERT INTO {FILES_TABLE} (id, path, size, storage_id, is_uploaded) - SELECT $1, $2, 0, $3, true - WHERE + SELECT $1, $2, 0, $3, true + WHERE NOT EXISTS ( - SELECT id + SELECT id FROM {FILES_TABLE} WHERE storage_id = $3 AND path LIKE $2 || '%' ); diff --git a/pentaract/src/routers/files.rs b/pentaract/src/routers/files.rs index 1c00504..91ab39c 100644 --- a/pentaract/src/routers/files.rs +++ b/pentaract/src/routers/files.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, path::Path, sync::Arc}; use axum::{ body::Full, - extract::{DefaultBodyLimit, Multipart, Path as RoutePath, State}, + extract::{DefaultBodyLimit, Multipart, Path as RoutePath, Query, State}, http::StatusCode, middleware, response::{AppendHeaders, IntoResponse, Response}, @@ -20,7 +20,9 @@ use crate::{ }, errors::{PentaractError, PentaractResult}, models::files::InFile, - schemas::files::{InFileSchema, InFolderSchema, UploadParams, IN_FILE_SCHEMA_FIELDS_AMOUNT}, + schemas::files::{ + InFileSchema, InFolderSchema, SearchQuery, UploadParams, IN_FILE_SCHEMA_FIELDS_AMOUNT, + }, services::files::FilesService, }; @@ -45,11 +47,22 @@ impl FilesRouter { State(state): State>, Extension(user): Extension, RoutePath((storage_id, path)): RoutePath<(Uuid, String)>, + query: Query, ) -> impl IntoResponse { let (root_path, path) = path.split_once("/").unwrap_or((&path, "")); match root_path { "tree" => Self::tree(state, user, storage_id, path).await, "download" => Self::download(state, user, storage_id, path).await, + "search" => { + if let Some(search_path) = query.0.search_path { + Self::search(state, user, storage_id, path, &search_path).await + } else { + Err(( + StatusCode::UNPROCESSABLE_ENTITY, + "search_path query parameter is required".to_owned(), + )) + } + } _ => Err((StatusCode::NOT_FOUND, "Not found".to_owned())), } } @@ -203,6 +216,23 @@ impl FilesRouter { .map_err(|e| <(StatusCode, String)>::from(e)) } + /// + /// Need path with trailing slash + /// + async fn search( + state: Arc, + user: AuthUser, + storage_id: Uuid, + path: &str, + search_path: &str, + ) -> Result { + FilesService::new(&state.db, state.tx.clone()) + .search(path, storage_id, search_path, &user) + .await + .map(|files| files.into_iter().map(|file| file.into()).into_response()(headers, body)) + .map_err(|e| <(StatusCode, String)>::from(e)) + } + async fn delete( State(state): State>, Extension(user): Extension, diff --git a/pentaract/src/schemas/files.rs b/pentaract/src/schemas/files.rs index f9b5054..5b585e8 100644 --- a/pentaract/src/schemas/files.rs +++ b/pentaract/src/schemas/files.rs @@ -1,8 +1,8 @@ use axum::body::Bytes; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::common::types::Position; +use crate::{common::types::Position, models::files::File}; #[derive(Deserialize)] pub struct UploadParams { @@ -29,6 +29,23 @@ impl InFileSchema { } } +#[derive(Serialize)] +pub struct SearchFileSchema { + pub id: Uuid, + pub path: String, + pub size: i64, +} + +impl From for SearchFileSchema { + fn from(value: File) -> Self { + Self { + id: value.id, + path: value.path, + size: value.size, + } + } +} + pub const IN_FILE_SCHEMA_FIELDS_AMOUNT: usize = 2; pub struct InFolderSchema { @@ -57,3 +74,8 @@ impl DownloadedChunkSchema { Self { position, data } } } + +#[derive(Deserialize)] +pub struct SearchQuery { + pub search_path: Option, +} diff --git a/pentaract/src/services/files.rs b/pentaract/src/services/files.rs index 699894f..04e62e6 100644 --- a/pentaract/src/services/files.rs +++ b/pentaract/src/services/files.rs @@ -233,6 +233,18 @@ impl<'d> FilesService<'d> { self.repo.list_dir(storage_id, path).await } + pub async fn search( + self, + storage_id: Uuid, + path: &str, + search_path: &str, + user: &AuthUser, + ) -> PentaractResult> { + check_access(&self.access_repo, user.id, storage_id, &AccessType::R).await?; + + self.repo.search(search_path, path, storage_id).await + } + pub async fn rename( &self, old_path: &str,